Merge changes from topic "KMP-navigation-common" into androidx-main
* changes:
Add a multiplatform SynchronizedObject to the navigation-common.
Configure kotlin multiplatform build in the navigation-common.
diff --git a/activity/activity-compose/build.gradle b/activity/activity-compose/build.gradle
index 00222883..d18f0eb 100644
--- a/activity/activity-compose/build.gradle
+++ b/activity/activity-compose/build.gradle
@@ -46,9 +46,9 @@
androidTestImplementation("androidx.annotation:annotation:1.8.1")
androidTestImplementation("androidx.compose.foundation:foundation-layout:1.6.0")
- androidTestImplementation project(":compose:ui:ui-test-junit4")
- androidTestImplementation project(":compose:material:material")
- androidTestRuntimeOnly project(":compose:test-utils")
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:material:material"))
+ androidTestRuntimeOnly(project(":compose:test-utils"))
androidTestImplementation(project(":compose:foundation:foundation"))
androidTestImplementation(project(":compose:runtime:runtime"))
androidTestImplementation(project(":compose:ui:ui"))
@@ -57,7 +57,7 @@
androidTestImplementation(project(":compose:ui:ui-text"))
androidTestImplementation(project(":lifecycle:lifecycle-common"))
androidTestImplementation(project(":lifecycle:lifecycle-runtime"))
- androidTestImplementation project(":lifecycle:lifecycle-runtime-testing")
+ androidTestImplementation(project(":lifecycle:lifecycle-runtime-testing"))
androidTestImplementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.2")
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
diff --git a/activity/activity-compose/samples/build.gradle b/activity/activity-compose/samples/build.gradle
index d178013..55c901d 100644
--- a/activity/activity-compose/samples/build.gradle
+++ b/activity/activity-compose/samples/build.gradle
@@ -34,12 +34,12 @@
implementation(libs.kotlinStdlib)
implementation(libs.kotlinCoroutinesCore)
- compileOnly project(":annotation:annotation-sampled")
+ compileOnly(project(":annotation:annotation-sampled"))
api("androidx.compose.foundation:foundation-layout:1.0.1")
api("androidx.compose.runtime:runtime:1.0.1")
implementation "androidx.compose.foundation:foundation:1.0.1"
- implementation project(":activity:activity-compose")
- implementation project(":activity:activity")
+ implementation(project(":activity:activity-compose"))
+ implementation(project(":activity:activity"))
implementation("androidx.compose.ui:ui-graphics:1.0.1")
implementation("androidx.compose.ui:ui-text:1.0.1")
implementation("androidx.compose.ui:ui:1.0.1")
diff --git a/activity/activity/src/main/java/androidx/activity/ComponentDialog.kt b/activity/activity/src/main/java/androidx/activity/ComponentDialog.kt
index 83593d6..c8c2782 100644
--- a/activity/activity/src/main/java/androidx/activity/ComponentDialog.kt
+++ b/activity/activity/src/main/java/androidx/activity/ComponentDialog.kt
@@ -60,7 +60,6 @@
return bundle
}
- @Suppress("ClassVerificationFailure") // needed for onBackInvokedDispatcher call
@CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt b/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt
index b26fdae..081d01e 100644
--- a/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt
+++ b/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt
@@ -601,7 +601,7 @@
* Note that this does not check for any Intent handled by
* [ACTION_SYSTEM_FALLBACK_PICK_IMAGES].
*/
- @SuppressLint("ClassVerificationFailure", "NewApi")
+ @SuppressLint("NewApi")
@Deprecated(
message =
"This method is deprecated in favor of isPhotoPickerAvailable(context) " +
@@ -687,7 +687,7 @@
* Android version, the SDK extension version or the picker provided by a system app
* implementing [ACTION_SYSTEM_FALLBACK_PICK_IMAGES].
*/
- @SuppressLint("ClassVerificationFailure", "NewApi")
+ @SuppressLint("NewApi")
@JvmStatic
fun isPhotoPickerAvailable(context: Context): Boolean {
return isSystemPickerAvailable() || isSystemFallbackPickerAvailable(context)
@@ -700,7 +700,7 @@
* Note that this does not check for any Intent handled by
* [ACTION_SYSTEM_FALLBACK_PICK_IMAGES].
*/
- @SuppressLint("ClassVerificationFailure", "NewApi")
+ @SuppressLint("NewApi")
@JvmStatic
internal fun isSystemPickerAvailable(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -871,7 +871,7 @@
}
@CallSuper
- @SuppressLint("NewApi", "ClassVerificationFailure")
+ @SuppressLint("NewApi")
override fun createIntent(context: Context, input: PickVisualMediaRequest): Intent {
// Check to see if the photo picker is available
return if (PickVisualMedia.isSystemPickerAvailable()) {
@@ -949,7 +949,7 @@
*
* @see MediaStore.EXTRA_PICK_IMAGES_MAX
*/
- @SuppressLint("NewApi", "ClassVerificationFailure")
+ @SuppressLint("NewApi")
internal fun getMaxItems() =
if (PickVisualMedia.isSystemPickerAvailable()) {
MediaStore.getPickImagesMaxLimit()
diff --git a/appcompat/appcompat/build.gradle b/appcompat/appcompat/build.gradle
index 27328bd..aa3833d 100644
--- a/appcompat/appcompat/build.gradle
+++ b/appcompat/appcompat/build.gradle
@@ -71,7 +71,7 @@
testImplementation(libs.junit)
testImplementation(libs.robolectric)
- lintPublish project(":appcompat:appcompat-lint")
+ lintPublish(project(":appcompat:appcompat-lint"))
}
android {
@@ -79,11 +79,6 @@
// This disables the builds tools automatic vector -> PNG generation
generatedDensities = []
}
-
- sourceSets {
- main.res.srcDirs += "src/main/res-public"
- }
-
aaptOptions {
additionalParameters "--no-version-vectors"
noCompress "ttf"
diff --git a/appcompat/appcompat/src/main/res-public/values/public_attrs.xml b/appcompat/appcompat/src/main/res/values/public_attrs.xml
similarity index 100%
rename from appcompat/appcompat/src/main/res-public/values/public_attrs.xml
rename to appcompat/appcompat/src/main/res/values/public_attrs.xml
diff --git a/appcompat/appcompat/src/main/res-public/values/public_layouts.xml b/appcompat/appcompat/src/main/res/values/public_layouts.xml
similarity index 100%
rename from appcompat/appcompat/src/main/res-public/values/public_layouts.xml
rename to appcompat/appcompat/src/main/res/values/public_layouts.xml
diff --git a/appcompat/appcompat/src/main/res-public/values/public_styles.xml b/appcompat/appcompat/src/main/res/values/public_styles.xml
similarity index 100%
rename from appcompat/appcompat/src/main/res-public/values/public_styles.xml
rename to appcompat/appcompat/src/main/res/values/public_styles.xml
diff --git a/appsearch/appsearch-builtin-types/api/1.1.0-beta01.txt b/appsearch/appsearch-builtin-types/api/1.1.0-beta01.txt
index c3032f2..afd7275 100644
--- a/appsearch/appsearch-builtin-types/api/1.1.0-beta01.txt
+++ b/appsearch/appsearch-builtin-types/api/1.1.0-beta01.txt
@@ -93,8 +93,8 @@
}
@SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public class BuiltInCorpusFilters {
- method public static androidx.appsearch.app.SearchSpec.Builder searchMobileApplicationCorpus(androidx.appsearch.app.SearchSpec.Builder);
- method public static androidx.appsearch.app.SearchSpec.Builder searchPersonCorpus(androidx.appsearch.app.SearchSpec.Builder);
+ method @RequiresPermission(value=android.Manifest.permission.QUERY_ALL_PACKAGES, conditional=true) public static androidx.appsearch.app.SearchSpec.Builder searchMobileApplicationCorpus(androidx.appsearch.app.SearchSpec.Builder);
+ method @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public static androidx.appsearch.app.SearchSpec.Builder searchPersonCorpus(androidx.appsearch.app.SearchSpec.Builder);
}
@androidx.appsearch.annotation.Document(name="builtin:ContactPoint") public class ContactPoint extends androidx.appsearch.builtintypes.Thing {
diff --git a/appsearch/appsearch-builtin-types/api/current.txt b/appsearch/appsearch-builtin-types/api/current.txt
index c3032f2..afd7275 100644
--- a/appsearch/appsearch-builtin-types/api/current.txt
+++ b/appsearch/appsearch-builtin-types/api/current.txt
@@ -93,8 +93,8 @@
}
@SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public class BuiltInCorpusFilters {
- method public static androidx.appsearch.app.SearchSpec.Builder searchMobileApplicationCorpus(androidx.appsearch.app.SearchSpec.Builder);
- method public static androidx.appsearch.app.SearchSpec.Builder searchPersonCorpus(androidx.appsearch.app.SearchSpec.Builder);
+ method @RequiresPermission(value=android.Manifest.permission.QUERY_ALL_PACKAGES, conditional=true) public static androidx.appsearch.app.SearchSpec.Builder searchMobileApplicationCorpus(androidx.appsearch.app.SearchSpec.Builder);
+ method @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public static androidx.appsearch.app.SearchSpec.Builder searchPersonCorpus(androidx.appsearch.app.SearchSpec.Builder);
}
@androidx.appsearch.annotation.Document(name="builtin:ContactPoint") public class ContactPoint extends androidx.appsearch.builtintypes.Thing {
diff --git a/appsearch/appsearch-builtin-types/api/restricted_1.1.0-beta01.txt b/appsearch/appsearch-builtin-types/api/restricted_1.1.0-beta01.txt
index 62256a0..a929d58 100644
--- a/appsearch/appsearch-builtin-types/api/restricted_1.1.0-beta01.txt
+++ b/appsearch/appsearch-builtin-types/api/restricted_1.1.0-beta01.txt
@@ -95,8 +95,8 @@
}
@SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public class BuiltInCorpusFilters {
- method public static androidx.appsearch.app.SearchSpec.Builder searchMobileApplicationCorpus(androidx.appsearch.app.SearchSpec.Builder);
- method public static androidx.appsearch.app.SearchSpec.Builder searchPersonCorpus(androidx.appsearch.app.SearchSpec.Builder);
+ method @RequiresPermission(value=android.Manifest.permission.QUERY_ALL_PACKAGES, conditional=true) public static androidx.appsearch.app.SearchSpec.Builder searchMobileApplicationCorpus(androidx.appsearch.app.SearchSpec.Builder);
+ method @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public static androidx.appsearch.app.SearchSpec.Builder searchPersonCorpus(androidx.appsearch.app.SearchSpec.Builder);
}
@androidx.appsearch.annotation.Document(name="builtin:ContactPoint") public class ContactPoint extends androidx.appsearch.builtintypes.Thing {
diff --git a/appsearch/appsearch-builtin-types/api/restricted_current.txt b/appsearch/appsearch-builtin-types/api/restricted_current.txt
index 62256a0..a929d58 100644
--- a/appsearch/appsearch-builtin-types/api/restricted_current.txt
+++ b/appsearch/appsearch-builtin-types/api/restricted_current.txt
@@ -95,8 +95,8 @@
}
@SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public class BuiltInCorpusFilters {
- method public static androidx.appsearch.app.SearchSpec.Builder searchMobileApplicationCorpus(androidx.appsearch.app.SearchSpec.Builder);
- method public static androidx.appsearch.app.SearchSpec.Builder searchPersonCorpus(androidx.appsearch.app.SearchSpec.Builder);
+ method @RequiresPermission(value=android.Manifest.permission.QUERY_ALL_PACKAGES, conditional=true) public static androidx.appsearch.app.SearchSpec.Builder searchMobileApplicationCorpus(androidx.appsearch.app.SearchSpec.Builder);
+ method @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public static androidx.appsearch.app.SearchSpec.Builder searchPersonCorpus(androidx.appsearch.app.SearchSpec.Builder);
}
@androidx.appsearch.annotation.Document(name="builtin:ContactPoint") public class ContactPoint extends androidx.appsearch.builtintypes.Thing {
diff --git a/appsearch/appsearch-builtin-types/build.gradle b/appsearch/appsearch-builtin-types/build.gradle
index 17d6d34..e78170c 100644
--- a/appsearch/appsearch-builtin-types/build.gradle
+++ b/appsearch/appsearch-builtin-types/build.gradle
@@ -30,14 +30,14 @@
dependencies {
api(libs.jspecify)
- implementation project(":appsearch:appsearch")
+ implementation(project(":appsearch:appsearch"))
implementation("androidx.core:core:1.8.0")
- annotationProcessor project(":appsearch:appsearch-compiler")
+ annotationProcessor(project(":appsearch:appsearch-compiler"))
- androidTestImplementation project(":appsearch:appsearch")
- androidTestImplementation project(":appsearch:appsearch-local-storage")
- androidTestImplementation project(":appsearch:appsearch-test-util")
+ androidTestImplementation(project(":appsearch:appsearch"))
+ androidTestImplementation(project(":appsearch:appsearch-local-storage"))
+ androidTestImplementation(project(":appsearch:appsearch-test-util"))
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/BuiltInCorpusFilters.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/BuiltInCorpusFilters.java
index a7eb3ba..caaa0aa 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/BuiltInCorpusFilters.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/BuiltInCorpusFilters.java
@@ -15,8 +15,10 @@
*/
package androidx.appsearch.builtintypes;
+import android.Manifest;
import android.content.pm.PackageManager;
+import androidx.annotation.RequiresPermission;
import androidx.appsearch.app.ExperimentalAppSearchApi;
import androidx.appsearch.app.SearchSpec;
import androidx.core.util.Preconditions;
@@ -30,7 +32,8 @@
@ExperimentalAppSearchApi
public class BuiltInCorpusFilters {
/**
- * Adds a filter for the {@link MobileApplication} corpus to the given {@link SearchSpec.Builder}.
+ * Adds a filter for the {@link MobileApplication} corpus to the given {@link
+ * SearchSpec.Builder}.
*
* <p>The MobileApplication corpus is a corpus of all apps on the device. Each document contains
* information about the app such as label, package id, and icon uri. This corpus is useful for
@@ -48,6 +51,7 @@
* @param builder The {@link SearchSpec.Builder} to add the filter to.
* @return The modified {@link SearchSpec.Builder}.
*/
+ @RequiresPermission(value = Manifest.permission.QUERY_ALL_PACKAGES, conditional = true)
public static SearchSpec.@NonNull Builder searchMobileApplicationCorpus(
SearchSpec.@NonNull Builder builder) {
return Preconditions.checkNotNull(builder)
@@ -74,6 +78,7 @@
* @param builder The {@link SearchSpec.Builder} to add the filter to.
* @return The modified {@link SearchSpec.Builder}.
*/
+ @RequiresPermission(value = Manifest.permission.READ_CONTACTS)
public static SearchSpec.@NonNull Builder searchPersonCorpus(
SearchSpec.@NonNull Builder builder) {
return Preconditions.checkNotNull(builder)
diff --git a/appsearch/appsearch-debug-view/build.gradle b/appsearch/appsearch-debug-view/build.gradle
index 8647343..ba6b515 100644
--- a/appsearch/appsearch-debug-view/build.gradle
+++ b/appsearch/appsearch-debug-view/build.gradle
@@ -35,9 +35,9 @@
dependencies {
api(libs.jspecify)
- implementation project(":appsearch:appsearch")
- implementation project(":appsearch:appsearch-local-storage")
- implementation project(":appsearch:appsearch-platform-storage")
+ implementation(project(":appsearch:appsearch"))
+ implementation(project(":appsearch:appsearch-local-storage"))
+ implementation(project(":appsearch:appsearch-platform-storage"))
implementation("androidx.concurrent:concurrent-futures:1.0.0")
implementation("androidx.constraintlayout:constraintlayout:2.2.0-beta01")
implementation("androidx.core:core:1.5.0")
diff --git a/appsearch/appsearch-debug-view/samples/build.gradle b/appsearch/appsearch-debug-view/samples/build.gradle
index 1adaced..c681993 100644
--- a/appsearch/appsearch-debug-view/samples/build.gradle
+++ b/appsearch/appsearch-debug-view/samples/build.gradle
@@ -34,13 +34,13 @@
}
dependencies {
- annotationProcessor project(":appsearch:appsearch-compiler")
+ annotationProcessor(project(":appsearch:appsearch-compiler"))
api("androidx.annotation:annotation:1.1.0")
- implementation project(":appsearch:appsearch")
- implementation project(":appsearch:appsearch-local-storage")
- implementation project(":appsearch:appsearch-debug-view")
+ implementation(project(":appsearch:appsearch"))
+ implementation(project(":appsearch:appsearch-local-storage"))
+ implementation(project(":appsearch:appsearch-debug-view"))
implementation("androidx.appcompat:appcompat:1.2.0")
implementation("androidx.concurrent:concurrent-futures:1.0.0")
implementation("com.google.android.material:material:1.0.0")
diff --git a/appsearch/appsearch-ktx/build.gradle b/appsearch/appsearch-ktx/build.gradle
index 22e454b..1910a48 100644
--- a/appsearch/appsearch-ktx/build.gradle
+++ b/appsearch/appsearch-ktx/build.gradle
@@ -33,10 +33,10 @@
dependencies {
api(libs.kotlinStdlib)
- kaptAndroidTest project(":appsearch:appsearch-compiler")
- androidTestImplementation project(":appsearch:appsearch")
- androidTestImplementation project(":appsearch:appsearch-local-storage")
- androidTestImplementation project(":appsearch:appsearch-test-util")
+ kaptAndroidTest(project(":appsearch:appsearch-compiler"))
+ androidTestImplementation(project(":appsearch:appsearch"))
+ androidTestImplementation(project(":appsearch:appsearch-local-storage"))
+ androidTestImplementation(project(":appsearch:appsearch-test-util"))
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
diff --git a/appsearch/appsearch-local-storage/build.gradle b/appsearch/appsearch-local-storage/build.gradle
index 255f767..566676d 100644
--- a/appsearch/appsearch-local-storage/build.gradle
+++ b/appsearch/appsearch-local-storage/build.gradle
@@ -88,7 +88,7 @@
implementation("androidx.concurrent:concurrent-futures:1.0.0")
implementation("androidx.core:core:1.6.0")
- androidTestImplementation project(":appsearch:appsearch-test-util")
+ androidTestImplementation(project(":appsearch:appsearch-test-util"))
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
index 7f68e51..af20e81 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
@@ -3701,11 +3701,11 @@
}
StorageInfo storageInfo1 = mAppSearchImpl.getStorageInfoForPackage("package1");
- assertThat(storageInfo1.getBlobSizeBytes()).isEqualTo(15 * 1024);
- assertThat(storageInfo1.getBlobCount()).isEqualTo(2);
+ assertThat(storageInfo1.getBlobsSizeBytes()).isEqualTo(15 * 1024);
+ assertThat(storageInfo1.getBlobsCount()).isEqualTo(2);
StorageInfo storageInfo2 = mAppSearchImpl.getStorageInfoForPackage("package2");
- assertThat(storageInfo2.getBlobSizeBytes()).isEqualTo(20 * 1024);
- assertThat(storageInfo2.getBlobCount()).isEqualTo(1);
+ assertThat(storageInfo2.getBlobsSizeBytes()).isEqualTo(20 * 1024);
+ assertThat(storageInfo2.getBlobsCount()).isEqualTo(1);
}
@@ -3754,11 +3754,11 @@
}
StorageInfo storageInfo1 = mAppSearchImpl.getStorageInfoForDatabase("package", "db1");
- assertThat(storageInfo1.getBlobSizeBytes()).isEqualTo(15 * 1024);
- assertThat(storageInfo1.getBlobCount()).isEqualTo(2);
+ assertThat(storageInfo1.getBlobsSizeBytes()).isEqualTo(15 * 1024);
+ assertThat(storageInfo1.getBlobsCount()).isEqualTo(2);
StorageInfo storageInfo2 = mAppSearchImpl.getStorageInfoForDatabase("package", "db2");
- assertThat(storageInfo2.getBlobSizeBytes()).isEqualTo(20 * 1024);
- assertThat(storageInfo2.getBlobCount()).isEqualTo(1);
+ assertThat(storageInfo2.getBlobsSizeBytes()).isEqualTo(20 * 1024);
+ assertThat(storageInfo2.getBlobsCount()).isEqualTo(1);
}
@Test
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index 390b420..6cb8f15 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -2497,8 +2497,8 @@
blobCount += blobStorageInfoProto.getNumBlobs();
}
}
- storageInfoBuilder.setBlobCount(blobCount)
- .setBlobSizeBytes(blobSizeBytes);
+ storageInfoBuilder.setBlobsCount(blobCount)
+ .setBlobsSizeBytes(blobSizeBytes);
}
/**
diff --git a/appsearch/appsearch-play-services-storage/build.gradle b/appsearch/appsearch-play-services-storage/build.gradle
index 3203f1a..2da9915 100644
--- a/appsearch/appsearch-play-services-storage/build.gradle
+++ b/appsearch/appsearch-play-services-storage/build.gradle
@@ -30,7 +30,7 @@
dependencies {
api(libs.jspecify)
- implementation project(":appsearch:appsearch")
+ implementation(project(":appsearch:appsearch"))
implementation("androidx.core:core:1.12.0")
implementation("androidx.concurrent:concurrent-futures:1.0.0")
implementation("androidx.collection:collection:1.4.2")
diff --git a/appsearch/appsearch/api/1.1.0-beta01.txt b/appsearch/appsearch/api/1.1.0-beta01.txt
index 70b88f0..ee6ec95 100644
--- a/appsearch/appsearch/api/1.1.0-beta01.txt
+++ b/appsearch/appsearch/api/1.1.0-beta01.txt
@@ -962,8 +962,8 @@
public final class StorageInfo {
method public int getAliveDocumentsCount();
method public int getAliveNamespacesCount();
- method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public int getBlobCount();
- method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public long getBlobSizeBytes();
+ method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public int getBlobsCount();
+ method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public long getBlobsSizeBytes();
method public long getSizeBytes();
}
@@ -972,8 +972,8 @@
method public androidx.appsearch.app.StorageInfo build();
method public androidx.appsearch.app.StorageInfo.Builder setAliveDocumentsCount(int);
method public androidx.appsearch.app.StorageInfo.Builder setAliveNamespacesCount(int);
- method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.app.StorageInfo.Builder setBlobCount(int);
- method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.app.StorageInfo.Builder setBlobSizeBytes(long);
+ method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.app.StorageInfo.Builder setBlobsCount(int);
+ method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.app.StorageInfo.Builder setBlobsSizeBytes(long);
method public androidx.appsearch.app.StorageInfo.Builder setSizeBytes(long);
}
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index 70b88f0..ee6ec95 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -962,8 +962,8 @@
public final class StorageInfo {
method public int getAliveDocumentsCount();
method public int getAliveNamespacesCount();
- method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public int getBlobCount();
- method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public long getBlobSizeBytes();
+ method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public int getBlobsCount();
+ method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public long getBlobsSizeBytes();
method public long getSizeBytes();
}
@@ -972,8 +972,8 @@
method public androidx.appsearch.app.StorageInfo build();
method public androidx.appsearch.app.StorageInfo.Builder setAliveDocumentsCount(int);
method public androidx.appsearch.app.StorageInfo.Builder setAliveNamespacesCount(int);
- method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.app.StorageInfo.Builder setBlobCount(int);
- method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.app.StorageInfo.Builder setBlobSizeBytes(long);
+ method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.app.StorageInfo.Builder setBlobsCount(int);
+ method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.app.StorageInfo.Builder setBlobsSizeBytes(long);
method public androidx.appsearch.app.StorageInfo.Builder setSizeBytes(long);
}
diff --git a/appsearch/appsearch/api/restricted_1.1.0-beta01.txt b/appsearch/appsearch/api/restricted_1.1.0-beta01.txt
index 70b88f0..ee6ec95 100644
--- a/appsearch/appsearch/api/restricted_1.1.0-beta01.txt
+++ b/appsearch/appsearch/api/restricted_1.1.0-beta01.txt
@@ -962,8 +962,8 @@
public final class StorageInfo {
method public int getAliveDocumentsCount();
method public int getAliveNamespacesCount();
- method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public int getBlobCount();
- method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public long getBlobSizeBytes();
+ method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public int getBlobsCount();
+ method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public long getBlobsSizeBytes();
method public long getSizeBytes();
}
@@ -972,8 +972,8 @@
method public androidx.appsearch.app.StorageInfo build();
method public androidx.appsearch.app.StorageInfo.Builder setAliveDocumentsCount(int);
method public androidx.appsearch.app.StorageInfo.Builder setAliveNamespacesCount(int);
- method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.app.StorageInfo.Builder setBlobCount(int);
- method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.app.StorageInfo.Builder setBlobSizeBytes(long);
+ method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.app.StorageInfo.Builder setBlobsCount(int);
+ method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.app.StorageInfo.Builder setBlobsSizeBytes(long);
method public androidx.appsearch.app.StorageInfo.Builder setSizeBytes(long);
}
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index 70b88f0..ee6ec95 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -962,8 +962,8 @@
public final class StorageInfo {
method public int getAliveDocumentsCount();
method public int getAliveNamespacesCount();
- method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public int getBlobCount();
- method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public long getBlobSizeBytes();
+ method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public int getBlobsCount();
+ method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public long getBlobsSizeBytes();
method public long getSizeBytes();
}
@@ -972,8 +972,8 @@
method public androidx.appsearch.app.StorageInfo build();
method public androidx.appsearch.app.StorageInfo.Builder setAliveDocumentsCount(int);
method public androidx.appsearch.app.StorageInfo.Builder setAliveNamespacesCount(int);
- method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.app.StorageInfo.Builder setBlobCount(int);
- method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.app.StorageInfo.Builder setBlobSizeBytes(long);
+ method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.app.StorageInfo.Builder setBlobsCount(int);
+ method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.app.StorageInfo.Builder setBlobsSizeBytes(long);
method public androidx.appsearch.app.StorageInfo.Builder setSizeBytes(long);
}
diff --git a/appsearch/appsearch/build.gradle b/appsearch/appsearch/build.gradle
index f4f9c7c..bbf2bdd 100644
--- a/appsearch/appsearch/build.gradle
+++ b/appsearch/appsearch/build.gradle
@@ -46,14 +46,14 @@
implementation("androidx.concurrent:concurrent-futures:1.0.0")
implementation("androidx.core:core:1.9.0")
- annotationProcessor project(":appsearch:appsearch-compiler")
+ annotationProcessor(project(":appsearch:appsearch-compiler"))
- androidTestAnnotationProcessor project(":appsearch:appsearch-compiler")
- androidTestImplementation project(":appsearch:appsearch-builtin-types")
- androidTestImplementation project(":appsearch:appsearch-local-storage")
- androidTestImplementation project(":appsearch:appsearch-platform-storage")
- androidTestImplementation project(":appsearch:appsearch-play-services-storage")
- androidTestImplementation project(":appsearch:appsearch-test-util")
+ androidTestAnnotationProcessor(project(":appsearch:appsearch-compiler"))
+ androidTestImplementation(project(":appsearch:appsearch-builtin-types"))
+ androidTestImplementation(project(":appsearch:appsearch-local-storage"))
+ androidTestImplementation(project(":appsearch:appsearch-platform-storage"))
+ androidTestImplementation(project(":appsearch:appsearch-play-services-storage"))
+ androidTestImplementation(project(":appsearch:appsearch-test-util"))
// Needed to check if PlayServicesAppSearch throws ApiException.
androidTestImplementation("com.google.android.gms:play-services-basement:18.1.0", {
exclude group: "androidx.core", module: "core"
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionBlobCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionBlobCtsTestBase.java
index d5c1437..fd4cb7a 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionBlobCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionBlobCtsTestBase.java
@@ -506,9 +506,9 @@
writeResponse.close();
StorageInfo after = mDb1.getStorageInfoAsync().get();
- assertThat(after.getBlobCount()).isEqualTo(before.getBlobCount() + 2);
- assertThat(after.getBlobSizeBytes()).isEqualTo(
- before.getBlobSizeBytes() + mData1.length + mData2.length);
+ assertThat(after.getBlobsCount()).isEqualTo(before.getBlobsCount() + 2);
+ assertThat(after.getBlobsSizeBytes()).isEqualTo(
+ before.getBlobsSizeBytes() + mData1.length + mData2.length);
}
@Test
@@ -539,22 +539,22 @@
writeResponse.close();
StorageInfo after = mDb1.getStorageInfoAsync().get();
- assertThat(after.getBlobCount()).isEqualTo(before.getBlobCount() + 2);
- assertThat(after.getBlobSizeBytes()).isEqualTo(
- before.getBlobSizeBytes() + mData1.length + mData2.length);
+ assertThat(after.getBlobsCount()).isEqualTo(before.getBlobsCount() + 2);
+ assertThat(after.getBlobsSizeBytes()).isEqualTo(
+ before.getBlobsSizeBytes() + mData1.length + mData2.length);
// remove blob 1
mDb1.removeBlobAsync(ImmutableSet.of(mHandle1)).get();
StorageInfo afterRemove1 = mDb1.getStorageInfoAsync().get();
- assertThat(afterRemove1.getBlobCount()).isEqualTo(before.getBlobCount() + 1);
- assertThat(afterRemove1.getBlobSizeBytes()).isEqualTo(
- before.getBlobSizeBytes() + mData2.length);
+ assertThat(afterRemove1.getBlobsCount()).isEqualTo(before.getBlobsCount() + 1);
+ assertThat(afterRemove1.getBlobsSizeBytes()).isEqualTo(
+ before.getBlobsSizeBytes() + mData2.length);
// remove blob 2
mDb1.removeBlobAsync(ImmutableSet.of(mHandle2)).get();
StorageInfo afterRemove2 = mDb1.getStorageInfoAsync().get();
- assertThat(afterRemove2.getBlobCount()).isEqualTo(before.getBlobCount());
- assertThat(afterRemove2.getBlobSizeBytes()).isEqualTo(before.getBlobSizeBytes());
+ assertThat(afterRemove2.getBlobsCount()).isEqualTo(before.getBlobsCount());
+ assertThat(afterRemove2.getBlobsSizeBytes()).isEqualTo(before.getBlobsSizeBytes());
}
@Test
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/StorageInfoCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/StorageInfoCtsTest.java
index 0b16822..2cedd09 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/StorageInfoCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/StorageInfoCtsTest.java
@@ -60,12 +60,12 @@
public void testBuildStorageInfo_withBlob() {
StorageInfo storageInfo =
new StorageInfo.Builder()
- .setBlobSizeBytes(12L)
- .setBlobCount(20)
+ .setBlobsSizeBytes(12L)
+ .setBlobsCount(20)
.build();
- assertThat(storageInfo.getBlobCount()).isEqualTo(20);
- assertThat(storageInfo.getBlobSizeBytes()).isEqualTo(12L);
+ assertThat(storageInfo.getBlobsCount()).isEqualTo(20);
+ assertThat(storageInfo.getBlobsSizeBytes()).isEqualTo(12L);
}
@Test
@@ -73,7 +73,7 @@
public void testBuildStorageInfo_withBlobDefaults() {
StorageInfo storageInfo = new StorageInfo.Builder().build();
- assertThat(storageInfo.getBlobCount()).isEqualTo(0);
- assertThat(storageInfo.getBlobSizeBytes()).isEqualTo(0L);
+ assertThat(storageInfo.getBlobsCount()).isEqualTo(0);
+ assertThat(storageInfo.getBlobsSizeBytes()).isEqualTo(0L);
}
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java
index d3d0988..70acc62 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java
@@ -46,24 +46,24 @@
@Field(id = 3, getter = "getAliveNamespacesCount")
private int mAliveNamespacesCount;
- @Field(id = 4, getter = "getBlobSizeBytes")
- private long mBlobSizeBytes;
+ @Field(id = 4, getter = "getBlobsSizeBytes")
+ private long mBlobsSizeBytes;
- @Field(id = 5, getter = "getBlobCount")
- private int mBlobCount;
+ @Field(id = 5, getter = "getBlobsCount")
+ private int mBlobsCount;
@Constructor
StorageInfo(
@Param(id = 1) long sizeBytes,
@Param(id = 2) int aliveDocumentsCount,
@Param(id = 3) int aliveNamespacesCount,
- @Param(id = 4) long blobSizeBytes,
- @Param(id = 5) int blobCount) {
+ @Param(id = 4) long blobsSizeBytes,
+ @Param(id = 5) int blobsCount) {
mSizeBytes = sizeBytes;
mAliveDocumentsCount = aliveDocumentsCount;
mAliveNamespacesCount = aliveNamespacesCount;
- mBlobSizeBytes = blobSizeBytes;
- mBlobCount = blobCount;
+ mBlobsSizeBytes = blobsSizeBytes;
+ mBlobsCount = blobsCount;
}
/** Returns the estimated size of the session's database in bytes. */
@@ -101,8 +101,8 @@
*/
@FlaggedApi(Flags.FLAG_ENABLE_BLOB_STORE)
@ExperimentalAppSearchApi
- public long getBlobSizeBytes() {
- return mBlobSizeBytes;
+ public long getBlobsSizeBytes() {
+ return mBlobsSizeBytes;
}
/**
@@ -114,8 +114,8 @@
*/
@FlaggedApi(Flags.FLAG_ENABLE_BLOB_STORE)
@ExperimentalAppSearchApi
- public int getBlobCount() {
- return mBlobCount;
+ public int getBlobsCount() {
+ return mBlobsCount;
}
/** Builder for {@link StorageInfo} objects. */
@@ -123,8 +123,8 @@
private long mSizeBytes;
private int mAliveDocumentsCount;
private int mAliveNamespacesCount;
- private long mBlobSizeBytes;
- private int mBlobCount;
+ private long mBlobsSizeBytes;
+ private int mBlobsCount;
/** Sets the size in bytes. */
@CanIgnoreReturnValue
@@ -151,8 +151,8 @@
@CanIgnoreReturnValue
@FlaggedApi(Flags.FLAG_ENABLE_BLOB_STORE)
@ExperimentalAppSearchApi
- public StorageInfo.@NonNull Builder setBlobSizeBytes(long blobSizeBytes) {
- mBlobSizeBytes = blobSizeBytes;
+ public StorageInfo.@NonNull Builder setBlobsSizeBytes(long blobsSizeBytes) {
+ mBlobsSizeBytes = blobsSizeBytes;
return this;
}
@@ -160,15 +160,15 @@
@CanIgnoreReturnValue
@FlaggedApi(Flags.FLAG_ENABLE_BLOB_STORE)
@ExperimentalAppSearchApi
- public StorageInfo.@NonNull Builder setBlobCount(int blobCount) {
- mBlobCount = blobCount;
+ public StorageInfo.@NonNull Builder setBlobsCount(int blobsCount) {
+ mBlobsCount = blobsCount;
return this;
}
/** Builds a {@link StorageInfo} object. */
public @NonNull StorageInfo build() {
return new StorageInfo(mSizeBytes, mAliveDocumentsCount, mAliveNamespacesCount,
- mBlobSizeBytes, mBlobCount);
+ mBlobsSizeBytes, mBlobsCount);
}
}
diff --git a/benchmark/benchmark-common/api/current.txt b/benchmark/benchmark-common/api/current.txt
index 71e919b..f1f1bfe 100644
--- a/benchmark/benchmark-common/api/current.txt
+++ b/benchmark/benchmark-common/api/current.txt
@@ -126,30 +126,13 @@
property public final String text;
}
- @SuppressCompatibility @RequiresApi(23) @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public final class PerfettoTrace {
- ctor public PerfettoTrace(String path);
- method public String getPath();
- method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- property public final String path;
- field public static final androidx.benchmark.perfetto.PerfettoTrace.Companion Companion;
- }
+}
- public static final class PerfettoTrace.Companion {
- method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+package androidx.benchmark.traceprocessor {
+
+ public final class PerfettoTraceKt {
+ method @SuppressCompatibility @RequiresApi(23) @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public static void record(androidx.benchmark.traceprocessor.PerfettoTrace.Companion, String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.traceprocessor.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+ method @SuppressCompatibility @RequiresApi(23) @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public static void record(androidx.benchmark.traceprocessor.PerfettoTrace.Companion, String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.traceprocessor.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
}
}
diff --git a/benchmark/benchmark-common/api/restricted_current.txt b/benchmark/benchmark-common/api/restricted_current.txt
index ce0b56a..7271b61 100644
--- a/benchmark/benchmark-common/api/restricted_current.txt
+++ b/benchmark/benchmark-common/api/restricted_current.txt
@@ -129,30 +129,13 @@
property public final String text;
}
- @SuppressCompatibility @RequiresApi(23) @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public final class PerfettoTrace {
- ctor public PerfettoTrace(String path);
- method public String getPath();
- method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public static void record(String fileLabel, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- property public final String path;
- field public static final androidx.benchmark.perfetto.PerfettoTrace.Companion Companion;
- }
+}
- public static final class PerfettoTrace.Companion {
- method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, kotlin.jvm.functions.Function0<kotlin.Unit> block);
- method public void record(String fileLabel, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+package androidx.benchmark.traceprocessor {
+
+ public final class PerfettoTraceKt {
+ method @SuppressCompatibility @RequiresApi(23) @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public static void record(androidx.benchmark.traceprocessor.PerfettoTrace.Companion, String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.traceprocessor.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+ method @SuppressCompatibility @RequiresApi(23) @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public static void record(androidx.benchmark.traceprocessor.PerfettoTrace.Companion, String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.traceprocessor.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
}
}
diff --git a/benchmark/benchmark-common/build.gradle b/benchmark/benchmark-common/build.gradle
index e00287e..d1537e3 100644
--- a/benchmark/benchmark-common/build.gradle
+++ b/benchmark/benchmark-common/build.gradle
@@ -79,6 +79,7 @@
implementation(libs.kotlinStdlib)
api("androidx.annotation:annotation:1.8.1")
api("androidx.annotation:annotation-experimental:1.4.1")
+ api(project(":benchmark:benchmark-traceprocessor"))
implementation("androidx.tracing:tracing-ktx:1.0.0")
implementation("androidx.tracing:tracing-perfetto-handshake:1.0.0")
implementation("androidx.test:monitor:1.6.1")
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt
index 6068fa7b..674a181 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt
@@ -19,9 +19,10 @@
import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
import androidx.benchmark.perfetto.PerfettoConfig
import androidx.benchmark.perfetto.PerfettoHelper
-import androidx.benchmark.perfetto.PerfettoTrace
import androidx.benchmark.perfetto.perfettoConfig
import androidx.benchmark.perfetto.validateAndEncode
+import androidx.benchmark.traceprocessor.PerfettoTrace
+import androidx.benchmark.traceprocessor.record
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellTest.kt
index 1ea94ba..2661c62 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellTest.kt
@@ -473,6 +473,55 @@
)
}
+ @Test
+ fun psLineContainsProcess() {
+ // shell executables
+ "root 10065 10061 14848 3932 poll_sched 7bcaf1fc8c S /data/local/tmp/tracebox"
+ .apply {
+ assertTrue(Shell.psLineContainsProcess(this, "tracebox"))
+ assertFalse(Shell.psLineContainsProcess(this, "tracebo"))
+ }
+
+ "root 10109 1 11552 1140 poll_sched 78c86eac8c S ./tracebox"
+ .apply {
+ assertTrue(Shell.psLineContainsProcess(this, "tracebox"))
+ assertFalse(Shell.psLineContainsProcess(this, "tracebo"))
+ }
+
+ // app
+ "u0_a140 9253 9778 15120128 139856 do_epoll_wait 0 S example.app"
+ .apply {
+ assertTrue(Shell.psLineContainsProcess(this, "example.app"))
+ assertFalse(Shell.psLineContainsProcess(this, "example.ap"))
+ }
+ // app subprocess
+ "u0_a140 9253 9778 15120128 139856 do_epoll_wait 0 S example.app:ui"
+ .apply {
+ assertTrue(Shell.psLineContainsProcess(this, "example.app:ui"))
+ assertTrue(Shell.psLineContainsProcess(this, "example.app"))
+ assertFalse(Shell.psLineContainsProcess(this, "example.ap"))
+ }
+ }
+
+ @Test
+ fun fullProcessNameMatchesProcess() {
+ // shell executables
+ assertTrue(Shell.fullProcessNameMatchesProcess("/data/local/tmp/tracebox", "tracebox"))
+ assertFalse(Shell.fullProcessNameMatchesProcess("/data/local/tmp/tracebox", "tracebo"))
+
+ assertTrue(Shell.fullProcessNameMatchesProcess("./tracebox", "tracebox"))
+ assertFalse(Shell.fullProcessNameMatchesProcess("./tracebox", "tracebo"))
+
+ // app
+ assertTrue(Shell.fullProcessNameMatchesProcess("example.app", "example.app"))
+ assertFalse(Shell.fullProcessNameMatchesProcess("example.app", "example.ap"))
+
+ // app subprocess
+ assertTrue(Shell.fullProcessNameMatchesProcess("example.app:ui", "example.app:ui"))
+ assertTrue(Shell.fullProcessNameMatchesProcess("example.app:ui", "example.app"))
+ assertFalse(Shell.fullProcessNameMatchesProcess("example.app:ui", "example.ap"))
+ }
+
private fun pidof(packageName: String): Int? {
return Shell.getPidsForProcess(packageName).firstOrNull()
}
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
index be459d07..c352a21 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
@@ -92,7 +92,7 @@
internal val requireAot: Boolean
internal val requireJitDisabledIfRooted: Boolean
val throwOnMainThreadMeasureRepeated: Boolean // non-internal, used in BenchmarkRule
- val runOnMainDeadlineSeconds: Long // non-internal, used in BenchmarkRule
+ val measureRepeatedOnMainThrowOnDeadline: Boolean // non-internal, used in BenchmarkRule
internal var error: String? = null
internal val additionalTestOutputDir: String?
@@ -332,11 +332,10 @@
dropShadersThrowOnFailure =
arguments.getBenchmarkArgument("dropShaders.throwOnFailure")?.toBoolean() ?: true
- // very relaxed default to start, ideally this would be less than 5 (ANR timeout),
- // but configurability should help experimenting / narrowing over time
- runOnMainDeadlineSeconds =
- arguments.getBenchmarkArgument("runOnMainDeadlineSeconds")?.toLong() ?: 30
- Log.d(BenchmarkState.TAG, "runOnMainDeadlineSeconds $runOnMainDeadlineSeconds")
+ measureRepeatedOnMainThrowOnDeadline =
+ arguments
+ .getBenchmarkArgument("measureRepeatedOnMainThread.throwOnDeadline")
+ ?.toBoolean() ?: true
requireAot = arguments.getBenchmarkArgument("requireAot")?.toBoolean() ?: false
requireJitDisabledIfRooted =
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/CpuInfo.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/CpuInfo.kt
index e09eebe..3fab906 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/CpuInfo.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/CpuInfo.kt
@@ -16,12 +16,10 @@
package androidx.benchmark
-import android.annotation.SuppressLint
import android.util.Log
import java.io.File
import java.io.IOException
-@SuppressLint("ClassVerificationFailure")
internal object CpuInfo {
private const val TAG = "Benchmark"
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
index 500f13f..5e81a00 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
@@ -59,19 +59,24 @@
* As this function is also used for package names (which never have a leading `/`), we simply
* check for either.
*/
- private fun psLineContainsProcess(psOutputLine: String, processName: String): Boolean {
- return psOutputLine.endsWith(" $processName") || psOutputLine.endsWith("/$processName")
+ internal fun psLineContainsProcess(psOutputLine: String, processName: String): Boolean {
+ val processLabel = psOutputLine.substringAfterLast(" ")
+ return processLabel == processName || // exact match
+ processLabel.startsWith("$processName:") || // app subprocess
+ processLabel.endsWith("/$processName") // executable with relative path
}
/**
* Equivalent of [psLineContainsProcess], but to be used with full process name string (e.g.
* from pgrep)
*/
- private fun fullProcessNameMatchesProcess(
+ internal fun fullProcessNameMatchesProcess(
fullProcessName: String,
processName: String
): Boolean {
- return fullProcessName == processName || fullProcessName.endsWith("/$processName")
+ return fullProcessName == processName || // exact match
+ fullProcessName.startsWith("$processName:") || // app subprocess
+ fullProcessName.endsWith("/$processName") // executable with relative path
}
fun connectUiAutomation() {
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoTrace.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoTrace.kt
deleted file mode 100644
index e68e4c5..0000000
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoTrace.kt
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * Copyright 2022 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
- *
- * http://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 androidx.benchmark.perfetto
-
-import androidx.annotation.RequiresApi
-import androidx.benchmark.perfetto.PerfettoCapture.PerfettoSdkConfig.InitialProcessState
-import androidx.test.platform.app.InstrumentationRegistry
-import java.io.File
-
-@RequiresApi(23) // should match PerfettoHelper.MIN_SDK_VERSION
-@ExperimentalPerfettoCaptureApi
-class PerfettoTrace(
- /**
- * Absolute file path of the trace.
- *
- * Note that the trace is not guaranteed to be placed into an app-accessible directory, and may
- * require shell commands to access.
- */
- val path: String
-) {
- companion object {
- /**
- * Record a Perfetto System Trace for the specified [block].
- *
- * ```
- * PerfettoTrace.record("myTrace") {
- * // content in here is traced to myTrace_<timestamp>.perfetto_trace
- * }
- * ```
- *
- * Reentrant Perfetto trace capture is not supported, so this API may not be combined with
- * `BenchmarkRule`, `MacrobenchmarkRule`, or `PerfettoTraceRule`.
- *
- * If the block throws, the trace is still captured and passed to [traceCallback].
- */
- @JvmStatic
- @JvmOverloads
- fun record(
- /**
- * Output trace file names are labelled `<fileLabel>_<timestamp>.perfetto_trace`
- *
- * This timestamp is used for uniqueness when trace files are pulled automatically to
- * Studio.
- */
- fileLabel: String,
- /**
- * Target process to trace with app tag (enables android.os.Trace / androidx.Trace).
- *
- * By default, traces this process.
- */
- appTagPackages: List<String> =
- listOf(InstrumentationRegistry.getInstrumentation().targetContext.packageName),
- /**
- * Process to trace with userspace tracing, i.e. `androidx.tracing:tracing-perfetto`,
- * ignored below API 30.
- *
- * This tracing is lower overhead than standard `android.os.Trace` tracepoints, but is
- * currently experimental.
- */
- userspaceTracingPackage: String? = null,
- /**
- * Callback for trace capture.
- *
- * This callback allows you to process the trace even if the block throws, e.g. during a
- * test failure.
- */
- traceCallback: ((PerfettoTrace) -> Unit)? = null,
- /** Block to be traced. */
- block: () -> Unit
- ) =
- record(
- fileLabel = fileLabel,
- config =
- PerfettoConfig.Benchmark(
- appTagPackages = appTagPackages,
- useStackSamplingConfig = true
- ),
- userspaceTracingPackage = userspaceTracingPackage,
- traceCallback = traceCallback,
- block = block
- )
-
- /**
- * Record a Perfetto System Trace for the specified [block], with a fully custom Perfetto
- * config, either text or binary.
- *
- * ```
- * PerfettoTrace.record("myTrace", config = """...""") {
- * // content in here is traced to myTrace_<timestamp>.perfetto_trace
- * }
- * ```
- *
- * Reentrant Perfetto trace capture is not supported, so this API may not be combined with
- * `BenchmarkRule`, `MacrobenchmarkRule`, or `PerfettoTraceRule`.
- *
- * If the block throws, the trace is still captured and passed to [traceCallback].
- */
- @JvmStatic
- @JvmOverloads
- fun record(
- /**
- * Output trace file names are labelled `<fileLabel>_<timestamp>.perfetto_trace`
- *
- * This timestamp is used for uniqueness when trace files are pulled automatically to
- * Studio.
- */
- fileLabel: String,
- /** Trace recording configuration. */
- config: PerfettoConfig,
- /**
- * Process to emphasize in the tracing UI.
- *
- * Used to emphasize the target process, e.g. by pre-populating Studio trace viewer
- * process selection.
- *
- * Defaults to the test's target process. Note that for self-instrumenting tests that
- * measure another app, you must pass that target app package.
- */
- highlightPackage: String =
- InstrumentationRegistry.getInstrumentation().targetContext.packageName,
- /**
- * Process to trace with userspace tracing, i.e. `androidx.tracing:tracing-perfetto`,
- * ignored below API 30.
- *
- * This tracing is lower overhead than standard `android.os.Trace` tracepoints, but is
- * currently experimental.
- */
- userspaceTracingPackage: String? = null,
- /**
- * Callback for trace capture.
- *
- * This callback allows you to process the trace even if the block throws, e.g. during a
- * test failure.
- */
- traceCallback: ((PerfettoTrace) -> Unit)? = null,
- /** Block to be traced. */
- block: () -> Unit
- ) {
- PerfettoCaptureWrapper()
- .record(
- fileLabel = fileLabel,
- config,
- perfettoSdkConfig =
- userspaceTracingPackage?.let {
- PerfettoCapture.PerfettoSdkConfig(it, InitialProcessState.Unknown)
- },
- traceCallback = { path ->
- File(path)
- .appendUiState(
- UiState(
- timelineStart = null,
- timelineEnd = null,
- highlightPackage = highlightPackage
- )
- )
- traceCallback?.invoke(PerfettoTrace(path))
- },
- block = block
- )
- }
- }
-}
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/traceprocessor/PerfettoTrace.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/traceprocessor/PerfettoTrace.kt
new file mode 100644
index 0000000..c0b60c7
--- /dev/null
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/traceprocessor/PerfettoTrace.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.benchmark.traceprocessor
+
+import androidx.annotation.RequiresApi
+import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
+import androidx.benchmark.perfetto.PerfettoCapture
+import androidx.benchmark.perfetto.PerfettoCapture.PerfettoSdkConfig.InitialProcessState
+import androidx.benchmark.perfetto.PerfettoCaptureWrapper
+import androidx.benchmark.perfetto.PerfettoConfig
+import androidx.benchmark.perfetto.UiState
+import androidx.benchmark.perfetto.appendUiState
+import androidx.test.platform.app.InstrumentationRegistry
+import java.io.File
+
+/**
+ * Record a Perfetto System Trace for the specified [block].
+ *
+ * ```
+ * PerfettoTrace.record("myTrace") {
+ * // content in here is traced to myTrace_<timestamp>.perfetto_trace
+ * }
+ * ```
+ *
+ * Reentrant Perfetto trace capture is not supported, so this API may not be combined with
+ * `BenchmarkRule`, `MacrobenchmarkRule`, or `PerfettoTraceRule`.
+ *
+ * If the block throws, the trace is still captured and passed to [traceCallback].
+ */
+@RequiresApi(23)
+@ExperimentalPerfettoCaptureApi
+fun PerfettoTrace.Companion.record(
+ /**
+ * Output trace file names are labelled `<fileLabel>_<timestamp>.perfetto_trace`
+ *
+ * This timestamp is used for uniqueness when trace files are pulled automatically to Studio.
+ */
+ fileLabel: String,
+ /**
+ * Target process to trace with app tag (enables android.os.Trace / androidx.Trace).
+ *
+ * By default, traces this process.
+ */
+ appTagPackages: List<String> =
+ listOf(InstrumentationRegistry.getInstrumentation().targetContext.packageName),
+ /**
+ * Process to trace with userspace tracing, i.e. `androidx.tracing:tracing-perfetto`, ignored
+ * below API 30.
+ *
+ * This tracing is lower overhead than standard `android.os.Trace` tracepoints, but is currently
+ * experimental.
+ */
+ userspaceTracingPackage: String? = null,
+ /**
+ * Callback for trace capture.
+ *
+ * This callback allows you to process the trace even if the block throws, e.g. during a test
+ * failure.
+ */
+ traceCallback: ((PerfettoTrace) -> Unit)? = null,
+ /** Block to be traced. */
+ block: () -> Unit
+) =
+ record(
+ fileLabel = fileLabel,
+ config =
+ PerfettoConfig.Benchmark(
+ appTagPackages = appTagPackages,
+ useStackSamplingConfig = true
+ ),
+ userspaceTracingPackage = userspaceTracingPackage,
+ traceCallback = traceCallback,
+ block = block
+ )
+
+/**
+ * Record a Perfetto System Trace for the specified [block], with a fully custom Perfetto config,
+ * either text or binary.
+ *
+ * ```
+ * PerfettoTrace.record("myTrace", config = """...""") {
+ * // content in here is traced to myTrace_<timestamp>.perfetto_trace
+ * }
+ * ```
+ *
+ * Reentrant Perfetto trace capture is not supported, so this API may not be combined with
+ * `BenchmarkRule`, `MacrobenchmarkRule`, or `PerfettoTraceRule`.
+ *
+ * If the block throws, the trace is still captured and passed to [traceCallback].
+ */
+@RequiresApi(23)
+@ExperimentalPerfettoCaptureApi
+fun PerfettoTrace.Companion.record(
+ /**
+ * Output trace file names are labelled `<fileLabel>_<timestamp>.perfetto_trace`
+ *
+ * This timestamp is used for uniqueness when trace files are pulled automatically to Studio.
+ */
+ fileLabel: String,
+ /** Trace recording configuration. */
+ config: PerfettoConfig,
+ /**
+ * Process to emphasize in the tracing UI.
+ *
+ * Used to emphasize the target process, e.g. by pre-populating Studio trace viewer process
+ * selection.
+ *
+ * Defaults to the test's target process. Note that for self-instrumenting tests that measure
+ * another app, you must pass that target app package.
+ */
+ highlightPackage: String =
+ InstrumentationRegistry.getInstrumentation().targetContext.packageName,
+ /**
+ * Process to trace with userspace tracing, i.e. `androidx.tracing:tracing-perfetto`, ignored
+ * below API 30.
+ *
+ * This tracing is lower overhead than standard `android.os.Trace` tracepoints, but is currently
+ * experimental.
+ */
+ userspaceTracingPackage: String? = null,
+ /**
+ * Callback for trace capture.
+ *
+ * This callback allows you to process the trace even if the block throws, e.g. during a test
+ * failure.
+ */
+ traceCallback: ((PerfettoTrace) -> Unit)? = null,
+ /** Block to be traced. */
+ block: () -> Unit
+) {
+ PerfettoCaptureWrapper()
+ .record(
+ fileLabel = fileLabel,
+ config,
+ perfettoSdkConfig =
+ userspaceTracingPackage?.let {
+ PerfettoCapture.PerfettoSdkConfig(it, InitialProcessState.Unknown)
+ },
+ traceCallback = { path ->
+ File(path)
+ .appendUiState(
+ UiState(
+ timelineStart = null,
+ timelineEnd = null,
+ highlightPackage = highlightPackage
+ )
+ )
+ traceCallback?.invoke(PerfettoTrace(path))
+ },
+ block = block
+ )
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/build.gradle b/benchmark/benchmark-darwin-gradle-plugin/build.gradle
index 22ffc9d..1566464 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/build.gradle
+++ b/benchmark/benchmark-darwin-gradle-plugin/build.gradle
@@ -50,7 +50,6 @@
androidx {
name = "Benchmarks - Darwin Gradle Plugin"
- publish = Publish.SNAPSHOT_ONLY
type = LibraryType.GRADLE_PLUGIN
inceptionYear = "2022"
description = "AndroidX Benchmarks - Darwin Gradle Plugin"
diff --git a/benchmark/benchmark-junit4/api/current.txt b/benchmark/benchmark-junit4/api/current.txt
index a1baf9b..9cf3391 100644
--- a/benchmark/benchmark-junit4/api/current.txt
+++ b/benchmark/benchmark-junit4/api/current.txt
@@ -23,14 +23,14 @@
@SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public final class PerfettoTraceRule implements org.junit.rules.TestRule {
ctor public PerfettoTraceRule();
- ctor public PerfettoTraceRule(optional boolean enableAppTagTracing, optional boolean enableUserspaceTracing, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback);
+ ctor public PerfettoTraceRule(optional boolean enableAppTagTracing, optional boolean enableUserspaceTracing, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.traceprocessor.PerfettoTrace,kotlin.Unit>? traceCallback);
method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
method public boolean getEnableAppTagTracing();
method public boolean getEnableUserspaceTracing();
- method public kotlin.jvm.functions.Function1<androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? getTraceCallback();
+ method public kotlin.jvm.functions.Function1<androidx.benchmark.traceprocessor.PerfettoTrace,kotlin.Unit>? getTraceCallback();
property public final boolean enableAppTagTracing;
property public final boolean enableUserspaceTracing;
- property public final kotlin.jvm.functions.Function1<androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback;
+ property public final kotlin.jvm.functions.Function1<androidx.benchmark.traceprocessor.PerfettoTrace,kotlin.Unit>? traceCallback;
}
}
diff --git a/benchmark/benchmark-junit4/api/restricted_current.txt b/benchmark/benchmark-junit4/api/restricted_current.txt
index 60b3cc5..13794ef 100644
--- a/benchmark/benchmark-junit4/api/restricted_current.txt
+++ b/benchmark/benchmark-junit4/api/restricted_current.txt
@@ -24,14 +24,14 @@
@SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public final class PerfettoTraceRule implements org.junit.rules.TestRule {
ctor public PerfettoTraceRule();
- ctor public PerfettoTraceRule(optional boolean enableAppTagTracing, optional boolean enableUserspaceTracing, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback);
+ ctor public PerfettoTraceRule(optional boolean enableAppTagTracing, optional boolean enableUserspaceTracing, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.traceprocessor.PerfettoTrace,kotlin.Unit>? traceCallback);
method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
method public boolean getEnableAppTagTracing();
method public boolean getEnableUserspaceTracing();
- method public kotlin.jvm.functions.Function1<androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? getTraceCallback();
+ method public kotlin.jvm.functions.Function1<androidx.benchmark.traceprocessor.PerfettoTrace,kotlin.Unit>? getTraceCallback();
property public final boolean enableAppTagTracing;
property public final boolean enableUserspaceTracing;
- property public final kotlin.jvm.functions.Function1<androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback;
+ property public final kotlin.jvm.functions.Function1<androidx.benchmark.traceprocessor.PerfettoTrace,kotlin.Unit>? traceCallback;
}
}
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
index 616329a..b1afdf0 100644
--- a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
+++ b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
@@ -323,7 +323,9 @@
val localScope = scope
val initialTimeNs = System.nanoTime()
+ // we try to stop next measurement after soft deadline...
val softDeadlineNs = initialTimeNs + TimeUnit.SECONDS.toNanos(2)
+ // ... and throw if took longer than hard deadline
val hardDeadlineNs = initialTimeNs + TimeUnit.SECONDS.toNanos(10)
var timeNs: Long = 0
@@ -355,7 +357,7 @@
resumeScheduled = true
localState.pauseTiming()
- if (timeNs > hardDeadlineNs) {
+ if (timeNs > hardDeadlineNs && Arguments.measureRepeatedOnMainThrowOnDeadline) {
localState.cleanupBeforeThrow()
val overrunInSec = (timeNs - hardDeadlineNs) / 1_000_000_000.0
throw IllegalStateException(
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoTraceRule.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoTraceRule.kt
index dc6cc23f..c4e67a5 100644
--- a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoTraceRule.kt
+++ b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoTraceRule.kt
@@ -20,7 +20,8 @@
import androidx.benchmark.InstrumentationResults
import androidx.benchmark.Profiler
import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
-import androidx.benchmark.perfetto.PerfettoTrace
+import androidx.benchmark.traceprocessor.PerfettoTrace
+import androidx.benchmark.traceprocessor.record
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.rules.TestRule
import org.junit.runner.Description
diff --git a/benchmark/benchmark-macro-junit4/build.gradle b/benchmark/benchmark-macro-junit4/build.gradle
index ac5de85..921cb59 100644
--- a/benchmark/benchmark-macro-junit4/build.gradle
+++ b/benchmark/benchmark-macro-junit4/build.gradle
@@ -77,7 +77,6 @@
"-opt-in=androidx.benchmark.ExperimentalBenchmarkConfigApi",
"-opt-in=androidx.benchmark.macro.ExperimentalMetricApi",
"-opt-in=androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi",
- "-opt-in=androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi",
]
}
}
diff --git a/benchmark/benchmark-macro/api/current.ignore b/benchmark/benchmark-macro/api/current.ignore
new file mode 100644
index 0000000..8799e6e
--- /dev/null
+++ b/benchmark/benchmark-macro/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedPackage: androidx.benchmark.perfetto:
+ Removed package androidx.benchmark.perfetto
diff --git a/benchmark/benchmark-macro/api/current.txt b/benchmark/benchmark-macro/api/current.txt
index 7bcd26d..6e7ee41 100644
--- a/benchmark/benchmark-macro/api/current.txt
+++ b/benchmark/benchmark-macro/api/current.txt
@@ -86,12 +86,12 @@
@SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class MemoryCountersMetric extends androidx.benchmark.macro.TraceMetric {
ctor public MemoryCountersMetric();
- method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
+ method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.traceprocessor.TraceProcessor.Session traceSession);
}
@SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class MemoryUsageMetric extends androidx.benchmark.macro.TraceMetric {
ctor public MemoryUsageMetric(androidx.benchmark.macro.MemoryUsageMetric.Mode mode, optional java.util.List<? extends androidx.benchmark.macro.MemoryUsageMetric.SubMetric> subMetrics);
- method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
+ method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.traceprocessor.TraceProcessor.Session traceSession);
}
public enum MemoryUsageMetric.Mode {
@@ -215,7 +215,12 @@
@SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public abstract class TraceMetric extends androidx.benchmark.macro.Metric {
ctor public TraceMetric();
- method public abstract java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
+ method public abstract java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.traceprocessor.TraceProcessor.Session traceSession);
+ }
+
+ public final class TraceProcessorExtensionsKt {
+ method public static <T> T runServer(androidx.benchmark.traceprocessor.TraceProcessor.Companion, kotlin.jvm.functions.Function1<? super androidx.benchmark.traceprocessor.TraceProcessor,? extends T> block);
+ method public static <T> T runServer(androidx.benchmark.traceprocessor.TraceProcessor.Companion, long timeout, kotlin.jvm.functions.Function1<? super androidx.benchmark.traceprocessor.TraceProcessor,? extends T> block);
}
@SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class TraceSectionMetric extends androidx.benchmark.macro.Metric {
@@ -254,47 +259,3 @@
}
-package androidx.benchmark.perfetto {
-
- @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalPerfettoTraceProcessorApi {
- }
-
- @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public final class PerfettoTraceProcessor {
- ctor public PerfettoTraceProcessor();
- method public <T> T loadTrace(androidx.benchmark.perfetto.PerfettoTrace trace, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor.Session,? extends T> block);
- method public static <T> T runServer(kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
- method public static <T> T runServer(long timeout, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
- field public static final androidx.benchmark.perfetto.PerfettoTraceProcessor.Companion Companion;
- }
-
- public static final class PerfettoTraceProcessor.Companion {
- method public <T> T runServer(kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
- method public static <T> T runServer(long timeout, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
- }
-
- public static final class PerfettoTraceProcessor.Session {
- method public kotlin.sequences.Sequence<androidx.benchmark.perfetto.Row> query(String query);
- method public String queryMetricsJson(java.util.List<java.lang.String> metrics);
- method public byte[] queryMetricsProtoBinary(java.util.List<java.lang.String> metrics);
- method public String queryMetricsProtoText(java.util.List<java.lang.String> metrics);
- method public byte[] rawQuery(@org.intellij.lang.annotations.Language("sql") String query);
- }
-
- @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public final class Row implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<java.lang.String,java.lang.Object?> {
- ctor public Row(java.util.Map<java.lang.String,? extends java.lang.Object?> map);
- method public byte[] bytes(String columnName);
- method public double double(String columnName);
- method public long long(String columnName);
- method public byte[]? nullableBytes(String columnName);
- method public Double? nullableDouble(String columnName);
- method public Long? nullableLong(String columnName);
- method public String? nullableString(String columnName);
- method public String string(String columnName);
- }
-
- public final class RowKt {
- method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public static androidx.benchmark.perfetto.Row rowOf(kotlin.Pair<java.lang.String,? extends java.lang.Object?>... pairs);
- }
-
-}
-
diff --git a/benchmark/benchmark-macro/api/restricted_current.ignore b/benchmark/benchmark-macro/api/restricted_current.ignore
new file mode 100644
index 0000000..8799e6e
--- /dev/null
+++ b/benchmark/benchmark-macro/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedPackage: androidx.benchmark.perfetto:
+ Removed package androidx.benchmark.perfetto
diff --git a/benchmark/benchmark-macro/api/restricted_current.txt b/benchmark/benchmark-macro/api/restricted_current.txt
index adfb9b2..e1bc0af 100644
--- a/benchmark/benchmark-macro/api/restricted_current.txt
+++ b/benchmark/benchmark-macro/api/restricted_current.txt
@@ -99,12 +99,12 @@
@SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class MemoryCountersMetric extends androidx.benchmark.macro.TraceMetric {
ctor public MemoryCountersMetric();
- method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
+ method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.traceprocessor.TraceProcessor.Session traceSession);
}
@SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class MemoryUsageMetric extends androidx.benchmark.macro.TraceMetric {
ctor public MemoryUsageMetric(androidx.benchmark.macro.MemoryUsageMetric.Mode mode, optional java.util.List<? extends androidx.benchmark.macro.MemoryUsageMetric.SubMetric> subMetrics);
- method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
+ method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.traceprocessor.TraceProcessor.Session traceSession);
}
public enum MemoryUsageMetric.Mode {
@@ -237,7 +237,12 @@
@SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public abstract class TraceMetric extends androidx.benchmark.macro.Metric {
ctor public TraceMetric();
- method public abstract java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
+ method public abstract java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.traceprocessor.TraceProcessor.Session traceSession);
+ }
+
+ public final class TraceProcessorExtensionsKt {
+ method public static <T> T runServer(androidx.benchmark.traceprocessor.TraceProcessor.Companion, kotlin.jvm.functions.Function1<? super androidx.benchmark.traceprocessor.TraceProcessor,? extends T> block);
+ method public static <T> T runServer(androidx.benchmark.traceprocessor.TraceProcessor.Companion, long timeout, kotlin.jvm.functions.Function1<? super androidx.benchmark.traceprocessor.TraceProcessor,? extends T> block);
}
@SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class TraceSectionMetric extends androidx.benchmark.macro.Metric {
@@ -276,47 +281,3 @@
}
-package androidx.benchmark.perfetto {
-
- @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalPerfettoTraceProcessorApi {
- }
-
- @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public final class PerfettoTraceProcessor {
- ctor public PerfettoTraceProcessor();
- method public <T> T loadTrace(androidx.benchmark.perfetto.PerfettoTrace trace, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor.Session,? extends T> block);
- method public static <T> T runServer(kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
- method public static <T> T runServer(long timeout, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
- field public static final androidx.benchmark.perfetto.PerfettoTraceProcessor.Companion Companion;
- }
-
- public static final class PerfettoTraceProcessor.Companion {
- method public <T> T runServer(kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
- method public static <T> T runServer(long timeout, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
- }
-
- public static final class PerfettoTraceProcessor.Session {
- method public kotlin.sequences.Sequence<androidx.benchmark.perfetto.Row> query(String query);
- method public String queryMetricsJson(java.util.List<java.lang.String> metrics);
- method public byte[] queryMetricsProtoBinary(java.util.List<java.lang.String> metrics);
- method public String queryMetricsProtoText(java.util.List<java.lang.String> metrics);
- method public byte[] rawQuery(@org.intellij.lang.annotations.Language("sql") String query);
- }
-
- @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public final class Row implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<java.lang.String,java.lang.Object?> {
- ctor public Row(java.util.Map<java.lang.String,? extends java.lang.Object?> map);
- method public byte[] bytes(String columnName);
- method public double double(String columnName);
- method public long long(String columnName);
- method public byte[]? nullableBytes(String columnName);
- method public Double? nullableDouble(String columnName);
- method public Long? nullableLong(String columnName);
- method public String? nullableString(String columnName);
- method public String string(String columnName);
- }
-
- public final class RowKt {
- method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public static androidx.benchmark.perfetto.Row rowOf(kotlin.Pair<java.lang.String,? extends java.lang.Object?>... pairs);
- }
-
-}
-
diff --git a/benchmark/benchmark-macro/build.gradle b/benchmark/benchmark-macro/build.gradle
index 5ec5df2..e602a99 100644
--- a/benchmark/benchmark-macro/build.gradle
+++ b/benchmark/benchmark-macro/build.gradle
@@ -31,7 +31,6 @@
id("AndroidXPlugin")
id("com.android.library")
id("kotlin-android")
- id("com.squareup.wire")
}
android {
@@ -113,62 +112,12 @@
freeCompilerArgs += [
"-opt-in=androidx.benchmark.ExperimentalBenchmarkConfigApi",
"-opt-in=androidx.benchmark.macro.ExperimentalMetricApi",
- "-opt-in=androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi",
"-opt-in=androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi",
+ "-opt-in=androidx.benchmark.traceprocessor.ExperimentalTraceProcessorApi",
]
}
}
-wire {
- kotlin {}
- sourcePath {
- srcDir AndroidXConfig.getPrebuiltsRoot(project).absolutePath + '/androidx/traceprocessor'
-
- // currently, all protos are at same tree depth
- // can add further includes if this stops working
- include 'protos/perfetto/*/*.proto'
- }
-
- prune 'perfetto.protos.AndroidBatteryMetric'
- prune 'perfetto.protos.AndroidBinderMetric'
- prune 'perfetto.protos.AndroidCameraMetric'
- prune 'perfetto.protos.AndroidCameraUnaggregatedMetric'
- prune 'perfetto.protos.AndroidCpuMetric'
- prune 'perfetto.protos.AndroidDisplayMetrics'
- prune 'perfetto.protos.AndroidDmaHeapMetric'
- prune 'perfetto.protos.AndroidDvfsMetric'
- prune 'perfetto.protos.AndroidFastrpcMetric'
- prune 'perfetto.protos.AndroidFrameTimelineMetric'
- prune 'perfetto.protos.AndroidGpuMetric'
- prune 'perfetto.protos.AndroidHwcomposerMetrics'
- prune 'perfetto.protos.AndroidHwuiMetric'
- prune 'perfetto.protos.AndroidIonMetric'
- prune 'perfetto.protos.AndroidIrqRuntimeMetric'
- prune 'perfetto.protos.AndroidJankCujMetric'
- prune 'perfetto.protos.AndroidLmkMetric'
- prune 'perfetto.protos.AndroidLmkReasonMetric'
- prune 'perfetto.protos.AndroidMemoryMetric'
- prune 'perfetto.protos.AndroidMemoryUnaggregatedMetric'
- prune 'perfetto.protos.AndroidMultiuserMetric'
- prune 'perfetto.protos.AndroidNetworkMetric'
- prune 'perfetto.protos.AndroidPackageList'
- prune 'perfetto.protos.AndroidPowerRails'
- prune 'perfetto.protos.AndroidProcessMetadata'
- prune 'perfetto.protos.AndroidRtRuntimeMetric'
- prune 'perfetto.protos.AndroidSimpleperfMetric'
- prune 'perfetto.protos.AndroidSurfaceflingerMetric'
- prune 'perfetto.protos.AndroidTaskNames'
- prune 'perfetto.protos.AndroidTraceQualityMetric'
- prune 'perfetto.protos.G2dMetrics'
- prune 'perfetto.protos.JavaHeapHistogram'
- prune 'perfetto.protos.JavaHeapStats'
- prune 'perfetto.protos.ProcessRenderInfo'
- prune 'perfetto.protos.ProfilerSmaps'
- prune 'perfetto.protos.TraceAnalysisStats'
- prune 'perfetto.protos.TraceMetadata'
- prune 'perfetto.protos.UnsymbolizedFrames'
-}
-
// Define a task dependency so the app is installed before we run macro benchmarks.
afterEvaluate {
// `:benchmark:integration-tests:macrobenchmark-target:installRelease` is not in the compose
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ArtMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ArtMetricTest.kt
index 0b31597..4f63111 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ArtMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ArtMetricTest.kt
@@ -18,7 +18,7 @@
import androidx.benchmark.DeviceInfo
import androidx.benchmark.perfetto.PerfettoHelper
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
@@ -91,7 +91,7 @@
)
metric.configure(captureInfo)
val result =
- PerfettoTraceProcessor.runSingleSessionServer(tracePath) {
+ TraceProcessor.runSingleSessionServer(tracePath) {
metric.getMeasurements(
// note that most args are incorrect here, but currently
// only targetPackageName matters in this context
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/InsightTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/InsightTest.kt
index 68c4be8..129d5e9 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/InsightTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/InsightTest.kt
@@ -21,7 +21,7 @@
import androidx.benchmark.TraceDeepLink
import androidx.benchmark.createInsightSummaries
import androidx.benchmark.macro.perfetto.queryStartupInsights
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -117,7 +117,7 @@
@MediumTest
@Test
fun queryStartupInsights() {
- PerfettoTraceProcessor.runSingleSessionServer(api35ColdStart) {
+ TraceProcessor.runSingleSessionServer(api35ColdStart) {
assertThat(
queryStartupInsights(
helpUrlBase = "https://d.android.com/test#",
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PerfettoTraceRuleTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PerfettoTraceRuleTest.kt
index e67db61..c4cd85f 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PerfettoTraceRuleTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PerfettoTraceRuleTest.kt
@@ -19,8 +19,8 @@
import androidx.benchmark.junit4.PerfettoTraceRule
import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
import androidx.benchmark.perfetto.PerfettoHelper
-import androidx.benchmark.perfetto.PerfettoTrace
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.PerfettoTrace
+import androidx.benchmark.traceprocessor.TraceProcessor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.tracing.trace
@@ -58,7 +58,7 @@
if (PerfettoHelper.isAbiSupported()) {
assertNotNull(trace)
val sliceNameInstances =
- PerfettoTraceProcessor.runSingleSessionServer(trace!!.path) {
+ TraceProcessor.runSingleSessionServer(trace!!.path) {
querySlices(UNIQUE_SLICE_NAME, packageName = null).map { slice
->
slice.name
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt
index 99415a4..44d0c75 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt
@@ -18,7 +18,7 @@
import androidx.benchmark.macro.Metric.Measurement
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
import androidx.test.filters.SdkSuppress
import kotlin.test.assertEquals
import org.junit.Assume.assumeTrue
@@ -44,7 +44,7 @@
PowerCategory.values().associateWith { PowerCategoryDisplayLevel.BREAKDOWN }
val actualMetrics =
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
PowerMetric(PowerMetric.Energy(categories)).getMeasurements(captureInfo, this)
}
@@ -82,7 +82,7 @@
val categories = PowerCategory.values().associateWith { PowerCategoryDisplayLevel.TOTAL }
val actualMetrics =
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
PowerMetric(PowerMetric.Power(categories)).getMeasurements(captureInfo, this)
}
@@ -118,7 +118,7 @@
)
val actualMetrics =
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
PowerMetric(PowerMetric.Power(categories)).getMeasurements(captureInfo, this)
}
@@ -147,7 +147,7 @@
PowerCategory.values().associateWith { PowerCategoryDisplayLevel.BREAKDOWN }
val actualMetrics =
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
PowerMetric(PowerMetric.Energy(categories)).getMeasurements(captureInfo, this)
}
@@ -162,7 +162,7 @@
val traceFile = createTempFileFromAsset("api31_battery_discharge", ".perfetto-trace")
val actualMetrics =
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
PowerMetric(PowerMetric.Battery()).getMeasurements(captureInfo, this)
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
index b3b9c7c..cadb7a0 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
@@ -27,7 +27,7 @@
import androidx.benchmark.perfetto.PerfettoCaptureWrapper
import androidx.benchmark.perfetto.PerfettoConfig
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
@@ -212,7 +212,7 @@
)
metric.configure(captureInfo)
- return PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ return TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
metric.getMeasurements(captureInfo = captureInfo, traceSession = this)
}
}
@@ -237,7 +237,7 @@
metric.configure(captureInfo)
val measurements =
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
metric.getMeasurements(captureInfo = captureInfo, traceSession = this)
}
@@ -321,7 +321,7 @@
block = measureBlock
)!!
- return PerfettoTraceProcessor.runSingleSessionServer(tracePath) {
+ return TraceProcessor.runSingleSessionServer(tracePath) {
metric.getMeasurements(captureInfo = captureInfo, traceSession = this)
}
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceMetricTest.kt
index 279a95d..d31f120 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceMetricTest.kt
@@ -17,7 +17,7 @@
package androidx.benchmark.macro
import androidx.benchmark.perfetto.PerfettoHelper
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
import androidx.test.filters.MediumTest
import org.junit.Assume.assumeTrue
import org.junit.Test
@@ -35,7 +35,7 @@
class ActivityResumeMetric : TraceMetric() {
override fun getMeasurements(
captureInfo: CaptureInfo,
- traceSession: PerfettoTraceProcessor.Session
+ traceSession: TraceProcessor.Session
): List<Measurement> {
val rowSequence =
traceSession.query(
@@ -83,7 +83,7 @@
metric.configure(captureInfo)
val result =
- PerfettoTraceProcessor.runSingleSessionServer(tracePath) {
+ TraceProcessor.runSingleSessionServer(tracePath) {
metric.getMeasurements(captureInfo = captureInfo, traceSession = this)
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt
index b96ad25..27fc565 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt
@@ -17,7 +17,7 @@
package androidx.benchmark.macro
import androidx.benchmark.perfetto.PerfettoHelper
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
import androidx.test.filters.MediumTest
import org.junit.Assume.assumeTrue
import org.junit.Test
@@ -172,7 +172,7 @@
metric.configure(captureInfo)
val result =
- PerfettoTraceProcessor.runSingleSessionServer(tracePath) {
+ TraceProcessor.runSingleSessionServer(tracePath) {
metric.getMeasurements(captureInfo = captureInfo, traceSession = this)
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AndroidxTracingTraceTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AndroidxTracingTraceTest.kt
index a265211..1bc7b49 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AndroidxTracingTraceTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AndroidxTracingTraceTest.kt
@@ -18,12 +18,13 @@
import androidx.benchmark.macro.FileLinkingRule
import androidx.benchmark.macro.Packages
+import androidx.benchmark.macro.runSingleSessionServer
import androidx.benchmark.perfetto.PerfettoCapture
import androidx.benchmark.perfetto.PerfettoConfig
import androidx.benchmark.perfetto.PerfettoHelper
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
-import androidx.benchmark.perfetto.toSlices
+import androidx.benchmark.traceprocessor.TraceProcessor
+import androidx.benchmark.traceprocessor.toSlices
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
@@ -101,7 +102,7 @@
perfettoCapture.stop(traceFilePath)
val queryResult =
- PerfettoTraceProcessor.runSingleSessionServer(traceFilePath) { query(query = QUERY) }
+ TraceProcessor.runSingleSessionServer(traceFilePath) { query(query = QUERY) }
val matchingSlices = queryResult.toSlices()
assertEquals(
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/BatteryDischargeQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/BatteryDischargeQueryTest.kt
index 33c5422..5c8fb6a 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/BatteryDischargeQueryTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/BatteryDischargeQueryTest.kt
@@ -21,8 +21,9 @@
import androidx.benchmark.macro.ExperimentalMetricApi
import androidx.benchmark.macro.PowerMetric
import androidx.benchmark.macro.createTempFileFromAsset
+import androidx.benchmark.macro.runSingleSessionServer
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
@@ -42,7 +43,7 @@
val traceFile = createTempFileFromAsset("api31_battery_discharge", ".perfetto-trace")
val actualMetrics =
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
val slice =
querySlices(PowerMetric.MEASURE_BLOCK_SECTION_NAME, packageName = null).first()
BatteryDischargeQuery.getBatteryDischargeMetrics(this, slice)
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/FrameTimingQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/FrameTimingQueryTest.kt
index 365e841..2051074 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/FrameTimingQueryTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/FrameTimingQueryTest.kt
@@ -22,8 +22,9 @@
import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric.FrameDurationUiNs
import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric.FrameOverrunNs
import androidx.benchmark.macro.perfetto.FrameTimingQuery.getFrameSubMetrics
+import androidx.benchmark.macro.runSingleSessionServer
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import kotlin.test.assertEquals
@@ -41,7 +42,7 @@
val traceFile = createTempFileFromAsset("api28_scroll", ".perfetto-trace")
val frameSubMetrics =
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
FrameTimingQuery.getFrameData(
session = this,
captureApiLevel = 28,
@@ -72,7 +73,7 @@
val traceFile = createTempFileFromAsset("api31_scroll", ".perfetto-trace")
val frameSubMetrics =
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
FrameTimingQuery.getFrameData(
session = this,
captureApiLevel = 31,
@@ -109,7 +110,7 @@
val traceFile = createTempFileFromAsset("api33_motionlayout_messagejson", ".perfetto-trace")
val frameData =
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
FrameTimingQuery.getFrameData(
session = this,
captureApiLevel = 33,
@@ -164,7 +165,7 @@
val traceFile = createTempFileFromAsset("api34_invalid_expect_actual", ".perfetto-trace")
val frameData =
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
FrameTimingQuery.getFrameData(
session = this,
captureApiLevel = 34,
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/MemoryCountersQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/MemoryCountersQueryTest.kt
index f3efe05..2ba48bf 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/MemoryCountersQueryTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/MemoryCountersQueryTest.kt
@@ -17,8 +17,9 @@
package androidx.benchmark.macro.perfetto
import androidx.benchmark.macro.createTempFileFromAsset
+import androidx.benchmark.macro.runSingleSessionServer
import androidx.benchmark.perfetto.PerfettoHelper
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import kotlin.test.assertEquals
@@ -34,7 +35,7 @@
assumeTrue(PerfettoHelper.isAbiSupported())
val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
val metrics =
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
MemoryCountersQuery.getMemoryCounters(
this,
"androidx.benchmark.integration.macrobenchmark.target"
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/MemoryUsageQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/MemoryUsageQueryTest.kt
index 77a5791..a2f6739 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/MemoryUsageQueryTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/MemoryUsageQueryTest.kt
@@ -19,8 +19,9 @@
import androidx.benchmark.macro.MemoryUsageMetric
import androidx.benchmark.macro.MemoryUsageMetric.SubMetric
import androidx.benchmark.macro.createTempFileFromAsset
+import androidx.benchmark.macro.runSingleSessionServer
import androidx.benchmark.perfetto.PerfettoHelper
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import kotlin.test.assertEquals
@@ -35,7 +36,7 @@
fun fixedTrace31() {
assumeTrue(PerfettoHelper.isAbiSupported())
val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
// Note: this particular trace has same values for last and max
val expected =
mapOf(
@@ -68,7 +69,7 @@
fun fixedTrace33() {
assumeTrue(PerfettoHelper.isAbiSupported())
val traceFile = createTempFileFromAsset("api33_motionlayout_messagejson", ".perfetto-trace")
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
assertEquals(
mapOf(
SubMetric.HeapSize to 25019,
@@ -103,7 +104,7 @@
fun fixedGpuTrace34() {
assumeTrue(PerfettoHelper.isAbiSupported())
val traceFile = createTempFileFromAsset("api34_startup_cold", ".perfetto-trace")
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
assertEquals(
mapOf(
SubMetric.Gpu to 30840,
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
index b7468b4..ea893e6 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
@@ -20,12 +20,13 @@
import android.os.Build
import androidx.benchmark.macro.FileLinkingRule
import androidx.benchmark.macro.Packages
+import androidx.benchmark.macro.runSingleSessionServer
import androidx.benchmark.perfetto.PerfettoCapture
import androidx.benchmark.perfetto.PerfettoConfig
import androidx.benchmark.perfetto.PerfettoHelper
import androidx.benchmark.perfetto.PerfettoHelper.Companion.MIN_BUNDLED_SDK_VERSION
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.tracing.Trace
@@ -119,7 +120,7 @@
perfettoCapture.stop(traceFilePath)
val matchingSlices =
- PerfettoTraceProcessor.runSingleSessionServer(traceFilePath) {
+ TraceProcessor.runSingleSessionServer(traceFilePath) {
querySlices("PerfettoCaptureTest_%", packageName = null)
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkTraceTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkTraceTest.kt
index 6c4392e..07cf35d 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkTraceTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkTraceTest.kt
@@ -18,9 +18,10 @@
import android.os.Build
import androidx.benchmark.junit4.PerfettoTraceRule
+import androidx.benchmark.macro.runSingleSessionServer
import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
import androidx.benchmark.perfetto.PerfettoHelper
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.tracing.trace
@@ -62,7 +63,7 @@
.flatMap { it }
.toList()
val actualSlices =
- PerfettoTraceProcessor.runSingleSessionServer(trace.path) {
+ TraceProcessor.runSingleSessionServer(trace.path) {
StringSource.allTraceStrings.flatMap {
querySlices(it, packageName = null).map { s -> s.name }
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PowerQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PowerQueryTest.kt
index ced85ef..2b95dc7 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PowerQueryTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PowerQueryTest.kt
@@ -20,8 +20,9 @@
import androidx.benchmark.macro.PowerCategory
import androidx.benchmark.macro.PowerMetric.Companion.MEASURE_BLOCK_SECTION_NAME
import androidx.benchmark.macro.createTempFileFromAsset
+import androidx.benchmark.macro.runSingleSessionServer
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
@@ -41,7 +42,7 @@
val traceFile = createTempFileFromAsset("api32_odpm_rails", ".perfetto-trace")
val actualMetrics =
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
PowerQuery.getPowerMetrics(
this,
querySlices(MEASURE_BLOCK_SECTION_NAME, packageName = null).first()
@@ -198,7 +199,7 @@
val traceFile = createTempFileFromAsset("api31_odpm_rails_empty", ".perfetto-trace")
val actualMetrics =
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
PowerQuery.getPowerMetrics(
this,
querySlices(MEASURE_BLOCK_SECTION_NAME, packageName = null).first()
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt
index cc3d69f..901592b 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt
@@ -18,8 +18,9 @@
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.createTempFileFromAsset
+import androidx.benchmark.macro.runSingleSessionServer
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import java.util.Locale
@@ -41,7 +42,7 @@
val traceFile = createTempFileFromAsset(prefix = tracePrefix, suffix = ".perfetto-trace")
val startupSubMetrics =
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
StartupTimingQuery.getFrameSubMetrics(
session = this,
captureApiLevel = api,
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/PerfettoTraceProcessorTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/PerfettoTraceProcessorTest.kt
deleted file mode 100644
index 5fcc5ca..0000000
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/PerfettoTraceProcessorTest.kt
+++ /dev/null
@@ -1,401 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx.benchmark.perfetto
-
-import androidx.benchmark.Outputs
-import androidx.benchmark.Shell
-import androidx.benchmark.macro.createTempFileFromAsset
-import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.test.filters.MediumTest
-import androidx.test.platform.app.InstrumentationRegistry
-import java.io.File
-import java.net.ConnectException
-import java.net.HttpURLConnection
-import java.net.URL
-import kotlin.test.assertContains
-import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
-import kotlin.test.assertNull
-import kotlin.time.Duration.Companion.milliseconds
-import org.junit.Assert.assertTrue
-import org.junit.Assume.assumeFalse
-import org.junit.Assume.assumeTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import perfetto.protos.TraceMetrics
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class PerfettoTraceProcessorTest {
- @Test
- fun shellPath() {
- assumeTrue(isAbiSupported())
- val shellPath = PerfettoTraceProcessor.shellPath
- val out = Shell.executeScriptCaptureStdout("$shellPath --version")
- assertTrue("expect to get Perfetto version string, saw: $out", out.contains("Perfetto v"))
- }
-
- @Test
- fun getJsonMetrics_tracePathWithSpaces() {
- assumeTrue(isAbiSupported())
- assertFailsWith<IllegalArgumentException> {
- PerfettoTraceProcessor.runSingleSessionServer("/a b") {}
- }
- }
-
- @Test
- fun getJsonMetrics_metricWithSpaces() {
- assumeTrue(isAbiSupported())
- assertFailsWith<IllegalArgumentException> {
- PerfettoTraceProcessor.runSingleSessionServer(
- createTempFileFromAsset("api31_startup_cold", ".perfetto-trace").absolutePath
- ) {
- getTraceMetrics("a b")
- }
- }
- }
-
- @Test
- fun validateAbiNotSupportedBehavior() {
- assumeFalse(isAbiSupported())
- assertFailsWith<IllegalStateException> { PerfettoTraceProcessor.shellPath }
-
- assertFailsWith<IllegalStateException> {
- PerfettoTraceProcessor.runSingleSessionServer(
- createTempFileFromAsset("api31_startup_cold", ".perfetto-trace").absolutePath
- ) {
- getTraceMetrics("ignored_metric")
- }
- }
- }
-
- enum class QuerySlicesMode(val target: String?) {
- ValidPackage("androidx.benchmark.integration.macrobenchmark.target"),
- Unspecified(null),
- InvalidPackage("not.a.real.package")
- }
-
- @Test fun querySlices_validPackage() = validateQuerySlices(QuerySlicesMode.ValidPackage)
-
- @Test fun querySlices_invalidPackage() = validateQuerySlices(QuerySlicesMode.InvalidPackage)
-
- @Test fun querySlices_unspecified() = validateQuerySlices(QuerySlicesMode.Unspecified)
-
- private fun validateQuerySlices(mode: QuerySlicesMode) {
- // check known slice content is queryable
- assumeTrue(isAbiSupported())
- val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
- assertEquals(
- expected =
- when (mode) {
- QuerySlicesMode.InvalidPackage -> emptyList()
- else ->
- listOf(
- Slice(name = "activityStart", ts = 186975009436431, dur = 29580628)
- )
- },
- actual = querySlices("activityStart", packageName = mode.target)
- )
- assertEquals(
- expected =
- when (mode) {
- QuerySlicesMode.InvalidPackage -> emptyList()
- else ->
- listOf(
- Slice(name = "activityStart", ts = 186975009436431, dur = 29580628),
- Slice(name = "activityResume", ts = 186975039764298, dur = 6570418)
- )
- },
- actual =
- querySlices("activityStart", "activityResume", packageName = mode.target)
- .sortedBy { it.ts }
- )
- assertEquals(
- expected =
- when (mode) {
- QuerySlicesMode.ValidPackage -> 7
- QuerySlicesMode.Unspecified -> 127
- QuerySlicesMode.InvalidPackage -> 0
- },
- actual = querySlices("Lock contention %", packageName = mode.target).size
- )
- }
- }
-
- @Test
- fun query_syntaxError() {
- assumeTrue(isAbiSupported())
- val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
- val error = assertFailsWith<IllegalStateException> { query("SYNTAX ERROR, PLEASE") }
- assertContains(
- charSequence = error.message!!,
- other = "syntax error",
- message = "expected 'syntax error', saw message : '''${error.message}'''"
- )
- }
- }
-
- @Test
- fun query() {
- assumeTrue(isAbiSupported())
- val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
- // raw list of maps
- assertEquals(
- expected =
- listOf(
- rowOf(
- "name" to "activityStart",
- "ts" to 186975009436431L,
- "dur" to 29580628L
- )
- ),
- actual =
- query("SELECT name,ts,dur FROM slice WHERE name LIKE \"activityStart\"")
- .toList(),
- )
-
- // list of lists
- assertEquals(
- expected = listOf(listOf("activityStart", 186975009436431L, 29580628L)),
- actual =
- query("SELECT name,ts,dur FROM slice WHERE name LIKE \"activityStart\"")
- .map { listOf(it.string("name"), it.long("ts"), it.long("dur")) }
- .toList(),
- )
-
- // multiple result query
- assertEquals(
- expected =
- listOf(
- listOf("activityStart", 186975009436431L, 29580628L),
- listOf("activityResume", 186975039764298L, 6570418L)
- ),
- actual =
- query(
- "SELECT name,ts,dur FROM slice WHERE" +
- " name LIKE \"activityStart\" OR" +
- " name LIKE \"activityResume\""
- )
- .map { listOf(it.string("name"), it.long("ts"), it.long("dur")) }
- .toList(),
- )
- }
- }
-
- /** Validate parsing of bytes is possible */
- @Test
- fun queryBytes() {
- assumeTrue(isAbiSupported())
- val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
- val query = "SELECT name,ts,dur FROM slice WHERE name LIKE \"activityStart\""
- val bytes = rawQuery(query)
- val queryResult = perfetto.protos.QueryResult.ADAPTER.decode(bytes)
- assertNull(queryResult.error, "no error expected")
- assertEquals(
- expected =
- listOf(
- rowOf(
- "name" to "activityStart",
- "ts" to 186975009436431L,
- "dur" to 29580628L
- )
- ),
- actual = QueryResultIterator(queryResult).asSequence().toList(),
- )
- }
- }
-
- @Test
- fun query_includeModule() {
- assumeTrue(isAbiSupported())
- val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
- val startups =
- PerfettoTraceProcessor.runServer {
- loadTrace(PerfettoTrace(traceFile.absolutePath)) {
- query(
- """
- INCLUDE PERFETTO MODULE android.startup.startups;
-
- SELECT * FROM android_startups;
- """
- .trimIndent()
- )
- .toList()
- }
- }
- // minimal validation, just verifying query worked
- assertEquals(1, startups.size)
- assertEquals(
- "androidx.benchmark.integration.macrobenchmark.target",
- startups.single().string("package")
- )
- }
-
- @Test
- fun queryMetricsJson() {
- assumeTrue(isAbiSupported())
- val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
- val metrics = queryMetricsJson(listOf("android_startup"))
- assertTrue(metrics.contains("\"android_startup\": {"))
- assertTrue(metrics.contains("\"startup_type\": \"cold\","))
- }
- }
-
- @Test
- fun queryMetricsProtoBinary() {
- assumeTrue(isAbiSupported())
- val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
- val metrics =
- TraceMetrics.ADAPTER.decode(queryMetricsProtoBinary(listOf("android_startup")))
- val startup = metrics.android_startup!!
- assertEquals(startup.startup.single().startup_type, "cold")
- }
- }
-
- @Test
- fun queryMetricsProtoText() {
- assumeTrue(isAbiSupported())
- val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
- val metrics = queryMetricsProtoText(listOf("android_startup"))
- assertTrue(metrics.contains("android_startup {"))
- assertTrue(metrics.contains("startup_type: \"cold\""))
- }
- }
-
- @Test
- fun validatePerfettoTraceProcessorBinariesExist() {
- val context = InstrumentationRegistry.getInstrumentation().targetContext
- val suffixes = listOf("aarch64")
- val entries = suffixes.map { "trace_processor_shell_$it" }.toSet()
- val assets = context.assets.list("") ?: emptyArray()
- assertTrue("Expected to find $entries", assets.toSet().containsAll(entries))
- }
-
- @Test
- fun runServerShouldHandleStartAndStopServer() {
- assumeTrue(isAbiSupported())
-
- // Check server is not running
- assertTrue(!isRunning())
-
- PerfettoTraceProcessor.runServer {
- // Check server is running
- assertTrue(isRunning())
- }
-
- // Check server is not running
- assertTrue(!isRunning())
- }
-
- @Test
- fun runServerWithNegativeTimeoutShouldStartAndStopServer() {
- assumeTrue(isAbiSupported())
-
- // Check server is not running
- assertTrue(!isRunning())
-
- PerfettoTraceProcessor.runServer((-1).milliseconds) {
- // Check server is running
- assertTrue(isRunning())
- }
-
- // Check server is not running
- assertTrue(!isRunning())
- }
-
- @Test
- fun runServerWithZeroTimeoutShouldStartAndStopServer() {
- assumeTrue(isAbiSupported())
-
- // Check server is not running
- assertTrue(!isRunning())
-
- PerfettoTraceProcessor.runServer((0).milliseconds) {
- // Check server is running
- assertTrue(isRunning())
- }
-
- // Check server is not running
- assertTrue(!isRunning())
- }
-
- @Test
- fun testParseTracesWithProcessTracks() {
- assumeTrue(isAbiSupported())
- val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
- val slices = querySlices("launching:%", packageName = null)
- assertEquals(
- expected =
- listOf(
- Slice(
- name =
- "launching: androidx.benchmark.integration.macrobenchmark.target",
- ts = 186974946587883,
- dur = 137401159
- )
- ),
- slices
- )
- }
- }
-
- @LargeTest
- @Test
- fun parseLongTrace() {
- val traceFile =
- File.createTempFile("long_trace", ".trace", Outputs.dirUsableByAppAndShell).apply {
- var length = 0L
- val out = outputStream()
- while (length < 70 * 1024 * 1024) {
- length +=
- InstrumentationRegistry.getInstrumentation()
- .context
- .assets
- .open("api31_startup_cold.perfetto-trace")
- .copyTo(out)
- }
- }
- PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
- // This would throw an exception if there is an error in the parsing.
- getTraceMetrics("android_startup")
- }
- }
-
- /**
- * This method will return true if the server status endpoint returns 200 (that is also the only
- * status code being returned).
- */
- private fun isRunning(): Boolean =
- try {
- val url = URL("http://localhost:${PerfettoTraceProcessor.PORT}/")
- with(url.openConnection() as HttpURLConnection) {
- return@with responseCode == 200
- }
- } catch (e: ConnectException) {
- false
- }
-}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/RowTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/RowTest.kt
deleted file mode 100644
index 00895dd..0000000
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/RowTest.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx.benchmark.perfetto
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import kotlin.test.assertContentEquals
-import kotlin.test.assertEquals
-import kotlin.test.assertNull
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class RowTest {
- @Test
- fun basic() {
- val map = mapOf<String, Any?>("name" to "Name", "ts" to 0L, "dur" to 1L)
- val row = rowOf("name" to "Name", "ts" to 0L, "dur" to 1L)
-
- assertEquals(map, row)
- assertEquals(map.hashCode(), row.hashCode())
- assertEquals(map.toString(), row.toString())
- }
-
- @Test
- fun gettersSetters() {
- val row =
- rowOf(
- "string" to "foo",
- "double" to 0.0,
- "long" to 1L,
- "bytes" to byteArrayOf(0x00, 0x01),
- "null" to null
- )
-
- assertEquals("foo", row.string("string"))
- assertEquals("foo", row.nullableString("string"))
- assertContentEquals(byteArrayOf(0x00, 0x01), row.bytes("bytes"))
- assertContentEquals(byteArrayOf(0x00, 0x01), row.nullableBytes("bytes"))
- assertEquals(0.0, row.double("double"))
- assertEquals(0.0, row.nullableDouble("double"))
- assertEquals(1L, row.long("long"))
- assertEquals(1L, row.nullableLong("long"))
-
- assertNull(row.nullableString("null"))
- assertNull(row.nullableBytes("null"))
- assertNull(row.nullableDouble("null"))
- assertNull(row.nullableLong("null"))
- }
-}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/SliceTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/SliceTest.kt
deleted file mode 100644
index f1effb9..0000000
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/SliceTest.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx.benchmark.perfetto
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import kotlin.test.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class SliceTest {
- @Test
- fun frameId() {
- assertEquals(1234, Slice("Choreographer#doFrame 1234", 1, 2).frameId)
- }
-
- @Test
- fun frameId_extended() {
- // some OEMs have added additional metadata to standard tracepoints
- // we'll fix these best effort as they are reported
- assertEquals(123, Slice("Choreographer#doFrame 123 234 extra=91929", 1, 2).frameId)
- }
-}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/traceprocessor/RowTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/traceprocessor/RowTest.kt
new file mode 100644
index 0000000..f52a99f
--- /dev/null
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/traceprocessor/RowTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.benchmark.traceprocessor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RowTest {
+ @Test
+ fun basic() {
+ val map = mapOf<String, Any?>("name" to "Name", "ts" to 0L, "dur" to 1L)
+ val row = rowOf("name" to "Name", "ts" to 0L, "dur" to 1L)
+
+ assertEquals(map, row)
+ assertEquals(map.hashCode(), row.hashCode())
+ assertEquals(map.toString(), row.toString())
+ }
+
+ @Test
+ fun gettersSetters() {
+ val row =
+ rowOf(
+ "string" to "foo",
+ "double" to 0.0,
+ "long" to 1L,
+ "bytes" to byteArrayOf(0x00, 0x01),
+ "null" to null
+ )
+
+ assertEquals("foo", row.string("string"))
+ assertEquals("foo", row.nullableString("string"))
+ assertContentEquals(byteArrayOf(0x00, 0x01), row.bytes("bytes"))
+ assertContentEquals(byteArrayOf(0x00, 0x01), row.nullableBytes("bytes"))
+ assertEquals(0.0, row.double("double"))
+ assertEquals(0.0, row.nullableDouble("double"))
+ assertEquals(1L, row.long("long"))
+ assertEquals(1L, row.nullableLong("long"))
+
+ assertNull(row.nullableString("null"))
+ assertNull(row.nullableBytes("null"))
+ assertNull(row.nullableDouble("null"))
+ assertNull(row.nullableLong("null"))
+ }
+}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/traceprocessor/SliceTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/traceprocessor/SliceTest.kt
new file mode 100644
index 0000000..9b87666
--- /dev/null
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/traceprocessor/SliceTest.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.benchmark.traceprocessor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import kotlin.test.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SliceTest {
+ @Test
+ fun frameId() {
+ assertEquals(1234, Slice("Choreographer#doFrame 1234", 1, 2).frameId)
+ }
+
+ @Test
+ fun frameId_extended() {
+ // some OEMs have added additional metadata to standard tracepoints
+ // we'll fix these best effort as they are reported
+ assertEquals(123, Slice("Choreographer#doFrame 123 234 extra=91929", 1, 2).frameId)
+ }
+}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/traceprocessor/TraceProcessorTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/traceprocessor/TraceProcessorTest.kt
new file mode 100644
index 0000000..87e1566
--- /dev/null
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/traceprocessor/TraceProcessorTest.kt
@@ -0,0 +1,404 @@
+/*
+ * Copyright 2023 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
+ *
+ * http://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 androidx.benchmark.traceprocessor
+
+import androidx.benchmark.Outputs
+import androidx.benchmark.Shell
+import androidx.benchmark.macro.ShellServerLifecycleManager
+import androidx.benchmark.macro.createTempFileFromAsset
+import androidx.benchmark.macro.runServer
+import androidx.benchmark.macro.runSingleSessionServer
+import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.MediumTest
+import androidx.test.platform.app.InstrumentationRegistry
+import java.io.File
+import java.net.ConnectException
+import java.net.HttpURLConnection
+import java.net.URL
+import kotlin.test.assertContains
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNull
+import kotlin.time.Duration.Companion.milliseconds
+import org.junit.Assert.assertTrue
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import perfetto.protos.TraceMetrics
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class TraceProcessorTest {
+ @Test
+ fun shellPath() {
+ assumeTrue(isAbiSupported())
+ val shellPath = ShellServerLifecycleManager.shellPath
+ val out = Shell.executeScriptCaptureStdout("$shellPath --version")
+ assertTrue("expect to get Perfetto version string, saw: $out", out.contains("Perfetto v"))
+ }
+
+ @Test
+ fun getJsonMetrics_tracePathWithSpaces() {
+ assumeTrue(isAbiSupported())
+ assertFailsWith<IllegalArgumentException> {
+ TraceProcessor.runSingleSessionServer("/a b") {}
+ }
+ }
+
+ @Test
+ fun getJsonMetrics_metricWithSpaces() {
+ assumeTrue(isAbiSupported())
+ assertFailsWith<IllegalArgumentException> {
+ TraceProcessor.runSingleSessionServer(
+ createTempFileFromAsset("api31_startup_cold", ".perfetto-trace").absolutePath
+ ) {
+ getTraceMetrics("a b")
+ }
+ }
+ }
+
+ @Test
+ fun validateAbiNotSupportedBehavior() {
+ assumeFalse(isAbiSupported())
+ assertFailsWith<IllegalStateException> { ShellServerLifecycleManager.shellPath }
+
+ assertFailsWith<IllegalStateException> {
+ TraceProcessor.runSingleSessionServer(
+ createTempFileFromAsset("api31_startup_cold", ".perfetto-trace").absolutePath
+ ) {
+ getTraceMetrics("ignored_metric")
+ }
+ }
+ }
+
+ enum class QuerySlicesMode(val target: String?) {
+ ValidPackage("androidx.benchmark.integration.macrobenchmark.target"),
+ Unspecified(null),
+ InvalidPackage("not.a.real.package")
+ }
+
+ @Test fun querySlices_validPackage() = validateQuerySlices(QuerySlicesMode.ValidPackage)
+
+ @Test fun querySlices_invalidPackage() = validateQuerySlices(QuerySlicesMode.InvalidPackage)
+
+ @Test fun querySlices_unspecified() = validateQuerySlices(QuerySlicesMode.Unspecified)
+
+ private fun validateQuerySlices(mode: QuerySlicesMode) {
+ // check known slice content is queryable
+ assumeTrue(isAbiSupported())
+ val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ assertEquals(
+ expected =
+ when (mode) {
+ QuerySlicesMode.InvalidPackage -> emptyList()
+ else ->
+ listOf(
+ Slice(name = "activityStart", ts = 186975009436431, dur = 29580628)
+ )
+ },
+ actual = querySlices("activityStart", packageName = mode.target)
+ )
+ assertEquals(
+ expected =
+ when (mode) {
+ QuerySlicesMode.InvalidPackage -> emptyList()
+ else ->
+ listOf(
+ Slice(name = "activityStart", ts = 186975009436431, dur = 29580628),
+ Slice(name = "activityResume", ts = 186975039764298, dur = 6570418)
+ )
+ },
+ actual =
+ querySlices("activityStart", "activityResume", packageName = mode.target)
+ .sortedBy { it.ts }
+ )
+ assertEquals(
+ expected =
+ when (mode) {
+ QuerySlicesMode.ValidPackage -> 7
+ QuerySlicesMode.Unspecified -> 127
+ QuerySlicesMode.InvalidPackage -> 0
+ },
+ actual = querySlices("Lock contention %", packageName = mode.target).size
+ )
+ }
+ }
+
+ @Test
+ fun query_syntaxError() {
+ assumeTrue(isAbiSupported())
+ val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ val error = assertFailsWith<IllegalStateException> { query("SYNTAX ERROR, PLEASE") }
+ assertContains(
+ charSequence = error.message!!,
+ other = "syntax error",
+ message = "expected 'syntax error', saw message : '''${error.message}'''"
+ )
+ }
+ }
+
+ @Test
+ fun query() {
+ assumeTrue(isAbiSupported())
+ val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ // raw list of maps
+ assertEquals(
+ expected =
+ listOf(
+ rowOf(
+ "name" to "activityStart",
+ "ts" to 186975009436431L,
+ "dur" to 29580628L
+ )
+ ),
+ actual =
+ query("SELECT name,ts,dur FROM slice WHERE name LIKE \"activityStart\"")
+ .toList(),
+ )
+
+ // list of lists
+ assertEquals(
+ expected = listOf(listOf("activityStart", 186975009436431L, 29580628L)),
+ actual =
+ query("SELECT name,ts,dur FROM slice WHERE name LIKE \"activityStart\"")
+ .map { listOf(it.string("name"), it.long("ts"), it.long("dur")) }
+ .toList(),
+ )
+
+ // multiple result query
+ assertEquals(
+ expected =
+ listOf(
+ listOf("activityStart", 186975009436431L, 29580628L),
+ listOf("activityResume", 186975039764298L, 6570418L)
+ ),
+ actual =
+ query(
+ "SELECT name,ts,dur FROM slice WHERE" +
+ " name LIKE \"activityStart\" OR" +
+ " name LIKE \"activityResume\""
+ )
+ .map { listOf(it.string("name"), it.long("ts"), it.long("dur")) }
+ .toList(),
+ )
+ }
+ }
+
+ /** Validate parsing of bytes is possible */
+ @Test
+ fun queryBytes() {
+ assumeTrue(isAbiSupported())
+ val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ val query = "SELECT name,ts,dur FROM slice WHERE name LIKE \"activityStart\""
+ val bytes = rawQuery(query)
+ val queryResult = perfetto.protos.QueryResult.ADAPTER.decode(bytes)
+ assertNull(queryResult.error, "no error expected")
+ assertEquals(
+ expected =
+ listOf(
+ rowOf(
+ "name" to "activityStart",
+ "ts" to 186975009436431L,
+ "dur" to 29580628L
+ )
+ ),
+ actual = QueryResultIterator(queryResult).asSequence().toList(),
+ )
+ }
+ }
+
+ @Test
+ fun query_includeModule() {
+ assumeTrue(isAbiSupported())
+ val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
+ val startups =
+ TraceProcessor.runServer {
+ loadTrace(PerfettoTrace(traceFile.absolutePath)) {
+ query(
+ """
+ INCLUDE PERFETTO MODULE android.startup.startups;
+
+ SELECT * FROM android_startups;
+ """
+ .trimIndent()
+ )
+ .toList()
+ }
+ }
+ // minimal validation, just verifying query worked
+ assertEquals(1, startups.size)
+ assertEquals(
+ "androidx.benchmark.integration.macrobenchmark.target",
+ startups.single().string("package")
+ )
+ }
+
+ @Test
+ fun queryMetricsJson() {
+ assumeTrue(isAbiSupported())
+ val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ val metrics = queryMetricsJson(listOf("android_startup"))
+ assertTrue(metrics.contains("\"android_startup\": {"))
+ assertTrue(metrics.contains("\"startup_type\": \"cold\","))
+ }
+ }
+
+ @Test
+ fun queryMetricsProtoBinary() {
+ assumeTrue(isAbiSupported())
+ val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ val metrics =
+ TraceMetrics.ADAPTER.decode(queryMetricsProtoBinary(listOf("android_startup")))
+ val startup = metrics.android_startup!!
+ assertEquals(startup.startup.single().startup_type, "cold")
+ }
+ }
+
+ @Test
+ fun queryMetricsProtoText() {
+ assumeTrue(isAbiSupported())
+ val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ val metrics = queryMetricsProtoText(listOf("android_startup"))
+ assertTrue(metrics.contains("android_startup {"))
+ assertTrue(metrics.contains("startup_type: \"cold\""))
+ }
+ }
+
+ @Test
+ fun validateTraceProcessorBinariesExist() {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ val suffixes = listOf("aarch64")
+ val entries = suffixes.map { "trace_processor_shell_$it" }.toSet()
+ val assets = context.assets.list("") ?: emptyArray()
+ assertTrue("Expected to find $entries", assets.toSet().containsAll(entries))
+ }
+
+ @Test
+ fun runServerShouldHandleStartAndStopServer() {
+ assumeTrue(isAbiSupported())
+
+ // Check server is not running
+ assertTrue(!isRunning())
+
+ TraceProcessor.runServer {
+ // Check server is running
+ assertTrue(isRunning())
+ }
+
+ // Check server is not running
+ assertTrue(!isRunning())
+ }
+
+ @Test
+ fun runServerWithNegativeTimeoutShouldStartAndStopServer() {
+ assumeTrue(isAbiSupported())
+
+ // Check server is not running
+ assertTrue(!isRunning())
+
+ TraceProcessor.runServer((-1).milliseconds) {
+ // Check server is running
+ assertTrue(isRunning())
+ }
+
+ // Check server is not running
+ assertTrue(!isRunning())
+ }
+
+ @Test
+ fun runServerWithZeroTimeoutShouldStartAndStopServer() {
+ assumeTrue(isAbiSupported())
+
+ // Check server is not running
+ assertTrue(!isRunning())
+
+ TraceProcessor.runServer((0).milliseconds) {
+ // Check server is running
+ assertTrue(isRunning())
+ }
+
+ // Check server is not running
+ assertTrue(!isRunning())
+ }
+
+ @Test
+ fun testParseTracesWithProcessTracks() {
+ assumeTrue(isAbiSupported())
+ val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ val slices = querySlices("launching:%", packageName = null)
+ assertEquals(
+ expected =
+ listOf(
+ Slice(
+ name =
+ "launching: androidx.benchmark.integration.macrobenchmark.target",
+ ts = 186974946587883,
+ dur = 137401159
+ )
+ ),
+ slices
+ )
+ }
+ }
+
+ @LargeTest
+ @Test
+ fun parseLongTrace() {
+ val traceFile =
+ File.createTempFile("long_trace", ".trace", Outputs.dirUsableByAppAndShell).apply {
+ var length = 0L
+ val out = outputStream()
+ while (length < 70 * 1024 * 1024) {
+ length +=
+ InstrumentationRegistry.getInstrumentation()
+ .context
+ .assets
+ .open("api31_startup_cold.perfetto-trace")
+ .copyTo(out)
+ }
+ }
+ TraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ // This would throw an exception if there is an error in the parsing.
+ getTraceMetrics("android_startup")
+ }
+ }
+
+ /**
+ * This method will return true if the server status endpoint returns 200 (that is also the only
+ * status code being returned).
+ */
+ private fun isRunning(): Boolean =
+ try {
+ val url = URL("http://localhost:${ShellServerLifecycleManager.PORT}/")
+ with(url.openConnection() as HttpURLConnection) {
+ return@with responseCode == 200
+ }
+ } catch (e: ConnectException) {
+ false
+ }
+}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index 2f1aa74..cc63a82 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -40,7 +40,7 @@
import androidx.benchmark.macro.MacrobenchmarkScope.KillFlushMode
import androidx.benchmark.perfetto.PerfettoCapture.PerfettoSdkConfig
import androidx.benchmark.perfetto.PerfettoCapture.PerfettoSdkConfig.InitialProcessState
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assume.assumeFalse
@@ -263,7 +263,7 @@
val macrobenchPackageName = InstrumentationRegistry.getInstrumentation().context.packageName
val iterationResults = mutableListOf<IterationResult>()
- PerfettoTraceProcessor.runServer {
+ TraceProcessor.runServer {
scope.withKillFlushMode(
current = KillFlushMode.None,
override =
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkPhase.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkPhase.kt
index c146870..bda11d3 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkPhase.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkPhase.kt
@@ -29,10 +29,10 @@
import androidx.benchmark.perfetto.PerfettoCapture
import androidx.benchmark.perfetto.PerfettoCaptureWrapper
import androidx.benchmark.perfetto.PerfettoConfig
-import androidx.benchmark.perfetto.PerfettoTrace
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
import androidx.benchmark.perfetto.UiState
import androidx.benchmark.perfetto.appendUiState
+import androidx.benchmark.traceprocessor.PerfettoTrace
+import androidx.benchmark.traceprocessor.TraceProcessor
import androidx.tracing.trace
import java.io.File
@@ -65,7 +65,7 @@
/** Run a Macrobenchmark Phase and collect a list of [IterationResult]. */
@ExperimentalBenchmarkConfigApi
-internal fun PerfettoTraceProcessor.runPhase(
+internal fun TraceProcessor.runPhase(
uniqueName: String,
packageName: String,
macrobenchmarkPackageName: String,
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
index 30c9738..2adfe66 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
@@ -268,6 +268,13 @@
return
}
+ if (!Shell.isPackageAlive(packageName)) {
+ throw IllegalStateException(
+ "Target package $packageName is not running," +
+ " check logcat to verify the activity launched correctly"
+ )
+ }
+
// `am start -W` doesn't reliably wait for process to complete and renderthread to produce
// a new frame (b/226179160), so we use `dumpsys gfxinfo <package> framestats` to determine
// when the next frame is produced.
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
index 5ec4de0..4810691 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
@@ -22,6 +22,7 @@
import androidx.benchmark.DeviceInfo
import androidx.benchmark.Shell
import androidx.benchmark.macro.BatteryCharge.hasMinimumCharge
+import androidx.benchmark.macro.PowerMetric.Companion.deviceSupportsHighPrecisionTracking
import androidx.benchmark.macro.PowerMetric.Type
import androidx.benchmark.macro.PowerRail.hasMetrics
import androidx.benchmark.macro.TraceSectionMetric.Mode
@@ -34,8 +35,8 @@
import androidx.benchmark.macro.perfetto.PowerQuery
import androidx.benchmark.macro.perfetto.StartupTimingQuery
import androidx.benchmark.macro.perfetto.camelCase
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
-import androidx.benchmark.perfetto.Slice
+import androidx.benchmark.traceprocessor.Slice
+import androidx.benchmark.traceprocessor.TraceProcessor
import androidx.test.platform.app.InstrumentationRegistry
/** Metric interface. */
@@ -49,7 +50,7 @@
/** After stopping, collect metrics */
internal abstract fun getMeasurements(
captureInfo: CaptureInfo,
- traceSession: PerfettoTraceProcessor.Session
+ traceSession: TraceProcessor.Session
): List<Measurement>
/**
@@ -202,7 +203,7 @@
class FrameTimingMetric : Metric() {
override fun getMeasurements(
captureInfo: CaptureInfo,
- traceSession: PerfettoTraceProcessor.Session
+ traceSession: TraceProcessor.Session
): List<Measurement> {
val frameData =
FrameTimingQuery.getFrameData(
@@ -320,7 +321,7 @@
override fun getMeasurements(
captureInfo: CaptureInfo,
- traceSession: PerfettoTraceProcessor.Session
+ traceSession: TraceProcessor.Session
): List<Measurement> {
return metrics
.map {
@@ -352,7 +353,7 @@
class StartupTimingMetric : Metric() {
override fun getMeasurements(
captureInfo: CaptureInfo,
- traceSession: PerfettoTraceProcessor.Session
+ traceSession: TraceProcessor.Session
): List<Measurement> {
return StartupTimingQuery.getFrameSubMetrics(
session = traceSession,
@@ -382,7 +383,7 @@
class StartupTimingLegacyMetric : Metric() {
override fun getMeasurements(
captureInfo: CaptureInfo,
- traceSession: PerfettoTraceProcessor.Session
+ traceSession: TraceProcessor.Session
): List<Measurement> {
// Acquires perfetto metrics
val traceMetrics = traceSession.getTraceMetrics("android_startup")
@@ -415,7 +416,7 @@
}
/**
- * Metric which captures results from a Perfetto trace with custom [PerfettoTraceProcessor] queries.
+ * Metric which captures results from a Perfetto trace with custom [TraceProcessor] queries.
*
* This is a more customizable version of [TraceSectionMetric] which can perform arbitrary queries
* against the captured PerfettoTrace.
@@ -426,7 +427,7 @@
* class ActivityResumeMetric : TraceMetric() {
* override fun getMeasurements(
* captureInfo: CaptureInfo,
- * traceSession: PerfettoTraceProcessor.Session
+ * traceSession: TraceProcessor.Session
* ): List<Measurement> {
* val rowSequence = traceSession.query(
* """
@@ -455,9 +456,9 @@
* }
* ```
*
- * @see PerfettoTraceProcessor
- * @see PerfettoTraceProcessor.Session
- * @see PerfettoTraceProcessor.Session.query
+ * @see TraceProcessor
+ * @see TraceProcessor.Session
+ * @see TraceProcessor.Session.query
*/
@ExperimentalMetricApi
abstract class TraceMetric : Metric() {
@@ -467,7 +468,7 @@
*/
public abstract override fun getMeasurements(
captureInfo: CaptureInfo,
- traceSession: PerfettoTraceProcessor.Session
+ traceSession: TraceProcessor.Session
): List<Measurement>
}
@@ -501,9 +502,9 @@
/**
* Section name or pattern to match.
*
- * "%" can be used as a wildcard, as this is supported by the underlying
- * [PerfettoTraceProcessor] query. For example `"JIT %"` will match a section named `"JIT
- * compiling int com.package.MyClass.method(int)"` present in the trace.
+ * "%" can be used as a wildcard, as this is supported by the underlying [TraceProcessor] query.
+ * For example `"JIT %"` will match a section named `"JIT compiling int
+ * com.package.MyClass.method(int)"` present in the trace.
*/
private val sectionName: String,
/**
@@ -577,7 +578,7 @@
override fun getMeasurements(
captureInfo: CaptureInfo,
- traceSession: PerfettoTraceProcessor.Session
+ traceSession: TraceProcessor.Session
): List<Measurement> {
val slices =
traceSession.querySlices(
@@ -719,7 +720,7 @@
class ArtMetric : Metric() {
override fun getMeasurements(
captureInfo: CaptureInfo,
- traceSession: PerfettoTraceProcessor.Session
+ traceSession: TraceProcessor.Session
): List<Measurement> {
return traceSession
.querySlices("JIT Compiling %", packageName = captureInfo.targetPackageName)
@@ -914,7 +915,7 @@
override fun getMeasurements(
captureInfo: CaptureInfo,
- traceSession: PerfettoTraceProcessor.Session
+ traceSession: TraceProcessor.Session
): List<Measurement> {
// collect metrics between trace point flags
val slice =
@@ -929,7 +930,7 @@
}
private fun getBatteryDischargeMetrics(
- session: PerfettoTraceProcessor.Session,
+ session: TraceProcessor.Session,
slice: Slice
): List<Measurement> {
val metrics = BatteryDischargeQuery.getBatteryDischargeMetrics(session, slice)
@@ -938,10 +939,7 @@
}
}
- private fun getPowerMetrics(
- session: PerfettoTraceProcessor.Session,
- slice: Slice
- ): List<Measurement> {
+ private fun getPowerMetrics(session: TraceProcessor.Session, slice: Slice): List<Measurement> {
val metrics = PowerQuery.getPowerMetrics(session, slice)
val metricMap: Map<String, Double> = getSpecifiedMetrics(metrics)
@@ -1065,7 +1063,7 @@
override fun getMeasurements(
captureInfo: CaptureInfo,
- traceSession: PerfettoTraceProcessor.Session
+ traceSession: TraceProcessor.Session
): List<Measurement> {
val suffix = mode.toString()
@@ -1089,7 +1087,7 @@
class MemoryCountersMetric : TraceMetric() {
override fun getMeasurements(
captureInfo: CaptureInfo,
- traceSession: PerfettoTraceProcessor.Session
+ traceSession: TraceProcessor.Session
): List<Measurement> {
val metrics =
MemoryCountersQuery.getMemoryCounters(
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/TraceProcessorExtensions.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/TraceProcessorExtensions.kt
new file mode 100644
index 0000000..5ed5486
--- /dev/null
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/TraceProcessorExtensions.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.benchmark.macro
+
+import android.annotation.SuppressLint
+import android.os.Build
+import android.security.NetworkSecurityPolicy
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.benchmark.InMemoryTracing
+import androidx.benchmark.InstrumentationResults
+import androidx.benchmark.Outputs
+import androidx.benchmark.Profiler
+import androidx.benchmark.Shell
+import androidx.benchmark.ShellScript
+import androidx.benchmark.StartedShellScript
+import androidx.benchmark.inMemoryTrace
+import androidx.benchmark.perfetto.PerfettoHelper
+import androidx.benchmark.traceprocessor.PerfettoTrace
+import androidx.benchmark.traceprocessor.ServerLifecycleManager
+import androidx.benchmark.traceprocessor.TraceProcessor
+import java.io.IOException
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+
+@RequiresApi(24)
+private object Api24Impl {
+ fun isCleartextTrafficPermittedForLocalhost() =
+ NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted("localhost")
+}
+
+internal class ShellServerLifecycleManager : ServerLifecycleManager {
+ companion object {
+ private const val SERVER_PROCESS_NAME = "trace_processor_shell"
+
+ internal val shellPath: String by lazy {
+ // Checks for ABI support
+ PerfettoHelper.createExecutable(SERVER_PROCESS_NAME)
+ }
+ internal const val PORT = 9001
+ }
+
+ private var shellScript: ShellScript? = null
+ private var startedShellScript: StartedShellScript? = null
+ private var processId: Int? = null
+
+ /**
+ * Returns a cached instance of the shell script to run the perfetto trace shell processor as
+ * http server. Note that the generated script doesn't specify the port and this must be passed
+ * as parameter when running the script.
+ */
+ private fun getOrCreateShellScript(): ShellScript =
+ shellScript
+ ?: synchronized(this) {
+ var instance = shellScript
+ if (instance != null) {
+ return@synchronized instance
+ }
+ val script = "echo pid:$$ ; exec $shellPath -D --http-port \"$PORT\""
+ instance = Shell.createShellScript(script)
+ shellScript = instance
+ instance
+ }
+
+ @SuppressLint("BanThreadSleep")
+ override fun start(): Int {
+ inMemoryTrace("ShellServerLifecycleManager#start") {
+ if (
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
+ !Api24Impl.isCleartextTrafficPermittedForLocalhost()
+ ) {
+ throw IOException(
+ """
+ Macrobenchmark requires cleartext HTTP traffic to the on-device localhost to enable
+ querying data from perfetto traces, such as timestamps that are used to calculate
+ metrics. This should be enabled by default via manifest merging when building with
+ Gradle. Please refer to
+ https://d.android.com/training/articles/security-config#CleartextTrafficPermitted
+ and enable cleartext http requests towards localhost in your test android manifest.
+ """
+ .trimIndent()
+ )
+ }
+
+ getOrCreateShellScript().start().apply {
+ processId =
+ stdOutLineSequence().first { it.startsWith("pid:") }.split("pid:")[1].toInt()
+ startedShellScript = this
+ println("Started, processId $processId")
+ }
+ }
+ return PORT
+ }
+
+ override fun timeoutMessage(): String {
+
+ // In the event that the instrumentation app cannot connect to the
+ // trace_processor_shell server, trying to read the full stderr may make the
+ // process hang. Here we check if the process is still running to determine if
+ // that's the case and throw the correct exception.
+
+ val processRunning =
+ processId?.let { Shell.isProcessAlive(it, SERVER_PROCESS_NAME) } ?: false
+
+ return if (processRunning) {
+ "The instrumentation app cannot connect to the trace_processor_shell server."
+ } else {
+ "Perfetto trace_processor_shell did not start correctly." +
+ " Stderr = ${startedShellScript?.getOutputAndClose()?.stderr}"
+ }
+ }
+
+ override fun stop() {
+ inMemoryTrace("ShellServerLifecycleManager#stop") {
+ if (processId == null) {
+ Log.w(TAG, "Tried to stop trace shell processor http server without starting it.")
+ return
+ }
+ println("stop, processId=$processId")
+ Shell.executeScriptSilent("kill -TERM $processId")
+ Log.i(TAG, "Perfetto trace processor shell server stopped (pid=$processId).")
+ processId = null
+ }
+ }
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun <T> TraceProcessor.Companion.runSingleSessionServer(
+ absoluteTracePath: String,
+ block: TraceProcessor.Session.() -> T
+) = TraceProcessor.runServer { loadTrace(PerfettoTrace(absoluteTracePath), block) }
+
+/**
+ * Starts a Perfetto Trace Processor shell server in http mode, loads a trace and executes the given
+ * block. It stops the server after the block is complete
+ *
+ * Uses a default timeout of 60 seconds.
+ *
+ * @param block Command to execute using trace processor
+ */
+fun <T> TraceProcessor.Companion.runServer(block: TraceProcessor.() -> T): T =
+ TraceProcessor.runServer(60.seconds, block)
+
+/**
+ * Starts a Perfetto Trace Processor shell server in http mode, loads a trace and executes the given
+ * block. It stops the server after the block is complete
+ *
+ * @param timeout waiting for the server to start. If less or equal to zero uses 60 seconds
+ * @param block Command to execute using trace processor
+ */
+fun <T> TraceProcessor.Companion.runServer(timeout: Duration, block: TraceProcessor.() -> T): T =
+ runServer(
+ ShellServerLifecycleManager(),
+ eventCallback =
+ object : TraceProcessor.EventCallback {
+ override fun onLoadTraceFailure(trace: PerfettoTrace, throwable: Throwable) {
+ // TODO: consider a label argument to control logging like this in the success
+ // case as
+ // well, which lets us get rid of FileLinkingRule (which doesn't work well
+ // anyway)
+ if (trace.path.startsWith(Outputs.outputDirectory.absolutePath)) {
+ // only link trace with failure to Studio if it's an output file
+ InstrumentationResults.instrumentationReport {
+ val label =
+ "Trace with processing error: ${throwable.message?.take(50)?.trim()}..."
+ reportSummaryToIde(
+ profilerResults =
+ listOf(
+ Profiler.ResultFile.ofPerfettoTrace(
+ label = label,
+ absolutePath = trace.path
+ )
+ )
+ )
+ }
+ }
+ }
+ },
+ tracer =
+ object : TraceProcessor.Tracer() {
+ override fun beginTraceSection(label: String) {
+ InMemoryTracing.beginSection(label)
+ }
+
+ override fun endTraceSection() {
+ InMemoryTracing.endSection()
+ }
+ },
+ timeout = timeout,
+ block = block,
+ )
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/BatteryDischargeQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/BatteryDischargeQuery.kt
index e403e70..ca8059d9 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/BatteryDischargeQuery.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/BatteryDischargeQuery.kt
@@ -16,8 +16,8 @@
package androidx.benchmark.macro.perfetto
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
-import androidx.benchmark.perfetto.Slice
+import androidx.benchmark.traceprocessor.Slice
+import androidx.benchmark.traceprocessor.TraceProcessor
internal object BatteryDischargeQuery {
private fun getFullQuery(slice: Slice) =
@@ -36,7 +36,7 @@
data class BatteryDischargeMeasurement(var name: String, var chargeMah: Double)
fun getBatteryDischargeMetrics(
- session: PerfettoTraceProcessor.Session,
+ session: TraceProcessor.Session,
slice: Slice
): List<BatteryDischargeMeasurement> {
val queryResult = session.query(query = getFullQuery(slice)).toList()
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt
index 22fb04f..2835259 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt
@@ -16,10 +16,10 @@
package androidx.benchmark.macro.perfetto
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
-import androidx.benchmark.perfetto.Slice
-import androidx.benchmark.perfetto.processNameLikePkg
-import androidx.benchmark.perfetto.toSlices
+import androidx.benchmark.traceprocessor.Slice
+import androidx.benchmark.traceprocessor.TraceProcessor
+import androidx.benchmark.traceprocessor.processNameLikePkg
+import androidx.benchmark.traceprocessor.toSlices
internal object FrameTimingQuery {
private fun getFullQuery(packageName: String) =
@@ -146,7 +146,7 @@
}
internal fun getFrameData(
- session: PerfettoTraceProcessor.Session,
+ session: TraceProcessor.Session,
captureApiLevel: Int,
packageName: String,
): List<FrameData> {
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/MemoryCountersQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/MemoryCountersQuery.kt
index 5ad49d3..5aa36717 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/MemoryCountersQuery.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/MemoryCountersQuery.kt
@@ -16,8 +16,8 @@
package androidx.benchmark.macro.perfetto
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
-import androidx.benchmark.perfetto.processNameLikePkg
+import androidx.benchmark.traceprocessor.TraceProcessor
+import androidx.benchmark.traceprocessor.processNameLikePkg
internal object MemoryCountersQuery {
// https://perfetto.dev/docs/data-sources/memory-counters
@@ -58,10 +58,7 @@
val memoryReclaimEvents: Double
)
- fun getMemoryCounters(
- session: PerfettoTraceProcessor.Session,
- targetPackageName: String
- ): SubMetrics? {
+ fun getMemoryCounters(session: TraceProcessor.Session, targetPackageName: String): SubMetrics? {
val queryResultIterator =
session.query(query = getQuery(targetPackageName = targetPackageName))
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/MemoryUsageQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/MemoryUsageQuery.kt
index 680b469..c8c61e37 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/MemoryUsageQuery.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/MemoryUsageQuery.kt
@@ -18,8 +18,8 @@
import androidx.benchmark.macro.MemoryUsageMetric
import androidx.benchmark.macro.MemoryUsageMetric.Mode
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
-import androidx.benchmark.perfetto.processNameLikePkg
+import androidx.benchmark.traceprocessor.TraceProcessor
+import androidx.benchmark.traceprocessor.processNameLikePkg
internal object MemoryUsageQuery {
// https://perfetto.dev/docs/data-sources/memory-counters
@@ -44,7 +44,7 @@
.trimIndent()
fun getMemoryUsageKb(
- session: PerfettoTraceProcessor.Session,
+ session: TraceProcessor.Session,
targetPackageName: String,
mode: Mode
): Map<MemoryUsageMetric.SubMetric, Int>? {
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PowerQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PowerQuery.kt
index 9769eef..2553e68 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PowerQuery.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PowerQuery.kt
@@ -19,8 +19,8 @@
import androidx.benchmark.macro.ExperimentalMetricApi
import androidx.benchmark.macro.PowerCategory
import androidx.benchmark.macro.PowerMetric
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
-import androidx.benchmark.perfetto.Slice
+import androidx.benchmark.traceprocessor.Slice
+import androidx.benchmark.traceprocessor.TraceProcessor
// We want to use android_powrails.sql, but cannot as they do not split into sections with slice
@@ -89,7 +89,7 @@
}
fun getPowerMetrics(
- session: PerfettoTraceProcessor.Session,
+ session: TraceProcessor.Session,
slice: Slice
): Map<PowerCategory, CategoryMeasurement> {
// gather all recorded rails
@@ -127,7 +127,7 @@
}
private fun getRailMetrics(
- session: PerfettoTraceProcessor.Session,
+ session: TraceProcessor.Session,
slice: Slice
): List<ComponentMeasurement> {
val query = getFullQuery(slice)
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StartupInsights.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StartupInsights.kt
index e8c3232..b0befaa 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StartupInsights.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StartupInsights.kt
@@ -19,7 +19,7 @@
import androidx.benchmark.Insight
import androidx.benchmark.TraceDeepLink
import androidx.benchmark.inMemoryTrace
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
import perfetto.protos.AndroidStartupMetric.SlowStartReason
import perfetto.protos.AndroidStartupMetric.ThresholdValue.ThresholdUnit
import perfetto.protos.TraceMetrics
@@ -59,7 +59,7 @@
else -> " ${thresholdUnit.toString().lowercase()}"
}
- val thresholdValue = expected_value.value_!!
+ val thresholdValue = expected_value!!.value_!!
val thresholdString =
StringBuilder()
@@ -75,8 +75,8 @@
)
}
} else {
- if (expected_value.higher_expected == true) append("> ")
- if (expected_value.higher_expected == false) append("< ")
+ if (expected_value!!.higher_expected == true) append("> ")
+ if (expected_value!!.higher_expected == false) append("< ")
append(thresholdValue)
append(unitSuffix)
}
@@ -110,7 +110,7 @@
)
}
-internal fun PerfettoTraceProcessor.Session.queryStartupInsights(
+internal fun TraceProcessor.Session.queryStartupInsights(
helpUrlBase: String?,
traceOutputRelativePath: String,
iteration: Int,
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StartupTimingQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StartupTimingQuery.kt
index 850a291..5897fd5 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StartupTimingQuery.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StartupTimingQuery.kt
@@ -18,10 +18,10 @@
import android.util.Log
import androidx.benchmark.macro.StartupMode
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
-import androidx.benchmark.perfetto.Slice
-import androidx.benchmark.perfetto.processNameLikePkg
-import androidx.benchmark.perfetto.toSlices
+import androidx.benchmark.traceprocessor.Slice
+import androidx.benchmark.traceprocessor.TraceProcessor
+import androidx.benchmark.traceprocessor.processNameLikePkg
+import androidx.benchmark.traceprocessor.toSlices
internal object StartupTimingQuery {
private fun getFullQuery(targetPackageName: String) =
@@ -113,7 +113,7 @@
}
fun getFrameSubMetrics(
- session: PerfettoTraceProcessor.Session,
+ session: TraceProcessor.Session,
captureApiLevel: Int,
targetPackageName: String,
startupMode: StartupMode
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/server/PerfettoHttpServer.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/server/PerfettoHttpServer.kt
deleted file mode 100644
index 4fb285b..0000000
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/server/PerfettoHttpServer.kt
+++ /dev/null
@@ -1,318 +0,0 @@
-/*
- * Copyright 2022 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
- *
- * http://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 androidx.benchmark.macro.perfetto.server
-
-import android.annotation.SuppressLint
-import android.os.Build
-import android.security.NetworkSecurityPolicy
-import android.util.Log
-import androidx.annotation.RequiresApi
-import androidx.benchmark.Shell
-import androidx.benchmark.ShellScript
-import androidx.benchmark.inMemoryTrace
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
-import java.io.FileNotFoundException
-import java.io.IOException
-import java.io.InputStream
-import java.io.OutputStream
-import java.net.ConnectException
-import java.net.HttpURLConnection
-import java.net.URL
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
-import kotlin.time.Duration.Companion.seconds
-import kotlin.time.DurationUnit
-import perfetto.protos.AppendTraceDataResult
-import perfetto.protos.ComputeMetricArgs
-import perfetto.protos.ComputeMetricResult
-import perfetto.protos.QueryArgs
-import perfetto.protos.StatusResult
-
-/**
- * Wrapper around perfetto trace_shell_processor that communicates via http. The implementation is
- * based on the python one of the official repo:
- * https://github.com/google/perfetto/blob/master/python/perfetto/trace_processor/http.py
- */
-internal class PerfettoHttpServer {
-
- companion object {
- private const val HTTP_ADDRESS = "http://localhost"
- private const val METHOD_GET = "GET"
- private const val METHOD_POST = "POST"
- private const val PATH_QUERY = "/query"
- private const val PATH_COMPUTE_METRIC = "/compute_metric"
- private const val PATH_PARSE = "/parse"
- private const val PATH_NOTIFY_EOF = "/notify_eof"
- private const val PATH_STATUS = "/status"
- private const val PATH_RESTORE_INITIAL_TABLES = "/restore_initial_tables"
-
- private const val TAG = "PerfettoHttpServer"
- private const val SERVER_PROCESS_NAME = "trace_processor_shell"
- private val WAIT_INTERVAL = 5.milliseconds
- private val READ_TIMEOUT = 30.seconds
-
- // Note that trace processor http server has a hard limit of 64Mb for payload size.
- // https://cs.android.com/android/platform/superproject/+/master:external/perfetto/src/base/http/http_server.cc;l=33
- private const val PARSE_PAYLOAD_SIZE = 16 * 1024 * 1024 // 16Mb
-
- private var shellScript: ShellScript? = null
-
- /**
- * Returns a cached instance of the shell script to run the perfetto trace shell processor
- * as http server. Note that the generated script doesn't specify the port and this must be
- * passed as parameter when running the script.
- */
- fun getOrCreateShellScript(): ShellScript =
- shellScript
- ?: synchronized(this) {
- var instance = shellScript
- if (instance != null) {
- return@synchronized instance
- }
- val script =
- "echo pid:$$ ; exec ${PerfettoTraceProcessor.shellPath} -D" +
- " --http-port \"${PerfettoTraceProcessor.PORT}\" "
- instance = Shell.createShellScript(script)
- shellScript = instance
- instance
- }
-
- /** Clean up the shell script */
- fun cleanUpShellScript() =
- synchronized(this) {
- shellScript?.cleanUp()
- shellScript = null
- }
- }
-
- private var processId: Int? = null
-
- /**
- * Blocking method that runs the perfetto trace_shell_processor in server mode.
- *
- * @throws IllegalStateException if the server is not running by the end of the timeout.
- */
- @SuppressLint("BanThreadSleep")
- fun startServer(timeout: Duration) =
- inMemoryTrace("PerfettoHttpServer#startServer") {
- if (processId != null) {
- Log.w(TAG, "Tried to start a trace shell processor that is already running.")
- return@inMemoryTrace
- }
-
- if (
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
- !Api24Impl.isCleartextTrafficPermittedForLocalhost()
- ) {
- throw IOException(
- """
- Macrobenchmark requires cleartext HTTP traffic to the on-device localhost to enable
- querying data from perfetto traces, such as timestamps that are used to calculate
- metrics. This should be enabled by default via manifest merging when building with
- Gradle. Please refer to
- https://d.android.com/training/articles/security-config#CleartextTrafficPermitted
- and enable cleartext http requests towards localhost in your test android manifest.
- """
- .trimIndent()
- )
- }
-
- val shellScript = getOrCreateShellScript().start()
-
- processId =
- shellScript
- .stdOutLineSequence()
- .first { it.startsWith("pid:") }
- .split("pid:")[1]
- .toInt()
-
- // Wait for the trace_processor_shell server to start.
- var elapsed = 0.milliseconds
- while (!isRunning()) {
- Thread.sleep(WAIT_INTERVAL.toLong(DurationUnit.MILLISECONDS))
- elapsed += WAIT_INTERVAL
- if (elapsed >= timeout) {
-
- // In the event that the instrumentation app cannot connect to the
- // trace_processor_shell server, trying to read the full stderr may make the
- // process hang. Here we check if the process is still running to determine if
- // that's the case and throw the correct exception.
-
- val processRunning =
- processId?.let { Shell.isProcessAlive(it, SERVER_PROCESS_NAME) } ?: false
-
- if (processRunning) {
- throw IllegalStateException(
- """
- The instrumentation app cannot connect to the trace_processor_shell server.
- """
- .trimIndent()
- )
- } else {
- throw IllegalStateException(
- """
- Perfetto trace_processor_shell did not start correctly.
- Process stderr:
- ${shellScript.getOutputAndClose().stderr}
- """
- .trimIndent()
- )
- }
- }
- }
- Log.i(TAG, "Perfetto trace processor shell server started (pid=$processId).")
- }
-
- /** Stops the server killing the associated process */
- fun stopServer() =
- inMemoryTrace("PerfettoHttpServer#stopServer") {
- if (processId == null) {
- Log.w(TAG, "Tried to stop trace shell processor http server without starting it.")
- return@inMemoryTrace
- }
- Shell.executeScriptSilent("kill -TERM $processId")
- Log.i(TAG, "Perfetto trace processor shell server stopped (pid=$processId).")
- }
-
- /** Returns true whether the server is running, false otherwise. */
- fun isRunning(): Boolean =
- inMemoryTrace("PerfettoHttpServer#isRunning") {
- return@inMemoryTrace try {
- val statusResult = status()
- return@inMemoryTrace statusResult.api_version != null &&
- statusResult.api_version > 0
- } catch (e: ConnectException) {
- // Note that this is fired when the server port is not bound yet.
- // This can happen before the perfetto trace processor server is fully started.
- false
- } catch (e: FileNotFoundException) {
- // Note that this is fired when the endpoint queried does not exist.
- // This can happen before the perfetto trace processor server is fully started.
- false
- }
- }
-
- /**
- * Executes the given [sqlQuery] on a previously parsed trace with custom decoding.
- *
- * Note that this does not decode the query result, so it's the caller's responsibility to check
- * for errors in the result.
- */
- fun <T> rawQuery(sqlQuery: String, decodeBlock: (InputStream) -> T): T =
- httpRequest(
- method = METHOD_POST,
- url = PATH_QUERY,
- encodeBlock = { QueryArgs.ADAPTER.encode(it, QueryArgs(sqlQuery)) },
- decodeBlock = decodeBlock
- )
-
- /** Computes the given metrics on a previously parsed trace. */
- fun computeMetric(
- metrics: List<String>,
- resultFormat: ComputeMetricArgs.ResultFormat
- ): ComputeMetricResult =
- httpRequest(
- method = METHOD_POST,
- url = PATH_COMPUTE_METRIC,
- encodeBlock = {
- ComputeMetricArgs.ADAPTER.encode(it, ComputeMetricArgs(metrics, resultFormat))
- },
- decodeBlock = { ComputeMetricResult.ADAPTER.decode(it) }
- )
-
- /**
- * Parses the trace file in chunks. Note that [notifyEof] should be called at the end to let the
- * processor know that no more chunks will be sent.
- */
- fun parse(inputStream: InputStream): List<AppendTraceDataResult> {
- val responses = mutableListOf<AppendTraceDataResult>()
- while (true) {
- val buffer = ByteArray(PARSE_PAYLOAD_SIZE)
- val read = inputStream.read(buffer)
- if (read <= 0) break
- responses.add(
- httpRequest(
- method = METHOD_POST,
- url = PATH_PARSE,
- encodeBlock = { it.write(buffer, 0, read) },
- decodeBlock = { AppendTraceDataResult.ADAPTER.decode(it) }
- )
- )
- }
- return responses
- }
-
- /** Notifies that the entire trace has been uploaded and no more chunks will be sent. */
- fun notifyEof() =
- httpRequest(
- method = METHOD_GET,
- url = PATH_NOTIFY_EOF,
- encodeBlock = null,
- decodeBlock = {}
- )
-
- /** Clears the loaded trace and restore the state of the initial tables */
- fun restoreInitialTables() =
- httpRequest(
- method = METHOD_GET,
- url = PATH_RESTORE_INITIAL_TABLES,
- encodeBlock = null,
- decodeBlock = {}
- )
-
- /** Checks the status of the trace_shell_processor http server. */
- private fun status(): StatusResult =
- httpRequest(
- method = METHOD_GET,
- url = PATH_STATUS,
- encodeBlock = null,
- decodeBlock = { StatusResult.ADAPTER.decode(it) }
- )
-
- private fun <T> httpRequest(
- method: String,
- url: String,
- contentType: String = "application/octet-stream",
- encodeBlock: ((OutputStream) -> Unit)?,
- decodeBlock: ((InputStream) -> T)
- ): T {
- with(
- URL("$HTTP_ADDRESS:${PerfettoTraceProcessor.PORT}$url").openConnection()
- as HttpURLConnection
- ) {
- requestMethod = method
- readTimeout = READ_TIMEOUT.toInt(DurationUnit.MILLISECONDS)
- setRequestProperty("Content-Type", contentType)
- if (encodeBlock != null) {
- doOutput = true
- encodeBlock(outputStream)
- outputStream.close()
- }
- val value = decodeBlock(inputStream)
- if (responseCode != 200) {
- throw IllegalStateException(responseMessage)
- }
- return value
- }
- }
-}
-
-@RequiresApi(24)
-private object Api24Impl {
- fun isCleartextTrafficPermittedForLocalhost() =
- NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted("localhost")
-}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/ExperimentalPerfettoTraceProcessorApi.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/ExperimentalPerfettoTraceProcessorApi.kt
deleted file mode 100644
index 27f9387..0000000
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/ExperimentalPerfettoTraceProcessorApi.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright 2021 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
- *
- * http://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 androidx.benchmark.perfetto
-
-/**
- * Annotation indicating experimental API for querying data from Perfetto traces with Perfetto Trace
- * Processor.
- */
-@RequiresOptIn
-@Retention(AnnotationRetention.BINARY)
-@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
-annotation class ExperimentalPerfettoTraceProcessorApi
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/PerfettoTraceProcessor.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/PerfettoTraceProcessor.kt
deleted file mode 100644
index c6d5a9d..0000000
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/PerfettoTraceProcessor.kt
+++ /dev/null
@@ -1,479 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx.benchmark.perfetto
-
-import androidx.annotation.RestrictTo
-import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
-import androidx.benchmark.InstrumentationResults
-import androidx.benchmark.Outputs
-import androidx.benchmark.Profiler
-import androidx.benchmark.inMemoryTrace
-import androidx.benchmark.macro.perfetto.server.PerfettoHttpServer
-import java.io.File
-import java.io.FileInputStream
-import java.io.InputStream
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.seconds
-import org.intellij.lang.annotations.Language
-import perfetto.protos.ComputeMetricArgs
-import perfetto.protos.ComputeMetricResult
-import perfetto.protos.QueryResult
-import perfetto.protos.TraceMetrics
-
-/**
- * Kotlin API for [Perfetto Trace Processor](https://perfetto.dev/docs/analysis/trace-processor),
- * which enables SQL querying against the data stored in a Perfetto trace.
- *
- * This includes synchronous and async trace sections, kernel-level scheduling timing, binder
- * events... If it's displayed in Android Studio system trace or
- * [ui.perfetto.dev](https://ui.perfetto.dev), it can be queried from this API.
- *
- * ```
- * // Collect the duration of all slices named "activityStart" in the trace
- * val activityStartDurationNs = PerfettoTraceProcessor.runServer {
- * loadTrace(trace) {
- * query("SELECT dur FROM slice WHERE name LIKE \"activityStart\"").toList {
- * it.long("dur")
- * }
- * }
- * }
- * ```
- *
- * Note that traces generally hold events from multiple apps, services and processes, so it's
- * recommended to filter potentially common trace events to the process you're interested in. See
- * the following example which queries `Choreographer#doFrame` slices (labelled spans of time) only
- * for a given package name:
- * ```
- * query("""
- * |SELECT
- * | slice.name,slice.ts,slice.dur
- * |FROM slice
- * | INNER JOIN thread_track on slice.track_id = thread_track.id
- * | INNER JOIN thread USING(utid)
- * | INNER JOIN process USING(upid)
- * |WHERE
- * | slice.name LIKE "Choreographer#doFrame%" AND
- * | process.name LIKE "$packageName"
- * """.trimMargin()
- * )
- * ```
- *
- * See also Perfetto project documentation:
- * * [Trace Processor overview](https://perfetto.dev/docs/analysis/trace-processor)
- * * [Common queries](https://perfetto.dev/docs/analysis/common-queries)
- *
- * @see PerfettoTrace
- */
-@ExperimentalPerfettoTraceProcessorApi
-class PerfettoTraceProcessor {
- companion object {
- private val SERVER_START_TIMEOUT_MS = 60.seconds
- internal const val PORT = 9001
-
- /**
- * The actual [File] path to the `trace_processor_shell`.
- *
- * Lazily copies the `trace_processor_shell` and enables parsing of the Perfetto trace
- * files.
- */
- internal val shellPath: String by lazy {
- // Checks for ABI support
- PerfettoHelper.createExecutable("trace_processor_shell")
- }
-
- /**
- * Starts a Perfetto trace processor shell server in http mode, loads a trace and executes
- * the given block. It stops the server after the block is complete
- *
- * Uses a default timeout of 5 seconds
- *
- * @param block Command to execute using trace processor
- */
- @JvmStatic
- fun <T> runServer(block: PerfettoTraceProcessor.() -> T): T =
- runServer(SERVER_START_TIMEOUT_MS, block)
-
- /**
- * Starts a Perfetto trace processor shell server in http mode, loads a trace and executes
- * the given block. It stops the server after the block is complete
- *
- * @param timeout waiting for the server to start. If less or equal to zero use 5 seconds
- * @param block Command to execute using trace processor
- */
- @JvmStatic
- fun <T> runServer(timeout: Duration, block: PerfettoTraceProcessor.() -> T): T =
- inMemoryTrace("PerfettoTraceProcessor#runServer") {
- var actualTimeout = timeout
- if (actualTimeout <= Duration.ZERO) {
- actualTimeout = SERVER_START_TIMEOUT_MS
- }
-
- var perfettoTraceProcessor: PerfettoTraceProcessor? = null
- try {
-
- // Initializes the server process
- perfettoTraceProcessor = PerfettoTraceProcessor().startServer(actualTimeout)
-
- // Executes the query block
- return@inMemoryTrace inMemoryTrace("PerfettoTraceProcessor#runServer#block") {
- block(perfettoTraceProcessor)
- }
- } finally {
- perfettoTraceProcessor?.stopServer()
- }
- }
-
- @RestrictTo(LIBRARY_GROUP)
- fun <T> runSingleSessionServer(absoluteTracePath: String, block: Session.() -> T) =
- runServer {
- loadTrace(PerfettoTrace(absoluteTracePath)) { block(this) }
- }
- }
-
- /** Loads a PerfettoTrace into the trace processor server to query data out of the trace. */
- fun <T> loadTrace(trace: PerfettoTrace, block: Session.() -> T): T {
- loadTraceImpl(trace.path)
- // TODO: unload trace after block
- try {
- return block.invoke(Session(this))
- } catch (t: Throwable) {
- // TODO: move this behavior to an extension function in benchmark when
- // this class moves out of benchmark group
- // TODO: consider a label argument to control logging like this in the success case as
- // well, which lets us get rid of FileLinkingRule (which doesn't work well anyway)
- if (trace.path.startsWith(Outputs.outputDirectory.absolutePath)) {
- // only link trace with failure to Studio if it's an output file
- InstrumentationResults.instrumentationReport {
- val label = "Trace with processing error: ${t.message?.take(50)?.trim()}..."
- reportSummaryToIde(
- profilerResults =
- listOf(
- Profiler.ResultFile.ofPerfettoTrace(
- label = label,
- absolutePath = trace.path
- )
- )
- )
- }
- }
- throw t
- }
- }
-
- /**
- * Handle to query sql data from a [PerfettoTrace].
- *
- * @see query
- */
- class Session internal constructor(private val traceProcessor: PerfettoTraceProcessor) {
- /** Computes the given metric on the previously loaded trace. */
- @RestrictTo(LIBRARY_GROUP) // avoids exposing Proto API
- fun getTraceMetrics(metric: String): TraceMetrics {
- val computeResult =
- queryAndVerifyMetricResult(
- listOf(metric),
- ComputeMetricArgs.ResultFormat.BINARY_PROTOBUF
- )
- return TraceMetrics.ADAPTER.decode(computeResult.metrics!!)
- }
-
- /**
- * Computes the given metrics, returning the results as a binary proto.
- *
- * The proto format definition for decoding this binary format can be found
- * [here](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/protos/perfetto/metrics/).
- *
- * See
- * [perfetto metric docs](https://perfetto.dev/docs/quickstart/trace-analysis#trace-based-metrics)
- * for an overview on trace based metrics.
- */
- fun queryMetricsProtoBinary(metrics: List<String>): ByteArray {
- val computeResult =
- queryAndVerifyMetricResult(metrics, ComputeMetricArgs.ResultFormat.BINARY_PROTOBUF)
- return computeResult.metrics!!.toByteArray()
- }
-
- /**
- * Computes the given metrics, returning the results as JSON text.
- *
- * The proto format definition for these metrics can be found
- * [here](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/protos/perfetto/metrics/).
- *
- * See
- * [perfetto metric docs](https://perfetto.dev/docs/quickstart/trace-analysis#trace-based-metrics)
- * for an overview on trace based metrics.
- */
- fun queryMetricsJson(metrics: List<String>): String {
- val computeResult =
- queryAndVerifyMetricResult(metrics, ComputeMetricArgs.ResultFormat.JSON)
- check(computeResult.metrics_as_json != null)
- return computeResult.metrics_as_json
- }
-
- /**
- * Computes the given metrics, returning the result as proto text.
- *
- * The proto format definition for these metrics can be found
- * [here](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/protos/perfetto/metrics/).
- *
- * See
- * [perfetto metric docs](https://perfetto.dev/docs/quickstart/trace-analysis#trace-based-metrics)
- * for an overview on trace based metrics.
- */
- fun queryMetricsProtoText(metrics: List<String>): String {
- val computeResult =
- queryAndVerifyMetricResult(metrics, ComputeMetricArgs.ResultFormat.TEXTPROTO)
- check(computeResult.metrics_as_prototext != null)
- return computeResult.metrics_as_prototext
- }
-
- private fun queryAndVerifyMetricResult(
- metrics: List<String>,
- format: ComputeMetricArgs.ResultFormat
- ): ComputeMetricResult {
- val nameString = metrics.joinToString()
- require(metrics.none { it.contains(" ") }) {
- "Metrics must not constain spaces, metrics: $nameString"
- }
-
- inMemoryTrace("PerfettoTraceProcessor#getTraceMetrics $nameString") {
- require(traceProcessor.perfettoHttpServer.isRunning()) {
- "Perfetto trace_shell_process is not running."
- }
-
- // Compute metrics
- val computeResult = traceProcessor.perfettoHttpServer.computeMetric(metrics, format)
- if (computeResult.error != null) {
- throw IllegalStateException(computeResult.error)
- }
-
- return computeResult
- }
- }
-
- /**
- * Computes the given query on the currently loaded trace.
- *
- * Each row returned by a query is returned by the `Sequence` as a [Row]. To extract data
- * from a `Row`, query by column name. The following example does this for name, timestamp,
- * and duration of slices:
- * ```
- * // Runs the provided callback on each activityStart instance in the trace,
- * // providing name, start timestamp (in ns) and duration (in ns)
- * fun PerfettoTraceProcessor.Session.forEachActivityStart(callback: (String, Long, Long) -> Unit) {
- * query("SELECT name,ts,dur FROM slice WHERE name LIKE \"activityStart\"").forEach {
- * callback(it.string("name"), it.long("ts"), it.long("dur")
- * // or, used as a map:
- * //callback(it["name"] as String, it["ts"] as Long, it["dur"] as Long)
- * }
- * }
- * ```
- *
- * @see PerfettoTraceProcessor
- * @see PerfettoTraceProcessor.Session
- */
- fun query(query: String): Sequence<Row> {
- inMemoryTrace("PerfettoTraceProcessor#query $query".take(127)) {
- require(traceProcessor.perfettoHttpServer.isRunning()) {
- "Perfetto trace_shell_process is not running."
- }
- val queryResult =
- traceProcessor.perfettoHttpServer.rawQuery(query) {
- // Note: check for errors as part of decode, so it's immediate
- // instead of lazily in QueryResultIterator
- QueryResult.decodeAndCheckError(query, it)
- }
- return Sequence { QueryResultIterator(queryResult) }
- }
- }
-
- private fun QueryResult.Companion.decodeAndCheckError(
- query: String,
- inputStream: InputStream
- ) =
- ADAPTER.decode(inputStream).also {
- check(it.error == null) {
- throw IllegalStateException("Error with query: --$query--, error=${it.error}")
- }
- }
-
- /**
- * Computes the given query on the currently loaded trace, returning the resulting protobuf
- * bytes as a [ByteArray].
- *
- * Use [Session.query] if you do not wish to parse the Proto result yourself.
- *
- * The `QueryResult` protobuf definition can be found
- * [in the Perfetto project](https://github.com/google/perfetto/blob/master/protos/perfetto/trace_processor/trace_processor.proto),
- * which can be used to decode the result returned here with a protobuf parsing library.
- *
- * Note that this method does not check for errors in the protobuf, that is the caller's
- * responsibility.
- *
- * @see Session.query
- */
- fun rawQuery(@Language("sql") query: String): ByteArray {
- inMemoryTrace("PerfettoTraceProcessor#query $query".take(127)) {
- require(traceProcessor.perfettoHttpServer.isRunning()) {
- "Perfetto trace_shell_process is not running."
- }
- return traceProcessor.perfettoHttpServer.rawQuery(query) { it.readBytes() }
- }
- }
-
- /**
- * Query a trace for a list of slices - name, timestamp, and duration.
- *
- * Note that sliceNames may include wildcard matches, such as `foo%`
- */
- @RestrictTo(LIBRARY_GROUP) // Slice API not currently exposed, since it doesn't track table
- fun querySlices(
- vararg sliceNames: String,
- packageName: String?,
- ): List<Slice> {
- require(traceProcessor.perfettoHttpServer.isRunning()) {
- "Perfetto trace_shell_process is not running."
- }
-
- val whereClause =
- sliceNames.joinToString(
- separator = " OR ",
- prefix =
- if (packageName == null) {
- "("
- } else {
- processNameLikePkg(packageName) + " AND ("
- },
- postfix = ")"
- ) {
- "slice_name LIKE \"$it\""
- }
- val innerJoins =
- if (packageName != null) {
- """
- INNER JOIN thread_track ON slice.track_id = thread_track.id
- INNER JOIN thread USING(utid)
- INNER JOIN process USING(upid)
- """
- .trimMargin()
- } else {
- ""
- }
-
- val processTrackInnerJoins =
- """
- INNER JOIN process_track ON slice.track_id = process_track.id
- INNER JOIN process USING(upid)
- """
- .trimIndent()
-
- return query(
- query =
- """
- SELECT slice.name AS slice_name,ts,dur
- FROM slice
- $innerJoins
- WHERE $whereClause
- UNION
- SELECT process_track.name AS slice_name,ts,dur
- FROM slice
- $processTrackInnerJoins
- WHERE $whereClause
- ORDER BY ts
- """
- .trimIndent()
- )
- .map { row ->
- // Using an explicit mapper here to account for the aliasing of `slice_name`
- Slice(
- name = row.string("slice_name"),
- ts = row.long("ts"),
- dur = row.long("dur")
- )
- }
- .filter { it.dur != -1L } // filter out non-terminating slices
- .toList()
- }
- }
-
- private val perfettoHttpServer: PerfettoHttpServer = PerfettoHttpServer()
- private var traceLoaded = false
-
- private fun startServer(timeout: Duration): PerfettoTraceProcessor =
- inMemoryTrace("PerfettoTraceProcessor#startServer") {
- println("startserver($timeout)")
- perfettoHttpServer.startServer(timeout)
- return@inMemoryTrace this
- }
-
- private fun stopServer() =
- inMemoryTrace("PerfettoTraceProcessor#stopServer") {
- println("stopserver")
- perfettoHttpServer.stopServer()
- }
-
- /**
- * Loads a trace in the current instance of the trace processor, clearing any previous loaded
- * trace if existing.
- */
- private fun loadTraceImpl(absoluteTracePath: String) {
- inMemoryTrace("PerfettoTraceProcessor#loadTraceImpl") {
- require(!absoluteTracePath.contains(" ")) {
- "Trace path must not contain spaces: $absoluteTracePath"
- }
-
- val traceFile = File(absoluteTracePath)
- require(traceFile.exists() && traceFile.isFile) {
- "Trace path must exist and not be a directory: $absoluteTracePath"
- }
-
- // In case a previous trace was loaded, ensures to clear
- if (traceLoaded) {
- clearTrace()
- }
-
- val parseResults = perfettoHttpServer.parse(FileInputStream(traceFile))
- parseResults.forEach { if (it.error != null) throw IllegalStateException(it.error) }
-
- // Notifies the server that it won't receive any more trace parts
- perfettoHttpServer.notifyEof()
-
- traceLoaded = true
- }
- }
-
- /** Clears the current loaded trace. */
- private fun clearTrace() =
- inMemoryTrace("PerfettoTraceProcessor#clearTrace") {
- perfettoHttpServer.restoreInitialTables()
- traceLoaded = false
- }
-}
-
-/** Helper for fuzzy matching process name to package */
-internal fun processNameLikePkg(pkg: String): String {
- // check for truncated package names, which can sometimes occur if perfetto can't capture full
- // names, and only has 16 bytes from sched info (which results in 15 chars due to null
- // termination)
- val truncated =
- if (pkg.length > 15) {
- " OR process.name LIKE \"${pkg.takeLast(15)}\""
- } else {
- ""
- }
- return """(process.name LIKE "$pkg" OR process.name LIKE "$pkg:%"$truncated)"""
-}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/QueryResultIterator.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/QueryResultIterator.kt
deleted file mode 100644
index dd70839..0000000
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/QueryResultIterator.kt
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx.benchmark.perfetto
-
-import perfetto.protos.QueryResult
-
-/** Iterator for results from a [PerfettoTraceProcessor] query. */
-internal class QueryResultIterator constructor(queryResult: QueryResult) : Iterator<Row> {
- private val dataLists =
- object {
- val stringBatches = mutableListOf<String>()
- val varIntBatches = mutableListOf<Long>()
- val float64Batches = mutableListOf<Double>()
- val blobBatches = mutableListOf<ByteArray>()
-
- var stringIndex = 0
- var varIntIndex = 0
- var float64Index = 0
- var blobIndex = 0
- }
-
- private val cells = mutableListOf<QueryResult.CellsBatch.CellType>()
- private val columnNames = queryResult.column_names
- private val columnCount: Int
- private val count: Int
-
- private var currentIndex = 0
-
- init {
- // Parsing every batch
- for (batch in queryResult.batch) {
- val stringsBatch = batch.string_cells!!.split(0x00.toChar()).dropLast(1)
- dataLists.stringBatches.addAll(stringsBatch)
- dataLists.varIntBatches.addAll(batch.varint_cells)
- dataLists.float64Batches.addAll(batch.float64_cells)
- dataLists.blobBatches.addAll(batch.blob_cells.map { it.toByteArray() })
- cells.addAll(batch.cells)
- }
-
- columnCount = columnNames.size
- count = if (columnCount > 0) cells.size / columnCount else 0
- }
-
- /** Returns the number of rows in the query result. */
- fun size(): Int {
- return count
- }
-
- /** Returns true whether there are no results stored in this iterator, false otherwise. */
- fun isEmpty(): Boolean {
- return count == 0
- }
-
- /** Returns true if there are more rows not yet parsed from the query result. */
- override fun hasNext(): Boolean {
- return currentIndex < count
- }
-
- /**
- * Returns a map containing the next row of the query results.
- *
- * @throws IllegalArgumentException if the query returns an invalid cell type
- * @throws NoSuchElementException if the query has no next row.
- */
- override fun next(): Row {
- // Parsing logic is copied from the python project:
- // https://github.com/google/perfetto/blob/master/python/perfetto/trace_processor/api.py#L89
-
- if (!hasNext()) throw NoSuchElementException()
-
- val row = mutableMapOf<String, Any?>()
- val baseCellIndex = currentIndex * columnCount
-
- for ((num, columnName) in columnNames.withIndex()) {
- val colType = cells[baseCellIndex + num]
- val colIndex: Int
- row[columnName] =
- when (colType) {
- QueryResult.CellsBatch.CellType.CELL_STRING -> {
- colIndex = dataLists.stringIndex
- dataLists.stringIndex += 1
- dataLists.stringBatches[colIndex]
- }
- QueryResult.CellsBatch.CellType.CELL_VARINT -> {
- colIndex = dataLists.varIntIndex
- dataLists.varIntIndex += 1
- dataLists.varIntBatches[colIndex]
- }
- QueryResult.CellsBatch.CellType.CELL_FLOAT64 -> {
- colIndex = dataLists.float64Index
- dataLists.float64Index += 1
- dataLists.float64Batches[colIndex]
- }
- QueryResult.CellsBatch.CellType.CELL_BLOB -> {
- colIndex = dataLists.blobIndex
- dataLists.blobIndex += 1
- dataLists.blobBatches[colIndex]
- }
- QueryResult.CellsBatch.CellType.CELL_INVALID ->
- throw IllegalArgumentException("Invalid cell type")
- QueryResult.CellsBatch.CellType.CELL_NULL -> null
- }
- }
-
- currentIndex += 1
- return Row(row)
- }
-}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/Row.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/Row.kt
deleted file mode 100644
index ed706f2..0000000
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/Row.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx.benchmark.perfetto
-
-/**
- * Convenience for constructing a RowResult for given column values.
- *
- * Useful when asserting expected query results in tests.
- */
-@ExperimentalPerfettoTraceProcessorApi
-fun rowOf(vararg pairs: Pair<String, Any?>): Row {
- return Row(pairs.toMap())
-}
-
-/**
- * A Map<String, Any?> that maps column name to value in a row result from a [QueryResultIterator].
- *
- * Provides convenience methods for converting to internal base types - `String`, `Long`, `Double`,
- * and `ByteArray`.
- *
- * ```
- * session.query("SELECT name,ts,dur FROM slice WHERE name LIKE \"activityStart\"").forEach {
- * callback(it.string("name"), it.long("ts"), it.long("dur")
- * // or, used as a map:
- * //callback(it["name"] as String, it["ts"] as Long, it["dur"] as Long)
- * }
- * ```
- *
- * Nullable variants of each convenience method are also provided.
- */
-@ExperimentalPerfettoTraceProcessorApi
-class Row(private val map: Map<String, Any?>) : Map<String, Any?> by map {
- fun string(columnName: String): String = map[columnName] as String
-
- fun long(columnName: String): Long = map[columnName] as Long
-
- fun double(columnName: String): Double = map[columnName] as Double
-
- fun bytes(columnName: String): ByteArray = map[columnName] as ByteArray
-
- fun nullableString(columnName: String): String? = map[columnName] as String?
-
- @Suppress("AutoBoxing") // primitives are already internally boxed
- fun nullableLong(columnName: String): Long? = map[columnName] as Long?
-
- @Suppress("AutoBoxing") // primitives are already internally boxed
- fun nullableDouble(columnName: String): Double? = map[columnName] as Double?
-
- fun nullableBytes(columnName: String): ByteArray? = map[columnName] as ByteArray?
-
- override fun hashCode(): Int = map.hashCode()
-
- override fun toString(): String = map.toString()
-
- override fun equals(other: Any?): Boolean = map == other
-}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/Slice.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/Slice.kt
deleted file mode 100644
index 28273dd..0000000
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/Slice.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx.benchmark.perfetto
-
-import androidx.annotation.RestrictTo
-
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-data class Slice(val name: String, val ts: Long, val dur: Long) {
- val endTs: Long = ts + dur
-
- val frameId: Int?
-
- init {
- val firstSpaceIndex = name.indexOf(" ")
- frameId =
- if (firstSpaceIndex >= 0) {
- // if see a space, check for id from end of first space to next space (or end of
- // String)
- val secondSpaceIndex = name.indexOf(" ", firstSpaceIndex + 1)
- val endFrameIdIndex = if (secondSpaceIndex < 0) name.length else secondSpaceIndex
- name.substring(firstSpaceIndex + 1, endFrameIdIndex).toIntOrNull()
- } else {
- null
- }
- }
-
- fun contains(targetTs: Long): Boolean {
- return targetTs >= ts && targetTs <= (ts + dur)
- }
-}
-
-/**
- * Convenient function to immediately retrieve a list of slices. Note that this method is provided
- * for convenience.
- */
-internal fun Sequence<Row>.toSlices(): List<Slice> =
- map { Slice(name = it.string("name"), ts = it.long("ts"), dur = it.long("dur")) }.toList()
diff --git a/benchmark/benchmark-traceprocessor/api/current.txt b/benchmark/benchmark-traceprocessor/api/current.txt
new file mode 100644
index 0000000..71edcdb
--- /dev/null
+++ b/benchmark/benchmark-traceprocessor/api/current.txt
@@ -0,0 +1,77 @@
+// Signature format: 4.0
+package androidx.benchmark.traceprocessor {
+
+ @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface ExperimentalTraceProcessorApi {
+ }
+
+ public final class PerfettoTrace {
+ ctor public PerfettoTrace(String path);
+ field public static final androidx.benchmark.traceprocessor.PerfettoTrace.Companion Companion;
+ }
+
+ public static final class PerfettoTrace.Companion {
+ }
+
+ public final class Row implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<java.lang.String,java.lang.Object?> {
+ ctor public Row(java.util.Map<java.lang.String,? extends java.lang.Object?> map);
+ method public byte[] bytes(String columnName);
+ method public double double(String columnName);
+ method public long long(String columnName);
+ method public byte[]? nullableBytes(String columnName);
+ method public Double? nullableDouble(String columnName);
+ method public Long? nullableLong(String columnName);
+ method public String? nullableString(String columnName);
+ method public String string(String columnName);
+ }
+
+ public final class RowKt {
+ method public static androidx.benchmark.traceprocessor.Row rowOf(kotlin.Pair<java.lang.String,? extends java.lang.Object?>... pairs);
+ }
+
+ @SuppressCompatibility @androidx.benchmark.traceprocessor.ExperimentalTraceProcessorApi public interface ServerLifecycleManager {
+ method public int start();
+ method public void stop();
+ method public default String timeoutMessage();
+ }
+
+ public final class TraceProcessor {
+ ctor @SuppressCompatibility @androidx.benchmark.traceprocessor.ExperimentalTraceProcessorApi public TraceProcessor(androidx.benchmark.traceprocessor.ServerLifecycleManager serverLifecycleManager, optional androidx.benchmark.traceprocessor.TraceProcessor.Tracer tracer, optional androidx.benchmark.traceprocessor.TraceProcessor.EventCallback eventCallback);
+ method public <T> T loadTrace(androidx.benchmark.traceprocessor.PerfettoTrace trace, kotlin.jvm.functions.Function1<? super androidx.benchmark.traceprocessor.TraceProcessor.Session,? extends T> block);
+ method @SuppressCompatibility @androidx.benchmark.traceprocessor.ExperimentalTraceProcessorApi public static <T> T runServer(androidx.benchmark.traceprocessor.ServerLifecycleManager serverLifecycleManager, androidx.benchmark.traceprocessor.TraceProcessor.EventCallback eventCallback, androidx.benchmark.traceprocessor.TraceProcessor.Tracer tracer, optional long timeout, kotlin.jvm.functions.Function1<? super androidx.benchmark.traceprocessor.TraceProcessor,? extends T> block);
+ field public static final androidx.benchmark.traceprocessor.TraceProcessor.Companion Companion;
+ }
+
+ public static final class TraceProcessor.Companion {
+ method @SuppressCompatibility @androidx.benchmark.traceprocessor.ExperimentalTraceProcessorApi public <T> T runServer(androidx.benchmark.traceprocessor.ServerLifecycleManager serverLifecycleManager, androidx.benchmark.traceprocessor.TraceProcessor.EventCallback eventCallback, androidx.benchmark.traceprocessor.TraceProcessor.Tracer tracer, optional long timeout, kotlin.jvm.functions.Function1<? super androidx.benchmark.traceprocessor.TraceProcessor,? extends T> block);
+ }
+
+ public static interface TraceProcessor.EventCallback {
+ method public void onLoadTraceFailure(androidx.benchmark.traceprocessor.PerfettoTrace trace, Throwable throwable);
+ }
+
+ public static final class TraceProcessor.EventCallback.Noop implements androidx.benchmark.traceprocessor.TraceProcessor.EventCallback {
+ method public void onLoadTraceFailure(androidx.benchmark.traceprocessor.PerfettoTrace trace, Throwable throwable);
+ field public static final androidx.benchmark.traceprocessor.TraceProcessor.EventCallback.Noop INSTANCE;
+ }
+
+ public static final class TraceProcessor.Session {
+ method public kotlin.sequences.Sequence<androidx.benchmark.traceprocessor.Row> query(String query);
+ method public String queryMetricsJson(java.util.List<java.lang.String> metrics);
+ method public byte[] queryMetricsProtoBinary(java.util.List<java.lang.String> metrics);
+ method public String queryMetricsProtoText(java.util.List<java.lang.String> metrics);
+ method public byte[] rawQuery(String query);
+ }
+
+ public static class TraceProcessor.Tracer {
+ ctor public TraceProcessor.Tracer();
+ method public void beginTraceSection(String label);
+ method public void endTraceSection();
+ method public final inline <T> T trace(String label, kotlin.jvm.functions.Function0<? extends T> block);
+ }
+
+ public final class TraceProcessorKt {
+ method public static String processNameLikePkg(String pkg);
+ }
+
+}
+
diff --git a/camera/camera-effects-still-portrait/api/res-current.txt b/benchmark/benchmark-traceprocessor/api/res-current.txt
similarity index 100%
rename from camera/camera-effects-still-portrait/api/res-current.txt
rename to benchmark/benchmark-traceprocessor/api/res-current.txt
diff --git a/benchmark/benchmark-traceprocessor/api/restricted_current.txt b/benchmark/benchmark-traceprocessor/api/restricted_current.txt
new file mode 100644
index 0000000..71edcdb
--- /dev/null
+++ b/benchmark/benchmark-traceprocessor/api/restricted_current.txt
@@ -0,0 +1,77 @@
+// Signature format: 4.0
+package androidx.benchmark.traceprocessor {
+
+ @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface ExperimentalTraceProcessorApi {
+ }
+
+ public final class PerfettoTrace {
+ ctor public PerfettoTrace(String path);
+ field public static final androidx.benchmark.traceprocessor.PerfettoTrace.Companion Companion;
+ }
+
+ public static final class PerfettoTrace.Companion {
+ }
+
+ public final class Row implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<java.lang.String,java.lang.Object?> {
+ ctor public Row(java.util.Map<java.lang.String,? extends java.lang.Object?> map);
+ method public byte[] bytes(String columnName);
+ method public double double(String columnName);
+ method public long long(String columnName);
+ method public byte[]? nullableBytes(String columnName);
+ method public Double? nullableDouble(String columnName);
+ method public Long? nullableLong(String columnName);
+ method public String? nullableString(String columnName);
+ method public String string(String columnName);
+ }
+
+ public final class RowKt {
+ method public static androidx.benchmark.traceprocessor.Row rowOf(kotlin.Pair<java.lang.String,? extends java.lang.Object?>... pairs);
+ }
+
+ @SuppressCompatibility @androidx.benchmark.traceprocessor.ExperimentalTraceProcessorApi public interface ServerLifecycleManager {
+ method public int start();
+ method public void stop();
+ method public default String timeoutMessage();
+ }
+
+ public final class TraceProcessor {
+ ctor @SuppressCompatibility @androidx.benchmark.traceprocessor.ExperimentalTraceProcessorApi public TraceProcessor(androidx.benchmark.traceprocessor.ServerLifecycleManager serverLifecycleManager, optional androidx.benchmark.traceprocessor.TraceProcessor.Tracer tracer, optional androidx.benchmark.traceprocessor.TraceProcessor.EventCallback eventCallback);
+ method public <T> T loadTrace(androidx.benchmark.traceprocessor.PerfettoTrace trace, kotlin.jvm.functions.Function1<? super androidx.benchmark.traceprocessor.TraceProcessor.Session,? extends T> block);
+ method @SuppressCompatibility @androidx.benchmark.traceprocessor.ExperimentalTraceProcessorApi public static <T> T runServer(androidx.benchmark.traceprocessor.ServerLifecycleManager serverLifecycleManager, androidx.benchmark.traceprocessor.TraceProcessor.EventCallback eventCallback, androidx.benchmark.traceprocessor.TraceProcessor.Tracer tracer, optional long timeout, kotlin.jvm.functions.Function1<? super androidx.benchmark.traceprocessor.TraceProcessor,? extends T> block);
+ field public static final androidx.benchmark.traceprocessor.TraceProcessor.Companion Companion;
+ }
+
+ public static final class TraceProcessor.Companion {
+ method @SuppressCompatibility @androidx.benchmark.traceprocessor.ExperimentalTraceProcessorApi public <T> T runServer(androidx.benchmark.traceprocessor.ServerLifecycleManager serverLifecycleManager, androidx.benchmark.traceprocessor.TraceProcessor.EventCallback eventCallback, androidx.benchmark.traceprocessor.TraceProcessor.Tracer tracer, optional long timeout, kotlin.jvm.functions.Function1<? super androidx.benchmark.traceprocessor.TraceProcessor,? extends T> block);
+ }
+
+ public static interface TraceProcessor.EventCallback {
+ method public void onLoadTraceFailure(androidx.benchmark.traceprocessor.PerfettoTrace trace, Throwable throwable);
+ }
+
+ public static final class TraceProcessor.EventCallback.Noop implements androidx.benchmark.traceprocessor.TraceProcessor.EventCallback {
+ method public void onLoadTraceFailure(androidx.benchmark.traceprocessor.PerfettoTrace trace, Throwable throwable);
+ field public static final androidx.benchmark.traceprocessor.TraceProcessor.EventCallback.Noop INSTANCE;
+ }
+
+ public static final class TraceProcessor.Session {
+ method public kotlin.sequences.Sequence<androidx.benchmark.traceprocessor.Row> query(String query);
+ method public String queryMetricsJson(java.util.List<java.lang.String> metrics);
+ method public byte[] queryMetricsProtoBinary(java.util.List<java.lang.String> metrics);
+ method public String queryMetricsProtoText(java.util.List<java.lang.String> metrics);
+ method public byte[] rawQuery(String query);
+ }
+
+ public static class TraceProcessor.Tracer {
+ ctor public TraceProcessor.Tracer();
+ method public void beginTraceSection(String label);
+ method public void endTraceSection();
+ method public final inline <T> T trace(String label, kotlin.jvm.functions.Function0<? extends T> block);
+ }
+
+ public final class TraceProcessorKt {
+ method public static String processNameLikePkg(String pkg);
+ }
+
+}
+
diff --git a/benchmark/benchmark-traceprocessor/build.gradle b/benchmark/benchmark-traceprocessor/build.gradle
new file mode 100644
index 0000000..70c9024
--- /dev/null
+++ b/benchmark/benchmark-traceprocessor/build.gradle
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+import androidx.build.AndroidXConfig
+import androidx.build.KotlinTarget
+import androidx.build.LibraryType
+import androidx.build.PlatformIdentifier
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+plugins {
+ id("AndroidXPlugin")
+ id("com.squareup.wire")
+}
+
+androidXMultiplatform {
+ jvm()
+ androidLibrary {
+ namespace = "androidx.benchmark.traceprocessor"
+ withAndroidTestOnDeviceBuilder {
+ it.compilationName = "instrumentedTest"
+ it.defaultSourceSetName = "androidInstrumentedTest"
+ it.sourceSetTreeName = "test"
+ }
+ }
+
+ defaultPlatform(PlatformIdentifier.JVM)
+
+ sourceSets {
+ commonMain {
+ dependencies {
+ api(libs.kotlinStdlib)
+ api("androidx.annotation:annotation:1.8.1")
+ implementation(libs.wireRuntime)
+ }
+ }
+ androidMain {
+ dependsOn(commonMain)
+ }
+ }
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ // Enable using experimental APIs from within same version group
+ freeCompilerArgs += [
+ "-opt-in=androidx.benchmark.traceprocessor.ExperimentalTraceProcessorApi",
+ ]
+ }
+}
+
+// Workarounds for Wire's plugin not setting code generation directory as task output correctly
+// See https://github.com/square/wire/issues/3199
+tasks.named("multiplatformSourceJar").configure {
+ dependsOn(tasks.named("generateCommonMainProtos"))
+}
+afterEvaluate {
+ tasks.named("generateJavaKzip").configure {
+ dependsOn(tasks.named("generateCommonMainProtos"))
+ }
+}
+
+wire {
+ kotlin {}
+ sourcePath {
+ srcDir AndroidXConfig.getPrebuiltsRoot(project).absolutePath + '/androidx/traceprocessor'
+
+ // currently, all protos are at same tree depth
+ // can add further includes if this stops working
+ include 'protos/perfetto/*/*.proto'
+ }
+
+ prune 'perfetto.protos.AndroidBatteryMetric'
+ prune 'perfetto.protos.AndroidBinderMetric'
+ prune 'perfetto.protos.AndroidCameraMetric'
+ prune 'perfetto.protos.AndroidCameraUnaggregatedMetric'
+ prune 'perfetto.protos.AndroidCpuMetric'
+ prune 'perfetto.protos.AndroidDisplayMetrics'
+ prune 'perfetto.protos.AndroidDmaHeapMetric'
+ prune 'perfetto.protos.AndroidDvfsMetric'
+ prune 'perfetto.protos.AndroidFastrpcMetric'
+ prune 'perfetto.protos.AndroidFrameTimelineMetric'
+ prune 'perfetto.protos.AndroidGpuMetric'
+ prune 'perfetto.protos.AndroidHwcomposerMetrics'
+ prune 'perfetto.protos.AndroidHwuiMetric'
+ prune 'perfetto.protos.AndroidIonMetric'
+ prune 'perfetto.protos.AndroidIrqRuntimeMetric'
+ prune 'perfetto.protos.AndroidJankCujMetric'
+ prune 'perfetto.protos.AndroidLmkMetric'
+ prune 'perfetto.protos.AndroidLmkReasonMetric'
+ prune 'perfetto.protos.AndroidMemoryMetric'
+ prune 'perfetto.protos.AndroidMemoryUnaggregatedMetric'
+ prune 'perfetto.protos.AndroidMultiuserMetric'
+ prune 'perfetto.protos.AndroidNetworkMetric'
+ prune 'perfetto.protos.AndroidPackageList'
+ prune 'perfetto.protos.AndroidPowerRails'
+ prune 'perfetto.protos.AndroidProcessMetadata'
+ prune 'perfetto.protos.AndroidRtRuntimeMetric'
+ prune 'perfetto.protos.AndroidSimpleperfMetric'
+ prune 'perfetto.protos.AndroidSurfaceflingerMetric'
+ prune 'perfetto.protos.AndroidTaskNames'
+ prune 'perfetto.protos.AndroidTraceQualityMetric'
+ prune 'perfetto.protos.G2dMetrics'
+ prune 'perfetto.protos.JavaHeapHistogram'
+ prune 'perfetto.protos.JavaHeapStats'
+ prune 'perfetto.protos.ProcessRenderInfo'
+ prune 'perfetto.protos.ProfilerSmaps'
+ prune 'perfetto.protos.TraceAnalysisStats'
+ prune 'perfetto.protos.TraceMetadata'
+ prune 'perfetto.protos.UnsymbolizedFrames'
+}
+
+androidx {
+ name = "Benchmark TraceProcessor"
+ type = LibraryType.PUBLISHED_LIBRARY
+ inceptionYear = "2024"
+ description = "AndroidX Benchmark TraceProcessor"
+ metalavaK2UastEnabled = false
+ kotlinTarget = KotlinTarget.KOTLIN_1_9
+}
\ No newline at end of file
diff --git a/benchmark/benchmark-traceprocessor/src/androidMain/kotlin/androidx/benchmark/traceprocessor/Log.android.kt b/benchmark/benchmark-traceprocessor/src/androidMain/kotlin/androidx/benchmark/traceprocessor/Log.android.kt
new file mode 100644
index 0000000..ff8c711
--- /dev/null
+++ b/benchmark/benchmark-traceprocessor/src/androidMain/kotlin/androidx/benchmark/traceprocessor/Log.android.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.benchmark.traceprocessor
+
+import android.util.Log
+
+internal actual fun log(string: String) {
+ Log.d("Benchmark", string)
+}
diff --git a/benchmark/benchmark-traceprocessor/src/androidMain/kotlin/perfetto/protos/package-info.java b/benchmark/benchmark-traceprocessor/src/androidMain/kotlin/perfetto/protos/package-info.java
new file mode 100644
index 0000000..02661a3
--- /dev/null
+++ b/benchmark/benchmark-traceprocessor/src/androidMain/kotlin/perfetto/protos/package-info.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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.
+ */
+
+/**
+ * Hide the perfetto.protos package, as it's an implementation detail of benchmark
+ *
+ * Note: To work around b/382741383, this file is present in both jvmMain/ and androidMain/
+ *
+ * Note: other attempts to use these protos in a benchmark process may clash with our
+ * definitions. If this becomes an issue, we can move ours to a separate, internal package.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package perfetto.protos;
+
+import androidx.annotation.RestrictTo;
diff --git a/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/ExperimentalTraceProcessorApi.kt b/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/ExperimentalTraceProcessorApi.kt
new file mode 100644
index 0000000..abc60e3
--- /dev/null
+++ b/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/ExperimentalTraceProcessorApi.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.benchmark.traceprocessor
+
+/** Annotation indicating experimental API for initializing Trace Processor. */
+@RequiresOptIn
+@Retention(AnnotationRetention.BINARY)
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR)
+public annotation class ExperimentalTraceProcessorApi
diff --git a/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/Log.kt b/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/Log.kt
new file mode 100644
index 0000000..ec70912
--- /dev/null
+++ b/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/Log.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.benchmark.traceprocessor
+
+internal expect fun log(string: String)
diff --git a/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/PerfettoTrace.kt b/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/PerfettoTrace.kt
new file mode 100644
index 0000000..fbd814f
--- /dev/null
+++ b/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/PerfettoTrace.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.benchmark.traceprocessor
+
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+
+public class PerfettoTrace(
+ /**
+ * Absolute file path of the trace.
+ *
+ * Note that the trace is not guaranteed to be placed into an app-accessible directory, and may
+ * require shell commands to access.
+ */
+ @get:RestrictTo(LIBRARY_GROUP) public val path: String
+) {
+ // this companion object exists to enable PerfettoTrace.Companion.record to be declared
+ public companion object
+}
diff --git a/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/QueryResultIterator.kt b/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/QueryResultIterator.kt
new file mode 100644
index 0000000..6ea6a13
--- /dev/null
+++ b/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/QueryResultIterator.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.benchmark.traceprocessor
+
+import androidx.annotation.RestrictTo
+import perfetto.protos.QueryResult
+
+/** Iterator for results from a [TraceProcessor] query. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class QueryResultIterator constructor(queryResult: QueryResult) : Iterator<Row> {
+ private val dataLists =
+ object {
+ val stringBatches = mutableListOf<String>()
+ val varIntBatches = mutableListOf<Long>()
+ val float64Batches = mutableListOf<Double>()
+ val blobBatches = mutableListOf<ByteArray>()
+
+ var stringIndex = 0
+ var varIntIndex = 0
+ var float64Index = 0
+ var blobIndex = 0
+ }
+
+ private val cells = mutableListOf<QueryResult.CellsBatch.CellType>()
+ private val columnNames = queryResult.column_names
+ private val columnCount: Int
+ private val count: Int
+
+ private var currentIndex = 0
+
+ init {
+ // Parsing every batch
+ for (batch in queryResult.batch) {
+ val stringsBatch = batch.string_cells!!.split(0x00.toChar()).dropLast(1)
+ dataLists.stringBatches.addAll(stringsBatch)
+ dataLists.varIntBatches.addAll(batch.varint_cells)
+ dataLists.float64Batches.addAll(batch.float64_cells)
+ dataLists.blobBatches.addAll(batch.blob_cells.map { it.toByteArray() })
+ cells.addAll(batch.cells)
+ }
+
+ columnCount = columnNames.size
+ count = if (columnCount > 0) cells.size / columnCount else 0
+ }
+
+ /** Returns the number of rows in the query result. */
+ public fun size(): Int {
+ return count
+ }
+
+ /** Returns true whether there are no results stored in this iterator, false otherwise. */
+ public fun isEmpty(): Boolean {
+ return count == 0
+ }
+
+ /** Returns true if there are more rows not yet parsed from the query result. */
+ override fun hasNext(): Boolean {
+ return currentIndex < count
+ }
+
+ /**
+ * Returns a map containing the next row of the query results.
+ *
+ * @throws IllegalArgumentException if the query returns an invalid cell type
+ * @throws NoSuchElementException if the query has no next row.
+ */
+ override fun next(): Row {
+ // Parsing logic is copied from the python project:
+ // https://github.com/google/perfetto/blob/master/python/perfetto/trace_processor/api.py#L89
+
+ if (!hasNext()) throw NoSuchElementException()
+
+ val row = mutableMapOf<String, Any?>()
+ val baseCellIndex = currentIndex * columnCount
+
+ for ((num, columnName) in columnNames.withIndex()) {
+ val colType = cells[baseCellIndex + num]
+ val colIndex: Int
+ row[columnName] =
+ when (colType) {
+ QueryResult.CellsBatch.CellType.CELL_STRING -> {
+ colIndex = dataLists.stringIndex
+ dataLists.stringIndex += 1
+ dataLists.stringBatches[colIndex]
+ }
+ QueryResult.CellsBatch.CellType.CELL_VARINT -> {
+ colIndex = dataLists.varIntIndex
+ dataLists.varIntIndex += 1
+ dataLists.varIntBatches[colIndex]
+ }
+ QueryResult.CellsBatch.CellType.CELL_FLOAT64 -> {
+ colIndex = dataLists.float64Index
+ dataLists.float64Index += 1
+ dataLists.float64Batches[colIndex]
+ }
+ QueryResult.CellsBatch.CellType.CELL_BLOB -> {
+ colIndex = dataLists.blobIndex
+ dataLists.blobIndex += 1
+ dataLists.blobBatches[colIndex]
+ }
+ QueryResult.CellsBatch.CellType.CELL_INVALID ->
+ throw IllegalArgumentException("Invalid cell type")
+ QueryResult.CellsBatch.CellType.CELL_NULL -> null
+ }
+ }
+
+ currentIndex += 1
+ return Row(row)
+ }
+}
diff --git a/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/Row.kt b/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/Row.kt
new file mode 100644
index 0000000..79895cd
--- /dev/null
+++ b/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/Row.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.benchmark.traceprocessor
+
+/**
+ * Convenience for constructing a RowResult for given column values.
+ *
+ * Useful when asserting expected query results in tests.
+ */
+public fun rowOf(vararg pairs: Pair<String, Any?>): Row {
+ return Row(pairs.toMap())
+}
+
+/**
+ * A Map<String, Any?> that maps column name to value in a row result from a [QueryResultIterator].
+ *
+ * Provides convenience methods for converting to internal base types - `String`, `Long`, `Double`,
+ * and `ByteArray`.
+ *
+ * ```
+ * session.query("SELECT name,ts,dur FROM slice WHERE name LIKE \"activityStart\"").forEach {
+ * callback(it.string("name"), it.long("ts"), it.long("dur")
+ * // or, used as a map:
+ * //callback(it["name"] as String, it["ts"] as Long, it["dur"] as Long)
+ * }
+ * ```
+ *
+ * Nullable variants of each convenience method are also provided.
+ */
+public class Row(private val map: Map<String, Any?>) : Map<String, Any?> by map {
+ public fun string(columnName: String): String = map[columnName] as String
+
+ public fun long(columnName: String): Long = map[columnName] as Long
+
+ public fun double(columnName: String): Double = map[columnName] as Double
+
+ public fun bytes(columnName: String): ByteArray = map[columnName] as ByteArray
+
+ public fun nullableString(columnName: String): String? = map[columnName] as String?
+
+ @Suppress("AutoBoxing") // primitives are already internally boxed
+ public fun nullableLong(columnName: String): Long? = map[columnName] as Long?
+
+ @Suppress("AutoBoxing") // primitives are already internally boxed
+ public fun nullableDouble(columnName: String): Double? = map[columnName] as Double?
+
+ public fun nullableBytes(columnName: String): ByteArray? = map[columnName] as ByteArray?
+
+ override fun hashCode(): Int = map.hashCode()
+
+ override fun toString(): String = map.toString()
+
+ override fun equals(other: Any?): Boolean = map == other
+}
diff --git a/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/ServerLifecycleManager.kt b/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/ServerLifecycleManager.kt
new file mode 100644
index 0000000..db2a9ac
--- /dev/null
+++ b/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/ServerLifecycleManager.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.benchmark.traceprocessor
+
+@ExperimentalTraceProcessorApi
+@Suppress("NotCloseable")
+public interface ServerLifecycleManager {
+ /**
+ * Called to Start an instance of Trace Processor
+ *
+ * @return the port to use to communicate to Trace Processor
+ */
+ public fun start(): Int
+
+ /**
+ * Called to construct a more detailed failure message when [TraceProcessor] cannot be connected
+ * to.
+ */
+ public fun timeoutMessage(): String = "Unable to start perfetto process"
+
+ /** Called to stop the running instance of Trace Processor. */
+ public fun stop()
+}
diff --git a/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/Slice.kt b/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/Slice.kt
new file mode 100644
index 0000000..37a68bf
--- /dev/null
+++ b/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/Slice.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.benchmark.traceprocessor
+
+import androidx.annotation.RestrictTo
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public data class Slice(val name: String, val ts: Long, val dur: Long) {
+ val endTs: Long = ts + dur
+
+ val frameId: Int?
+
+ init {
+ val firstSpaceIndex = name.indexOf(" ")
+ frameId =
+ if (firstSpaceIndex >= 0) {
+ // if see a space, check for id from end of first space to next space (or end of
+ // String)
+ val secondSpaceIndex = name.indexOf(" ", firstSpaceIndex + 1)
+ val endFrameIdIndex = if (secondSpaceIndex < 0) name.length else secondSpaceIndex
+ name.substring(firstSpaceIndex + 1, endFrameIdIndex).toIntOrNull()
+ } else {
+ null
+ }
+ }
+
+ public fun contains(targetTs: Long): Boolean {
+ return targetTs >= ts && targetTs <= (ts + dur)
+ }
+}
+
+/**
+ * Convenient function to immediately retrieve a list of slices. Note that this method is provided
+ * for convenience.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public fun Sequence<Row>.toSlices(): List<Slice> =
+ map { Slice(name = it.string("name"), ts = it.long("ts"), dur = it.long("dur")) }.toList()
diff --git a/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/TraceProcessor.kt b/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/TraceProcessor.kt
new file mode 100644
index 0000000..ac40112
--- /dev/null
+++ b/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/TraceProcessor.kt
@@ -0,0 +1,476 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.benchmark.traceprocessor
+
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+import java.io.File
+import java.io.FileInputStream
+import java.io.InputStream
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import perfetto.protos.ComputeMetricArgs
+import perfetto.protos.ComputeMetricResult
+import perfetto.protos.QueryResult
+import perfetto.protos.TraceMetrics
+
+/**
+ * Kotlin API for [Perfetto Trace Processor](https://perfetto.dev/docs/analysis/trace-processor),
+ * which enables SQL querying against the data stored in a Perfetto trace.
+ *
+ * This includes synchronous and async trace sections, kernel-level scheduling timing, binder
+ * events... If it's displayed in Android Studio system trace or
+ * [ui.perfetto.dev](https://ui.perfetto.dev), it can be queried from this API.
+ *
+ * ```
+ * // Collect the duration of all slices named "activityStart" in the trace
+ * val activityStartDurationNs = TraceProcessor.runServer {
+ * loadTrace(trace) {
+ * query("SELECT dur FROM slice WHERE name LIKE \"activityStart\"").toList {
+ * it.long("dur")
+ * }
+ * }
+ * }
+ * ```
+ *
+ * Note that traces generally hold events from multiple apps, services and processes, so it's
+ * recommended to filter potentially common trace events to the process you're interested in. See
+ * the following example which queries `Choreographer#doFrame` slices (labelled spans of time) only
+ * for a given package name:
+ * ```
+ * query("""
+ * |SELECT
+ * | slice.name,slice.ts,slice.dur
+ * |FROM slice
+ * | INNER JOIN thread_track on slice.track_id = thread_track.id
+ * | INNER JOIN thread USING(utid)
+ * | INNER JOIN process USING(upid)
+ * |WHERE
+ * | slice.name LIKE "Choreographer#doFrame%" AND
+ * | process.name LIKE "$packageName"
+ * """.trimMargin()
+ * )
+ * ```
+ *
+ * See also Perfetto project documentation:
+ * * [Trace Processor overview](https://perfetto.dev/docs/analysis/trace-processor)
+ * * [Common queries](https://perfetto.dev/docs/analysis/common-queries)
+ *
+ * @see PerfettoTrace
+ */
+public class TraceProcessor
+@Suppress("ExecutorRegistration") // purely synchronous
+@ExperimentalTraceProcessorApi
+constructor(
+ private val serverLifecycleManager: ServerLifecycleManager,
+ private val tracer: Tracer = Tracer(),
+ private val eventCallback: EventCallback = EventCallback.Noop,
+) {
+ public open class Tracer {
+ public open fun beginTraceSection(label: String) {}
+
+ public open fun endTraceSection() {}
+
+ public inline fun <T> trace(label: String, block: () -> T): T {
+ beginTraceSection(label)
+ return try {
+ block()
+ } finally {
+ endTraceSection()
+ }
+ }
+ }
+
+ public interface EventCallback {
+ public fun onLoadTraceFailure(trace: PerfettoTrace, throwable: Throwable)
+
+ public object Noop : EventCallback {
+ override fun onLoadTraceFailure(trace: PerfettoTrace, throwable: Throwable) {}
+ }
+ }
+
+ public companion object {
+ private val SERVER_START_TIMEOUT_MS = 60.seconds
+
+ /**
+ * Starts a Perfetto trace processor shell server in http mode, loads a trace and executes
+ * the given block.
+ *
+ * @param serverLifecycleManager controls starting and stopping the TraceProcessor process.
+ * @param eventCallback callback for events such as trace load failure.
+ * @param tracer used to trace begin and end of significant events within this managed run.
+ * @param timeout waiting for the server to start. If less or equal to zero use 60 seconds
+ * @param block Command to execute using trace processor
+ */
+ @Suppress(
+ "ExecutorRegistration", // purely synchronous
+ "MissingJvmstatic", // JvmOverload doesn't handle mangling
+ )
+ @ExperimentalTraceProcessorApi
+ @JvmStatic
+ public fun <T> runServer(
+ serverLifecycleManager: ServerLifecycleManager,
+ eventCallback: EventCallback,
+ @Suppress("ListenerLast") tracer: Tracer,
+ @Suppress("ListenerLast") timeout: Duration = SERVER_START_TIMEOUT_MS,
+ @Suppress("ListenerLast") block: TraceProcessor.() -> T
+ ): T =
+ tracer.trace("TraceProcessor#runServer") {
+ var actualTimeout = timeout
+ if (actualTimeout <= Duration.ZERO) {
+ actualTimeout = SERVER_START_TIMEOUT_MS
+ }
+
+ var traceProcessor: TraceProcessor? = null
+ try {
+
+ // Initializes the server process
+ traceProcessor =
+ TraceProcessor(
+ eventCallback = eventCallback,
+ serverLifecycleManager = serverLifecycleManager
+ )
+ .startServer(actualTimeout)
+
+ // Executes the query block
+ return@trace tracer.trace("TraceProcessor#runServer#block") {
+ block(traceProcessor)
+ }
+ } finally {
+ traceProcessor?.stopServer()
+ }
+ }
+ }
+
+ /** Loads a PerfettoTrace into the trace processor server to query data out of the trace. */
+ public fun <T> loadTrace(trace: PerfettoTrace, block: Session.() -> T): T {
+ loadTraceImpl(trace.path)
+ // TODO: unload trace after block
+ try {
+ return block.invoke(Session(this))
+ } catch (t: Throwable) {
+ eventCallback.onLoadTraceFailure(trace, t)
+ throw t
+ }
+ }
+
+ /**
+ * Handle to query sql data from a [PerfettoTrace].
+ *
+ * @see query
+ */
+ public class Session internal constructor(private val traceProcessor: TraceProcessor) {
+ /** Computes the given metric on the previously loaded trace. */
+ @RestrictTo(LIBRARY_GROUP) // avoids exposing Proto API
+ public fun getTraceMetrics(metric: String): TraceMetrics {
+ val computeResult =
+ queryAndVerifyMetricResult(
+ listOf(metric),
+ ComputeMetricArgs.ResultFormat.BINARY_PROTOBUF
+ )
+ return TraceMetrics.ADAPTER.decode(computeResult.metrics!!)
+ }
+
+ /**
+ * Computes the given metrics, returning the results as a binary proto.
+ *
+ * The proto format definition for decoding this binary format can be found
+ * [here](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/protos/perfetto/metrics/).
+ *
+ * See
+ * [perfetto metric docs](https://perfetto.dev/docs/quickstart/trace-analysis#trace-based-metrics)
+ * for an overview on trace based metrics.
+ */
+ public fun queryMetricsProtoBinary(metrics: List<String>): ByteArray {
+ val computeResult =
+ queryAndVerifyMetricResult(metrics, ComputeMetricArgs.ResultFormat.BINARY_PROTOBUF)
+ return computeResult.metrics!!.toByteArray()
+ }
+
+ /**
+ * Computes the given metrics, returning the results as JSON text.
+ *
+ * The proto format definition for these metrics can be found
+ * [here](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/protos/perfetto/metrics/).
+ *
+ * See
+ * [perfetto metric docs](https://perfetto.dev/docs/quickstart/trace-analysis#trace-based-metrics)
+ * for an overview on trace based metrics.
+ */
+ public fun queryMetricsJson(metrics: List<String>): String {
+ val computeResult =
+ queryAndVerifyMetricResult(metrics, ComputeMetricArgs.ResultFormat.JSON)
+ check(computeResult.metrics_as_json != null)
+ return computeResult.metrics_as_json
+ }
+
+ /**
+ * Computes the given metrics, returning the result as proto text.
+ *
+ * The proto format definition for these metrics can be found
+ * [here](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/protos/perfetto/metrics/).
+ *
+ * See
+ * [perfetto metric docs](https://perfetto.dev/docs/quickstart/trace-analysis#trace-based-metrics)
+ * for an overview on trace based metrics.
+ */
+ public fun queryMetricsProtoText(metrics: List<String>): String {
+ val computeResult =
+ queryAndVerifyMetricResult(metrics, ComputeMetricArgs.ResultFormat.TEXTPROTO)
+ check(computeResult.metrics_as_prototext != null)
+ return computeResult.metrics_as_prototext
+ }
+
+ private fun queryAndVerifyMetricResult(
+ metrics: List<String>,
+ format: ComputeMetricArgs.ResultFormat
+ ): ComputeMetricResult {
+ val nameString = metrics.joinToString()
+ require(metrics.none { it.contains(" ") }) {
+ "Metrics must not constain spaces, metrics: $nameString"
+ }
+
+ traceProcessor.tracer.trace("TraceProcessor#getTraceMetrics $nameString") {
+ require(traceProcessor.traceProcessorHttpServer.isRunning()) {
+ "Perfetto trace_shell_process is not running."
+ }
+
+ // Compute metrics
+ val computeResult =
+ traceProcessor.traceProcessorHttpServer.computeMetric(metrics, format)
+ if (computeResult.error != null) {
+ throw IllegalStateException(computeResult.error)
+ }
+
+ return computeResult
+ }
+ }
+
+ /**
+ * Computes the given query on the currently loaded trace.
+ *
+ * Each row returned by a query is returned by the `Sequence` as a [Row]. To extract data
+ * from a `Row`, query by column name. The following example does this for name, timestamp,
+ * and duration of slices:
+ * ```
+ * // Runs the provided callback on each activityStart instance in the trace,
+ * // providing name, start timestamp (in ns) and duration (in ns)
+ * fun TraceProcessor.Session.forEachActivityStart(callback: (String, Long, Long) -> Unit) {
+ * query("SELECT name,ts,dur FROM slice WHERE name LIKE \"activityStart\"").forEach {
+ * callback(it.string("name"), it.long("ts"), it.long("dur")
+ * // or, used as a map:
+ * //callback(it["name"] as String, it["ts"] as Long, it["dur"] as Long)
+ * }
+ * }
+ * ```
+ *
+ * @see TraceProcessor
+ * @see TraceProcessor.Session
+ */
+ public fun query(query: String): Sequence<Row> {
+ traceProcessor.tracer.trace("TraceProcessor#query $query") {
+ require(traceProcessor.traceProcessorHttpServer.isRunning()) {
+ "Perfetto trace_shell_process is not running."
+ }
+ val queryResult =
+ traceProcessor.traceProcessorHttpServer.rawQuery(query) {
+ // Note: check for errors as part of decode, so it's immediate
+ // instead of lazily in QueryResultIterator
+ QueryResult.decodeAndCheckError(query, it)
+ }
+ return Sequence { QueryResultIterator(queryResult) }
+ }
+ }
+
+ private fun QueryResult.Companion.decodeAndCheckError(
+ query: String,
+ inputStream: InputStream
+ ) =
+ ADAPTER.decode(inputStream).also {
+ check(it.error == null) {
+ throw IllegalStateException("Error with query: --$query--, error=${it.error}")
+ }
+ }
+
+ /**
+ * Computes the given query on the currently loaded trace, returning the resulting protobuf
+ * bytes as a [ByteArray].
+ *
+ * Use [Session.query] if you do not wish to parse the Proto result yourself.
+ *
+ * The `QueryResult` protobuf definition can be found
+ * [in the Perfetto project](https://github.com/google/perfetto/blob/master/protos/perfetto/trace_processor/trace_processor.proto),
+ * which can be used to decode the result returned here with a protobuf parsing library.
+ *
+ * Note that this method does not check for errors in the protobuf, that is the caller's
+ * responsibility.
+ *
+ * @see Session.query
+ */
+ public fun rawQuery(query: String): ByteArray {
+ traceProcessor.tracer.trace("TraceProcessor#query $query") {
+ require(traceProcessor.traceProcessorHttpServer.isRunning()) {
+ "Perfetto trace_shell_process is not running."
+ }
+ return traceProcessor.traceProcessorHttpServer.rawQuery(query) { it.readBytes() }
+ }
+ }
+
+ /**
+ * Query a trace for a list of slices - name, timestamp, and duration.
+ *
+ * Note that sliceNames may include wildcard matches, such as `foo%`
+ */
+ @RestrictTo(LIBRARY_GROUP) // Slice API not currently exposed, since it doesn't track table
+ public fun querySlices(
+ vararg sliceNames: String,
+ packageName: String?,
+ ): List<Slice> {
+ require(traceProcessor.traceProcessorHttpServer.isRunning()) {
+ "Perfetto trace_shell_process is not running."
+ }
+
+ val whereClause =
+ sliceNames.joinToString(
+ separator = " OR ",
+ prefix =
+ if (packageName == null) {
+ "("
+ } else {
+ processNameLikePkg(packageName) + " AND ("
+ },
+ postfix = ")"
+ ) {
+ "slice_name LIKE \"$it\""
+ }
+ val innerJoins =
+ if (packageName != null) {
+ """
+ INNER JOIN thread_track ON slice.track_id = thread_track.id
+ INNER JOIN thread USING(utid)
+ INNER JOIN process USING(upid)
+ """
+ .trimMargin()
+ } else {
+ ""
+ }
+
+ val processTrackInnerJoins =
+ """
+ INNER JOIN process_track ON slice.track_id = process_track.id
+ INNER JOIN process USING(upid)
+ """
+ .trimIndent()
+
+ return query(
+ query =
+ """
+ SELECT slice.name AS slice_name,ts,dur
+ FROM slice
+ $innerJoins
+ WHERE $whereClause
+ UNION
+ SELECT process_track.name AS slice_name,ts,dur
+ FROM slice
+ $processTrackInnerJoins
+ WHERE $whereClause
+ ORDER BY ts
+ """
+ .trimIndent()
+ )
+ .map { row ->
+ // Using an explicit mapper here to account for the aliasing of `slice_name`
+ Slice(
+ name = row.string("slice_name"),
+ ts = row.long("ts"),
+ dur = row.long("dur")
+ )
+ }
+ .filter { it.dur != -1L } // filter out non-terminating slices
+ .toList()
+ }
+ }
+
+ @OptIn(ExperimentalTraceProcessorApi::class)
+ private val traceProcessorHttpServer: TraceProcessorHttpServer =
+ TraceProcessorHttpServer(serverLifecycleManager)
+ private var traceLoaded = false
+
+ private fun startServer(timeout: Duration): TraceProcessor =
+ tracer.trace("TraceProcessor#startServer") {
+ println("startserver($timeout)")
+ traceProcessorHttpServer.startServer(timeout)
+ return@trace this
+ }
+
+ private fun stopServer() =
+ tracer.trace("TraceProcessor#stopServer") {
+ println("stopserver")
+ traceProcessorHttpServer.stopServer()
+ }
+
+ /**
+ * Loads a trace in the current instance of the trace processor, clearing any previous loaded
+ * trace if existing.
+ */
+ private fun loadTraceImpl(absoluteTracePath: String) {
+ tracer.trace("TraceProcessor#loadTraceImpl") {
+ require(!absoluteTracePath.contains(" ")) {
+ "Trace path must not contain spaces: $absoluteTracePath"
+ }
+
+ val traceFile = File(absoluteTracePath)
+ require(traceFile.exists() && traceFile.isFile) {
+ "Trace path must exist and not be a directory: $absoluteTracePath"
+ }
+
+ // In case a previous trace was loaded, ensures to clear
+ if (traceLoaded) {
+ clearTrace()
+ }
+
+ val parseResults = traceProcessorHttpServer.parse(FileInputStream(traceFile))
+ parseResults.forEach { if (it.error != null) throw IllegalStateException(it.error) }
+
+ // Notifies the server that it won't receive any more trace parts
+ traceProcessorHttpServer.notifyEof()
+
+ traceLoaded = true
+ }
+ }
+
+ /** Clears the current loaded trace. */
+ private fun clearTrace() =
+ tracer.trace("TraceProcessor#clearTrace") {
+ traceProcessorHttpServer.restoreInitialTables()
+ traceLoaded = false
+ }
+}
+
+/** Helper for fuzzy matching process name to package */
+public fun processNameLikePkg(pkg: String): String {
+ // check for truncated package names, which can sometimes occur if perfetto can't capture full
+ // names, and only has 16 bytes from sched info (which results in 15 chars due to null
+ // termination)
+ val truncated =
+ if (pkg.length > 15) {
+ " OR process.name LIKE \"${pkg.takeLast(15)}\""
+ } else {
+ ""
+ }
+ return """(process.name LIKE "$pkg" OR process.name LIKE "$pkg:%"$truncated)"""
+}
diff --git a/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/TraceProcessorHttpServer.kt b/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/TraceProcessorHttpServer.kt
new file mode 100644
index 0000000..f586b2c
--- /dev/null
+++ b/benchmark/benchmark-traceprocessor/src/commonMain/kotlin/androidx/benchmark/traceprocessor/TraceProcessorHttpServer.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.benchmark.traceprocessor
+
+import java.io.FileNotFoundException
+import java.io.InputStream
+import java.io.OutputStream
+import java.net.ConnectException
+import java.net.HttpURLConnection
+import java.net.URL
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.DurationUnit
+import perfetto.protos.AppendTraceDataResult
+import perfetto.protos.ComputeMetricArgs
+import perfetto.protos.ComputeMetricResult
+import perfetto.protos.QueryArgs
+import perfetto.protos.StatusResult
+
+@OptIn(ExperimentalTraceProcessorApi::class)
+internal class TraceProcessorHttpServer(
+ private val serverLifecycleManager: ServerLifecycleManager
+) {
+ companion object {
+ private const val HTTP_ADDRESS = "http://localhost"
+ private const val METHOD_GET = "GET"
+ private const val METHOD_POST = "POST"
+ private const val PATH_QUERY = "/query"
+ private const val PATH_COMPUTE_METRIC = "/compute_metric"
+ private const val PATH_PARSE = "/parse"
+ private const val PATH_NOTIFY_EOF = "/notify_eof"
+ private const val PATH_STATUS = "/status"
+ private const val PATH_RESTORE_INITIAL_TABLES = "/restore_initial_tables"
+ private val WAIT_INTERVAL = 5.milliseconds
+ private val READ_TIMEOUT = 30.seconds
+
+ // Note that trace processor http server has a hard limit of 64Mb for payload size.
+ // https://cs.android.com/android/platform/superproject/+/master:external/perfetto/src/base/http/http_server.cc;l=33
+ private const val PARSE_PAYLOAD_SIZE = 16 * 1024 * 1024 // 16Mb
+ }
+
+ private var hasStarted = false
+ private var port: Int = 9001 // todo: track better
+
+ /**
+ * Blocking method that runs the perfetto trace_shell_processor in server mode.
+ *
+ * @throws IllegalStateException if the server is not running by the end of the timeout.
+ */
+ @Suppress("BanThreadSleep") // needed for awaiting trace processor instance
+ fun startServer(timeout: Duration) {
+ if (hasStarted) {
+ log("Tried to start a trace shell processor that is already running.")
+ } else {
+ port = serverLifecycleManager.start()
+ // Wait for the trace_processor_shell server to start.
+ var elapsed = 0.milliseconds
+ while (!isRunning()) {
+ Thread.sleep(WAIT_INTERVAL.toLong(DurationUnit.MILLISECONDS))
+ elapsed += WAIT_INTERVAL
+ if (elapsed >= timeout) {
+ throw IllegalStateException(serverLifecycleManager.timeoutMessage())
+ }
+ }
+
+ hasStarted = true
+
+ log("Perfetto trace processor shell server started (port=$port).")
+ }
+ }
+
+ /** Stops the server killing the associated process */
+ fun stopServer() {
+ serverLifecycleManager.stop()
+ }
+
+ /** Returns true whether the server is running, false otherwise. */
+ fun isRunning(): Boolean {
+ return try {
+ val statusResult = status()
+ return statusResult.api_version != null && statusResult.api_version > 0
+ } catch (e: ConnectException) {
+ // Note that this is fired when the server port is not bound yet.
+ // This can happen before the perfetto trace processor server is fully started.
+ false
+ } catch (e: FileNotFoundException) {
+ // Note that this is fired when the endpoint queried does not exist.
+ // This can happen before the perfetto trace processor server is fully started.
+ false
+ }
+ }
+
+ /**
+ * Executes the given [sqlQuery] on a previously parsed trace with custom decoding.
+ *
+ * Note that this does not decode the query result, so it's the caller's responsibility to check
+ * for errors in the result.
+ */
+ fun <T> rawQuery(sqlQuery: String, decodeBlock: (InputStream) -> T): T =
+ httpRequest(
+ method = METHOD_POST,
+ url = PATH_QUERY,
+ encodeBlock = { QueryArgs.ADAPTER.encode(it, QueryArgs(sqlQuery)) },
+ decodeBlock = decodeBlock
+ )
+
+ /** Computes the given metrics on a previously parsed trace. */
+ fun computeMetric(
+ metrics: List<String>,
+ resultFormat: ComputeMetricArgs.ResultFormat
+ ): ComputeMetricResult =
+ httpRequest(
+ method = METHOD_POST,
+ url = PATH_COMPUTE_METRIC,
+ encodeBlock = {
+ ComputeMetricArgs.ADAPTER.encode(it, ComputeMetricArgs(metrics, resultFormat))
+ },
+ decodeBlock = { ComputeMetricResult.ADAPTER.decode(it) }
+ )
+
+ /**
+ * Parses the trace file in chunks. Note that [notifyEof] should be called at the end to let the
+ * processor know that no more chunks will be sent.
+ */
+ fun parse(inputStream: InputStream): List<AppendTraceDataResult> {
+ val responses = mutableListOf<AppendTraceDataResult>()
+ while (true) {
+ val buffer = ByteArray(PARSE_PAYLOAD_SIZE)
+ val read = inputStream.read(buffer)
+ if (read <= 0) break
+ responses.add(
+ httpRequest(
+ method = METHOD_POST,
+ url = PATH_PARSE,
+ encodeBlock = { it.write(buffer, 0, read) },
+ decodeBlock = { AppendTraceDataResult.ADAPTER.decode(it) }
+ )
+ )
+ }
+ return responses
+ }
+
+ /** Notifies that the entire trace has been uploaded and no more chunks will be sent. */
+ fun notifyEof() =
+ httpRequest(
+ method = METHOD_GET,
+ url = PATH_NOTIFY_EOF,
+ encodeBlock = null,
+ decodeBlock = {}
+ )
+
+ /** Clears the loaded trace and restore the state of the initial tables */
+ fun restoreInitialTables() =
+ httpRequest(
+ method = METHOD_GET,
+ url = PATH_RESTORE_INITIAL_TABLES,
+ encodeBlock = null,
+ decodeBlock = {}
+ )
+
+ /** Checks the status of the trace_shell_processor http server. */
+ private fun status(): StatusResult =
+ httpRequest(
+ method = METHOD_GET,
+ url = PATH_STATUS,
+ encodeBlock = null,
+ decodeBlock = { StatusResult.ADAPTER.decode(it) }
+ )
+
+ private fun <T> httpRequest(
+ method: String,
+ url: String,
+ contentType: String = "application/octet-stream",
+ encodeBlock: ((OutputStream) -> Unit)?,
+ decodeBlock: ((InputStream) -> T)
+ ): T {
+ with(URL("$HTTP_ADDRESS:${port}$url").openConnection() as HttpURLConnection) {
+ requestMethod = method
+ readTimeout = READ_TIMEOUT.toInt(DurationUnit.MILLISECONDS)
+ setRequestProperty("Content-Type", contentType)
+ if (encodeBlock != null) {
+ doOutput = true
+ encodeBlock(outputStream)
+ outputStream.close()
+ }
+ val value = decodeBlock(inputStream)
+ if (responseCode != 200) {
+ throw IllegalStateException(responseMessage)
+ }
+ return value
+ }
+ }
+}
diff --git a/benchmark/benchmark-traceprocessor/src/jvmMain/kotlin/androidx/benchmark/traceprocessor/Log.jvm.kt b/benchmark/benchmark-traceprocessor/src/jvmMain/kotlin/androidx/benchmark/traceprocessor/Log.jvm.kt
new file mode 100644
index 0000000..c1bbb88
--- /dev/null
+++ b/benchmark/benchmark-traceprocessor/src/jvmMain/kotlin/androidx/benchmark/traceprocessor/Log.jvm.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.benchmark.traceprocessor
+
+internal actual fun log(string: String) {
+ println("TraceProcessor: $string")
+}
diff --git a/benchmark/benchmark-traceprocessor/src/jvmMain/kotlin/perfetto/protos/package-info.java b/benchmark/benchmark-traceprocessor/src/jvmMain/kotlin/perfetto/protos/package-info.java
new file mode 100644
index 0000000..02661a3
--- /dev/null
+++ b/benchmark/benchmark-traceprocessor/src/jvmMain/kotlin/perfetto/protos/package-info.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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.
+ */
+
+/**
+ * Hide the perfetto.protos package, as it's an implementation detail of benchmark
+ *
+ * Note: To work around b/382741383, this file is present in both jvmMain/ and androidMain/
+ *
+ * Note: other attempts to use these protos in a benchmark process may clash with our
+ * definitions. If this becomes an issue, we can move ours to a separate, internal package.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package perfetto.protos;
+
+import androidx.annotation.RestrictTo;
diff --git a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/TrivialKotlinBenchmark.kt b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/TrivialKotlinBenchmark.kt
index b3b309d..326ebe5 100644
--- a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/TrivialKotlinBenchmark.kt
+++ b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/TrivialKotlinBenchmark.kt
@@ -16,6 +16,7 @@
package androidx.benchmark.benchmark
+import android.annotation.SuppressLint
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -29,7 +30,9 @@
class TrivialKotlinBenchmark {
@get:Rule val benchmarkRule = BenchmarkRule()
- @Test fun nothing() = benchmarkRule.measureRepeated {}
+ @SuppressLint("BanThreadSleep") // intentional bad behavior / regression
+ @Test
+ fun nothing() = benchmarkRule.measureRepeated { Thread.sleep(1) }
@Test
fun increment() {
diff --git a/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/PerfettoTraceProcessorBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/PerfettoTraceProcessorBenchmark.kt
deleted file mode 100644
index b06e13a..0000000
--- a/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/PerfettoTraceProcessorBenchmark.kt
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- * Copyright 2022 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
- *
- * http://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 androidx.benchmark.integration.macrobenchmark
-
-import androidx.benchmark.Outputs
-import androidx.benchmark.macro.ExperimentalMetricApi
-import androidx.benchmark.macro.TraceSectionMetric
-import androidx.benchmark.macro.junit4.MacrobenchmarkRule
-import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
-import androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi
-import androidx.benchmark.perfetto.PerfettoHelper
-import androidx.benchmark.perfetto.PerfettoTrace
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
-import androidx.test.filters.SdkSuppress
-import androidx.test.filters.SmallTest
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.tracing.trace
-import java.io.File
-import org.junit.Assume
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-
-@SmallTest
-@SdkSuppress(minSdkVersion = 29)
-@OptIn(
- ExperimentalMetricApi::class,
- ExperimentalPerfettoTraceProcessorApi::class,
- ExperimentalPerfettoCaptureApi::class
-)
-class PerfettoTraceProcessorBenchmark {
-
- @get:Rule val benchmarkRule = MacrobenchmarkRule()
-
- private val traceFile = createTempFileFromAsset("api32_startup_warm", ".perfetto-trace")
-
- @Before fun setUp() = Assume.assumeTrue(PerfettoHelper.isAbiSupported())
-
- @Test
- fun loadServer() =
- benchmarkRule.measureRepeated(
- packageName = PACKAGE_NAME,
- metrics = measureBlockMetric,
- iterations = 5,
- ) {
- measureBlock { PerfettoTraceProcessor.runServer {} }
- }
-
- @Test
- fun singleTrace() =
- benchmarkRule.measureRepeated(
- packageName = PACKAGE_NAME,
- metrics = measureBlockMetric,
- iterations = 5,
- ) {
- measureBlock {
- PerfettoTraceProcessor.runServer {
- loadTrace(PerfettoTrace(traceFile.absolutePath)) {}
- }
- }
- }
-
- @Test
- fun doubleTrace() =
- benchmarkRule.measureRepeated(
- packageName = PACKAGE_NAME,
- metrics = measureBlockMetric,
- iterations = 5,
- ) {
- measureBlock {
- PerfettoTraceProcessor.runServer {
- loadTrace(PerfettoTrace(traceFile.absolutePath)) {}
- loadTrace(PerfettoTrace(traceFile.absolutePath)) {}
- }
- }
- }
-
- @Test fun computeSingleMetric() = benchmarkWithTrace { runComputeStartupMetric() }
-
- @Test fun executeSingleSliceQuery() = benchmarkWithTrace { runSlicesQuery() }
-
- @Test
- fun executeMultipleQueries() = benchmarkWithTrace {
- runSlicesQuery()
- runCounterQuery()
- runProcessQuery()
- }
-
- @Test
- fun executeMultipleQueriesAndComputeMetric() = benchmarkWithTrace {
- runComputeStartupMetric()
- runSlicesQuery()
- runCounterQuery()
- runProcessQuery()
- }
-
- private fun benchmarkWithTrace(block: PerfettoTraceProcessor.Session.() -> Unit) =
- benchmarkRule.measureRepeated(
- packageName = PACKAGE_NAME,
- metrics = measureBlockMetric,
- iterations = 5,
- ) {
- measureBlock {
- // This will run perfetto trace processor http server on the specified port 10555.
- // Note that this is an arbitrary number and the default cannot be used because
- // the macrobenchmark instance of the server is running at the same time.
- PerfettoTraceProcessor.runSingleSessionServer(
- absoluteTracePath = traceFile.absolutePath,
- block = block
- )
- }
- }
-
- private fun PerfettoTraceProcessor.Session.runComputeStartupMetric() {
- getTraceMetrics("android_startup")
- }
-
- private fun PerfettoTraceProcessor.Session.runSlicesQuery() {
- query(
- """
- SELECT slice.name, slice.ts, slice.dur, thread_track.id, thread_track.name
- FROM slice
- INNER JOIN thread_track on slice.track_id = thread_track.id
- INNER JOIN thread USING(utid)
- INNER JOIN process USING(upid)
- """
- .trimIndent()
- )
- }
-
- private fun PerfettoTraceProcessor.Session.runCounterQuery() {
- query(
- """
- SELECT track.name, counter.value, counter.ts
- FROM track
- JOIN counter ON track.id = counter.track_id
- """
- .trimIndent()
- )
- }
-
- private fun PerfettoTraceProcessor.Session.runProcessQuery() {
- query(
- """
- SELECT upid
- FROM counter
- JOIN process_counter_track ON process_counter_track.id = counter.track_id
- WHERE process_counter_track.name = 'mem.swap' AND value > 1000
- """
- .trimIndent()
- )
- }
-
- private fun createTempFileFromAsset(prefix: String, suffix: String): File {
- val file = File.createTempFile(prefix, suffix, Outputs.dirUsableByAppAndShell)
- InstrumentationRegistry.getInstrumentation()
- .context
- .assets
- .open(prefix + suffix)
- .copyTo(file.outputStream())
- return file
- }
-
- companion object {
- private const val PACKAGE_NAME = "androidx.benchmark.integration.macrobenchmark.target"
- private const val SECTION_NAME = "PerfettoTraceProcessorBenchmark"
-
- /** Measures single call to [measureBlock] function */
- private val measureBlockMetric =
- listOf(
- TraceSectionMetric(
- sectionName = SECTION_NAME,
- targetPackageOnly = false // tracing in test process, not target app
- )
- )
-
- /** This block is measured by [measureBlockMetric] */
- internal inline fun <T> measureBlock(block: () -> T): T = trace(SECTION_NAME) { block() }
- }
-}
diff --git a/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/TraceProcessorBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/TraceProcessorBenchmark.kt
new file mode 100644
index 0000000..ba068a9
--- /dev/null
+++ b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/TraceProcessorBenchmark.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2022 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
+ *
+ * http://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 androidx.benchmark.integration.macrobenchmark
+
+import androidx.benchmark.Outputs
+import androidx.benchmark.macro.ExperimentalMetricApi
+import androidx.benchmark.macro.TraceSectionMetric
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.benchmark.macro.runServer
+import androidx.benchmark.macro.runSingleSessionServer
+import androidx.benchmark.perfetto.PerfettoHelper
+import androidx.benchmark.traceprocessor.PerfettoTrace
+import androidx.benchmark.traceprocessor.TraceProcessor
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.tracing.trace
+import java.io.File
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@SmallTest
+@SdkSuppress(minSdkVersion = 29)
+@OptIn(ExperimentalMetricApi::class)
+class TraceProcessorBenchmark {
+ @get:Rule val benchmarkRule = MacrobenchmarkRule()
+
+ private val traceFile = createTempFileFromAsset("api32_startup_warm", ".perfetto-trace")
+
+ @Before fun setUp() = Assume.assumeTrue(PerfettoHelper.isAbiSupported())
+
+ @Test
+ fun loadServer() =
+ benchmarkRule.measureRepeated(
+ packageName = PACKAGE_NAME,
+ metrics = measureBlockMetric,
+ iterations = 5,
+ ) {
+ measureBlock { TraceProcessor.runServer {} }
+ }
+
+ @Test
+ fun singleTrace() =
+ benchmarkRule.measureRepeated(
+ packageName = PACKAGE_NAME,
+ metrics = measureBlockMetric,
+ iterations = 5,
+ ) {
+ measureBlock {
+ TraceProcessor.runServer { loadTrace(PerfettoTrace(traceFile.absolutePath)) {} }
+ }
+ }
+
+ @Test
+ fun doubleTrace() =
+ benchmarkRule.measureRepeated(
+ packageName = PACKAGE_NAME,
+ metrics = measureBlockMetric,
+ iterations = 5,
+ ) {
+ measureBlock {
+ TraceProcessor.runServer {
+ loadTrace(PerfettoTrace(traceFile.absolutePath)) {}
+ loadTrace(PerfettoTrace(traceFile.absolutePath)) {}
+ }
+ }
+ }
+
+ @Test fun computeSingleMetric() = benchmarkWithTrace { runComputeStartupMetric() }
+
+ @Test fun executeSingleSliceQuery() = benchmarkWithTrace { runSlicesQuery() }
+
+ @Test
+ fun executeMultipleQueries() = benchmarkWithTrace {
+ runSlicesQuery()
+ runCounterQuery()
+ runProcessQuery()
+ }
+
+ @Test
+ fun executeMultipleQueriesAndComputeMetric() = benchmarkWithTrace {
+ runComputeStartupMetric()
+ runSlicesQuery()
+ runCounterQuery()
+ runProcessQuery()
+ }
+
+ private fun benchmarkWithTrace(block: TraceProcessor.Session.() -> Unit) =
+ benchmarkRule.measureRepeated(
+ packageName = PACKAGE_NAME,
+ metrics = measureBlockMetric,
+ iterations = 5,
+ ) {
+ measureBlock {
+ // This will run perfetto trace processor http server on the specified port 10555.
+ // Note that this is an arbitrary number and the default cannot be used because
+ // the macrobenchmark instance of the server is running at the same time.
+ TraceProcessor.runSingleSessionServer(
+ absoluteTracePath = traceFile.absolutePath,
+ block = block
+ )
+ }
+ }
+
+ private fun TraceProcessor.Session.runComputeStartupMetric() {
+ getTraceMetrics("android_startup")
+ }
+
+ private fun TraceProcessor.Session.runSlicesQuery() {
+ query(
+ """
+ SELECT slice.name, slice.ts, slice.dur, thread_track.id, thread_track.name
+ FROM slice
+ INNER JOIN thread_track on slice.track_id = thread_track.id
+ INNER JOIN thread USING(utid)
+ INNER JOIN process USING(upid)
+ """
+ .trimIndent()
+ )
+ }
+
+ private fun TraceProcessor.Session.runCounterQuery() {
+ query(
+ """
+ SELECT track.name, counter.value, counter.ts
+ FROM track
+ JOIN counter ON track.id = counter.track_id
+ """
+ .trimIndent()
+ )
+ }
+
+ private fun TraceProcessor.Session.runProcessQuery() {
+ query(
+ """
+ SELECT upid
+ FROM counter
+ JOIN process_counter_track ON process_counter_track.id = counter.track_id
+ WHERE process_counter_track.name = 'mem.swap' AND value > 1000
+ """
+ .trimIndent()
+ )
+ }
+
+ private fun createTempFileFromAsset(prefix: String, suffix: String): File {
+ val file = File.createTempFile(prefix, suffix, Outputs.dirUsableByAppAndShell)
+ InstrumentationRegistry.getInstrumentation()
+ .context
+ .assets
+ .open(prefix + suffix)
+ .copyTo(file.outputStream())
+ return file
+ }
+
+ companion object {
+ private const val PACKAGE_NAME = "androidx.benchmark.integration.macrobenchmark.target"
+ private const val SECTION_NAME = "TraceProcessorBenchmark"
+
+ /** Measures single call to [measureBlock] function */
+ private val measureBlockMetric =
+ listOf(
+ TraceSectionMetric(
+ sectionName = SECTION_NAME,
+ targetPackageOnly = false // tracing in test process, not target app
+ )
+ )
+
+ /** This block is measured by [measureBlockMetric] */
+ internal inline fun <T> measureBlock(block: () -> T): T = trace(SECTION_NAME) { block() }
+ }
+}
diff --git a/buildSrc-tests/lint-baseline.xml b/buildSrc-tests/lint-baseline.xml
index 406a7467..f24bb50 100644
--- a/buildSrc-tests/lint-baseline.xml
+++ b/buildSrc-tests/lint-baseline.xml
@@ -220,15 +220,6 @@
<issue
id="GradleProjectIsolation"
message="Use isolated.rootProject instead of getRootProject"
- errorLine1=" rootProject.extensions.findByType<NodeJsRootExtension>()?.let { nodeJs ->"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use isolated.rootProject instead of getRootProject"
errorLine1=" rootProject.extensions.findByType(YarnRootExtension::class.java)?.let { yarn ->"
errorLine2=" ~~~~~~~~~~~">
<location
@@ -436,15 +427,6 @@
<issue
id="GradleProjectIsolation"
message="Use isolated.rootProject instead of getRootProject"
- errorLine1=" project.rootProject.gradle.sharedServices.registerIfAbsent("
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/ProjectParser.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use isolated.rootProject instead of getRootProject"
errorLine1=" regenerate(project.rootProject, groupId, artifactId, artifactVersion, location)"
errorLine2=" ~~~~~~~~~~~">
<location
diff --git a/buildSrc-tests/src/test/java/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt b/buildSrc-tests/src/test/java/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt
index 530d3e9..ab979b6 100644
--- a/buildSrc-tests/src/test/java/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt
+++ b/buildSrc-tests/src/test/java/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt
@@ -38,6 +38,7 @@
builder = ConfigBuilder()
builder
.configName("placeHolderAndroidTest.xml")
+ .configType(TestConfigType.DEFAULT)
.isMicrobenchmark(false)
.applicationId("com.androidx.placeholder.Placeholder")
.isPostsubmit(true)
@@ -55,8 +56,23 @@
@Test
fun testXmlAgainstGoldenMainSandboxConfiguration() {
- builder.initialSetupApks(listOf("init-placeholder.apk"))
- builder.enablePrivacySandbox(true)
+ builder.appApksModel(
+ AppApksModel(
+ apkGroups =
+ listOf(
+ singleFileApkFileGroup("init-placeholder.apk"),
+ ApkFileGroup(
+ apks =
+ listOf(
+ ApkFile(name = "app.apk"),
+ ApkFile(name = "split1.apk"),
+ ApkFile(name = "split2.apk")
+ )
+ ),
+ )
+ )
+ )
+ builder.configType(TestConfigType.PRIVACY_SANDBOX_MAIN)
MatcherAssert.assertThat(
builder.buildXml(),
CoreMatchers.`is`(goldenConfigForMainSandboxConfiguration)
@@ -64,13 +80,6 @@
}
@Test
- fun testXmlAgainstGoldenWithSplits() {
- builder.appApkName("app.apk")
- builder.appSplits(listOf("split1.apk", "split2.apk"))
- MatcherAssert.assertThat(builder.buildXml(), CoreMatchers.`is`(goldenConfigWithSplits))
- }
-
- @Test
fun testXmlAgainstGoldenMicrobenchmark() {
builder.isMicrobenchmark(true)
@@ -91,8 +100,7 @@
builder.isMacrobenchmark(true)
builder.instrumentationArgsMap["androidx.test.argument1"] = "something1"
builder.instrumentationArgsMap["androidx.test.argument2"] = "something2"
- builder.appApkSha256("654321")
- builder.appApkName("targetApp.apk")
+ builder.appApksModel(singleFileAppApksModel(name = "targetApp.apk", sha256 = "654321"))
// NOTE: blocklisted arg is removed
builder.instrumentationArgsMap["androidx.benchmark.profiling.skipWhenDurationRisksAnr"] =
@@ -104,6 +112,37 @@
}
@Test
+ fun testXmlAgainstGoldenMainSandboxMacroBenchmark() {
+ builder.isMacrobenchmark(true)
+ builder.instrumentationArgsMap["androidx.test.argument1"] = "something1"
+ builder.instrumentationArgsMap["androidx.test.argument2"] = "something2"
+ builder.appApksModel(
+ AppApksModel(
+ apkGroups =
+ listOf(
+ singleFileApkFileGroup(name = "targetSdk.apk", sha256 = "1"),
+ ApkFileGroup(
+ apks =
+ listOf(
+ ApkFile(name = "targetApp.apk", sha256 = "2"),
+ ApkFile(name = "targetAppSplit.apk", sha256 = "3")
+ )
+ ),
+ )
+ )
+ )
+ builder.configType(TestConfigType.PRIVACY_SANDBOX_MAIN)
+
+ // NOTE: blocklisted arg is removed
+ builder.instrumentationArgsMap["androidx.benchmark.profiling.skipWhenDurationRisksAnr"] =
+ "true"
+ MatcherAssert.assertThat(
+ builder.buildXml(),
+ CoreMatchers.`is`(goldenConfigForMainSandboxMacroBenchmark)
+ )
+ }
+
+ @Test
fun testJsonAgainstGoldenDefault() {
builder.instrumentationArgsMap["androidx.test.argument1"] = "something1"
builder.instrumentationArgsMap["androidx.test.argument2"] = "something2"
@@ -207,7 +246,9 @@
@Test
fun testJsonAgainstAppTestGolden() {
- builder.appApkName("app-placeholder.apk").appApkSha256("654321")
+ builder.appApksModel(
+ singleFileAppApksModel(name = "app-placeholder.apk", sha256 = "654321")
+ )
MatcherAssert.assertThat(
builder.buildJson(),
CoreMatchers.`is`(
@@ -299,13 +340,13 @@
@Test
fun testValidTestConfigXml_withAppApk() {
- builder.appApkName("Placeholder.apk")
+ builder.appApksModel(singleFileAppApksModel(name = "Placeholder.apk"))
validate(builder.buildXml())
}
@Test
fun testValidTestConfigXml_presubmitWithAppApk() {
- builder.isPostsubmit(false).appApkName("Placeholder.apk")
+ builder.isPostsubmit(false).appApksModel(singleFileAppApksModel(name = "Placeholder.apk"))
validate(builder.buildXml())
}
@@ -395,12 +436,14 @@
<option name="config-descriptor:metadata" key="applicationId" value="com.androidx.placeholder.Placeholder" />
<option name="wifi:disable" value="true" />
<option name="instrumentation-arg" key="notAnnotation" value="androidx.test.filters.FlakyTest" />
+ <option name="instrumentation-arg" key="androidx.testConfigType" value="PRIVACY_SANDBOX_MAIN" />
<include name="google/unbundled/common/setup" />
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
<option name="install-arg" value="-t" />
- <option name="test-file-name" value="init-placeholder.apk" />
<option name="test-file-name" value="placeholder.apk" />
+ <option name="test-file-name" value="init-placeholder.apk" />
+ <option name="split-apk-file-names" value="app.apk,split1.apk,split2.apk" />
</target_preparer>
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="run-command" value="cmd sdk_sandbox set-state --enabled"/>
@@ -416,42 +459,6 @@
"""
.trimIndent()
-private val goldenConfigWithSplits =
- """
- <?xml version="1.0" encoding="utf-8"?>
- <!-- Copyright (C) 2020 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
- http://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.-->
- <configuration description="Runs tests for the module">
- <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MinApiLevelModuleController">
- <option name="min-api-level" value="15" />
- </object>
- <option name="test-suite-tag" value="placeholder_tag" />
- <option name="config-descriptor:metadata" key="applicationId" value="com.androidx.placeholder.Placeholder" />
- <option name="wifi:disable" value="true" />
- <option name="instrumentation-arg" key="notAnnotation" value="androidx.test.filters.FlakyTest" />
- <include name="google/unbundled/common/setup" />
- <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
- <option name="cleanup-apks" value="true" />
- <option name="install-arg" value="-t" />
- <option name="test-file-name" value="placeholder.apk" />
- <option name="split-apk-file-names" value="app.apk,split1.apk,split2.apk" />
- </target_preparer>
- <test class="com.android.tradefed.testtype.AndroidJUnitTest">
- <option name="runner" value="com.example.Runner"/>
- <option name="package" value="com.androidx.placeholder.Placeholder" />
- </test>
- </configuration>
-"""
- .trimIndent()
-
private val goldenDefaultConfigBenchmark =
"""
<?xml version="1.0" encoding="utf-8"?>
@@ -537,3 +544,54 @@
</configuration>
"""
.trimIndent()
+
+private val goldenConfigForMainSandboxMacroBenchmark =
+ """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- Copyright (C) 2020 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
+ http://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.-->
+ <configuration description="Runs tests for the module">
+ <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MinApiLevelModuleController">
+ <option name="min-api-level" value="15" />
+ </object>
+ <option name="test-suite-tag" value="placeholder_tag" />
+ <option name="config-descriptor:metadata" key="applicationId" value="com.androidx.placeholder.Placeholder" />
+ <option name="wifi:disable" value="true" />
+ <option name="instrumentation-arg" key="notAnnotation" value="androidx.test.filters.FlakyTest" />
+ <option name="instrumentation-arg" key="androidx.test.argument1" value="something1" />
+ <option name="instrumentation-arg" key="androidx.test.argument2" value="something2" />
+ <option name="instrumentation-arg" key="androidx.benchmark.output.payload.testApkSha256" value="123456" />
+ <option name="instrumentation-arg" key="androidx.benchmark.output.payload.appApkSha256" value="a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3" />
+ <option name="instrumentation-arg" key="androidx.benchmark.enabledRules" value="Macrobenchmark" />
+ <option name="instrumentation-arg" key="androidx.testConfigType" value="PRIVACY_SANDBOX_MAIN" />
+ <include name="google/unbundled/common/setup" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="install-arg" value="-t" />
+ <option name="test-file-name" value="placeholder.apk" />
+ <option name="test-file-name" value="targetSdk.apk" />
+ <option name="split-apk-file-names" value="targetApp.apk,targetAppSplit.apk" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="cmd sdk_sandbox set-state --enabled"/>
+ <option name="run-command" value="device_config set_sync_disabled_for_tests persistent" />
+ <option name="teardown-command" value="cmd sdk_sandbox set-state --reset"/>
+ <option name="teardown-command" value="device_config set_sync_disabled_for_tests none" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="runner" value="com.example.Runner"/>
+ <option name="package" value="com.androidx.placeholder.Placeholder" />
+ <option name="device-listeners" value="androidx.benchmark.macro.junit4.InstrumentationResultsRunListener" />
+ <option name="device-listeners" value="androidx.benchmark.macro.junit4.SideEffectRunListener" />
+ </test>
+ </configuration>
+"""
+ .trimIndent()
diff --git a/buildSrc/imports/privacysandbox-gradle-plugin/build.gradle b/buildSrc/imports/privacysandbox-gradle-plugin/build.gradle
new file mode 100644
index 0000000..b90b78b
--- /dev/null
+++ b/buildSrc/imports/privacysandbox-gradle-plugin/build.gradle
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+apply from: "../../shared.gradle"
+apply plugin: "java-gradle-plugin"
+
+sourceSets {
+ main.java.srcDirs += "${supportRootFolder}/privacysandbox/plugins/plugins-privacysandbox-library/src/main/java"
+}
+
+gradlePlugin {
+ plugins {
+ privacysandboxplugin {
+ id = "androidx.privacysandboxplugin"
+ implementationClass = "androidx.privacysandboxlibraryplugin.PrivacySandboxLibraryPlugin"
+ }
+ }
+}
+
+validatePlugins {
+ enableStricterValidation = true
+}
diff --git a/buildSrc/karmaconfig/karma.conf.js b/buildSrc/karmaconfig/karma.conf.js
index 9d1d211..2624c3c 100644
--- a/buildSrc/karmaconfig/karma.conf.js
+++ b/buildSrc/karmaconfig/karma.conf.js
@@ -1,6 +1,6 @@
// Set a fairly long test timeout because some tests in collection
// (specifically insertManyRemoveMany) occasionally take 20+ seconds to complete.
-var testTimeoutInMs = 10000 * 30
+var testTimeoutInMs = 1000 * 30
// disconnect timeout should be longer than test timeout so we don't disconnect before the timeout
// is reported.
var browserDisconnectTimeoutInMs = testTimeoutInMs + 5000
@@ -8,11 +8,23 @@
// https://karma-runner.github.io/6.4/config/configuration-file.html
browserDisconnectTimeout: browserDisconnectTimeoutInMs,
processKillTimeout: testTimeoutInMs,
- concurrency: 1,
- logLevel: config.LOG_DEBUG,
+ concurrency: 10,
client: {
mocha: {
timeout: testTimeoutInMs
}
}
-})
+});
+
+// Add 5 second delay exit to ensure log flushing. This is needed for Kotlin to avoid flakiness when
+// marking a test as complete. See (b/382336155)
+// Remove when https://youtrack.jetbrains.com/issue/KT-73911/ is resolved.
+(function() {
+ const originalExit = process.exit;
+ process.exit = function(code) {
+ console.log('Delaying exit for logs...');
+ setTimeout(() => {
+ originalExit(code);
+ }, 5000);
+ };
+})();
diff --git a/buildSrc/plugins/build.gradle b/buildSrc/plugins/build.gradle
index 64cc36d..cdeba21 100644
--- a/buildSrc/plugins/build.gradle
+++ b/buildSrc/plugins/build.gradle
@@ -2,14 +2,15 @@
dependencies {
implementation(project(":public"))
- api project(":imports:baseline-profile-gradle-plugin")
- api project(":imports:benchmark-darwin-plugin")
- api project(":imports:benchmark-gradle-plugin")
- api project(":imports:glance-layout-generator")
- api project(":imports:inspection-gradle-plugin")
- api project(":imports:room-gradle-plugin")
- api project(":imports:stableaidl-gradle-plugin")
- api project(":imports:binary-compatibility-validator")
+ api(project(":imports:baseline-profile-gradle-plugin"))
+ api(project(":imports:benchmark-darwin-plugin"))
+ api(project(":imports:benchmark-gradle-plugin"))
+ api(project(":imports:binary-compatibility-validator"))
+ api(project(":imports:glance-layout-generator"))
+ api(project(":imports:inspection-gradle-plugin"))
+ api(project(":imports:privacysandbox-gradle-plugin"))
+ api(project(":imports:room-gradle-plugin"))
+ api(project(":imports:stableaidl-gradle-plugin"))
}
apply from: "../shared.gradle"
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 921b41a..84178ef 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -50,6 +50,9 @@
import com.android.build.api.attributes.BuildTypeAttr
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.CommonExtension
+import com.android.build.api.dsl.KotlinMultiplatformAndroidDeviceTestCompilation
+import com.android.build.api.dsl.KotlinMultiplatformAndroidHostTestCompilation
+import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
import com.android.build.api.dsl.LibraryExtension
import com.android.build.api.dsl.PrivacySandboxSdkExtension
import com.android.build.api.dsl.TestBuildType
@@ -682,10 +685,9 @@
project.configureJavaCompilationWarnings(androidXExtension)
}
- @Suppress("TYPEALIAS_EXPANSION_DEPRECATION")
private fun configureWithKotlinMultiplatformAndroidPlugin(
project: Project,
- kotlinMultiplatformAndroidTarget: DeprecatedKotlinMultiplatformAndroidTarget,
+ kotlinMultiplatformAndroidTarget: KotlinMultiplatformAndroidLibraryTarget,
androidXExtension: AndroidXExtension
) {
val kotlinMultiplatformAndroidComponentsExtension =
@@ -1093,8 +1095,7 @@
buildToolsVersion = project.defaultAndroidConfig.buildToolsVersion
- // b/366238650
- defaultConfig.ndk.abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64"))
+ defaultConfig.ndk.abiFilters.addAll(SUPPORTED_BUILD_ABIS)
defaultConfig.minSdk = defaultMinSdk
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -1155,8 +1156,7 @@
)
}
- @Suppress("TYPEALIAS_EXPANSION_DEPRECATION")
- private fun DeprecatedKotlinMultiplatformAndroidTarget.configureAndroidBaseOptions(
+ private fun KotlinMultiplatformAndroidLibraryTarget.configureAndroidBaseOptions(
project: Project,
componentsExtension: KotlinMultiplatformAndroidComponentsExtension
) {
@@ -1170,13 +1170,13 @@
lint.targetSdk = project.defaultAndroidConfig.targetSdk
compilations
- .withType(DeprecatedKotlinMultiplatformAndroidTestOnDeviceCompilation::class.java)
+ .withType(KotlinMultiplatformAndroidDeviceTestCompilation::class.java)
.configureEach {
it.instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
it.animationsDisabled = true
}
compilations
- .withType(DeprecatedKotlinMultiplatformAndroidTestOnJvmCompilation::class.java)
+ .withType(KotlinMultiplatformAndroidHostTestCompilation::class.java)
.configureEach {
it.isReturnDefaultValues = true
// Include resources in Robolectric tests as a workaround for b/184641296
@@ -1447,6 +1447,9 @@
const val EXTENSION_NAME = "androidx"
+ // b/366238650
+ val SUPPORTED_BUILD_ABIS = listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
+
/** Fail the build if a non-Studio task runs longer than expected */
const val TASK_TIMEOUT_MINUTES = 60L
}
@@ -1520,11 +1523,20 @@
* [TASK_TIMEOUT_MINUTES].
*/
internal fun Project.configureTaskTimeouts() {
+ // A set of tasks that sometimes take >60 minutes. b/383874664
+ val slowTasks =
+ setOf(
+ ":compose:ui:ui:compileReleaseAndroidTestKotlinAndroid",
+ ":compose:foundation:foundation:compileReleaseAndroidTestKotlinAndroid",
+ ":compose:foundation:foundation:integration-tests:lazy-tests:compileReleaseAndroidTestKotlin"
+ )
tasks.configureEach { t ->
// skip adding a timeout for some tasks that both take a long time and
// that we can count on the user to monitor
if (t !is StudioTask) {
- t.timeout.set(Duration.ofMinutes(TASK_TIMEOUT_MINUTES))
+ t.timeout.set(
+ Duration.ofMinutes(if (t.path in slowTasks) 80L else TASK_TIMEOUT_MINUTES)
+ )
}
}
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
index 97b9604..d5187fa 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
@@ -14,10 +14,7 @@
* limitations under the License.
*/
-@file:Suppress(
- "UnstableApiUsage",
- "TYPEALIAS_EXPANSION_DEPRECATION"
-) // // KotlinMultiplatformAndroidTarget / DeprecatedKotlinMultiplatformAndroidTarget
+@file:Suppress("UnstableApiUsage")
package androidx.build
@@ -25,6 +22,7 @@
import androidx.build.clang.MultiTargetNativeCompilation
import androidx.build.clang.NativeLibraryBundler
import androidx.build.clang.configureCinterop
+import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
import com.android.build.gradle.api.KotlinMultiplatformAndroidPlugin
import groovy.lang.Closure
import java.io.File
@@ -50,7 +48,7 @@
import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsTargetDsl
import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinWasmTargetDsl
import org.jetbrains.kotlin.gradle.targets.js.ir.DefaultIncrementalSyncTask
-import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension
+import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsEnvSpec
import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension
@@ -78,10 +76,10 @@
// make sure to initialize the kotlin extension by accessing the property
val extension = (kotlinExtension as ExtensionAware)
project.plugins.apply(KotlinMultiplatformAndroidPlugin::class.java)
- extension.extensions.getByType(DeprecatedKotlinMultiplatformAndroidTarget::class.java)
+ extension.extensions.getByType(KotlinMultiplatformAndroidLibraryTarget::class.java)
}
- val agpKmpExtension: DeprecatedKotlinMultiplatformAndroidTarget by agpKmpExtensionDelegate
+ val agpKmpExtension: KotlinMultiplatformAndroidLibraryTarget by agpKmpExtensionDelegate
/**
* The list of platforms that have been declared as supported in the build configuration.
@@ -420,8 +418,8 @@
@JvmOverloads
fun androidLibrary(
- block: Action<DeprecatedKotlinMultiplatformAndroidTarget>? = null
- ): DeprecatedKotlinMultiplatformAndroidTarget? {
+ block: Action<KotlinMultiplatformAndroidLibraryTarget>? = null
+ ): KotlinMultiplatformAndroidLibraryTarget? {
supportedPlatforms.add(PlatformIdentifier.ANDROID)
return if (project.enableJvm()) {
agpKmpExtension.also { block?.execute(it) }
@@ -748,16 +746,18 @@
}
private fun Project.configureNode() {
- rootProject.extensions.findByType<NodeJsRootExtension>()?.let { nodeJs ->
- nodeJs.version = getVersionByName("node")
+ extensions.findByType<NodeJsEnvSpec>()?.let { nodeJs ->
+ nodeJs.version.set(getVersionByName("node"))
if (!ProjectLayoutType.isPlayground(this)) {
- nodeJs.downloadBaseUrl =
+ nodeJs.downloadBaseUrl.set(
File(project.getPrebuiltsRoot(), "androidx/external/org/nodejs/node")
.toURI()
.toString()
+ )
}
}
+ // https://youtrack.jetbrains.com/issue/KT-73913/K-Wasm-yarn-version-per-project
rootProject.extensions.findByType(YarnRootExtension::class.java)?.let { yarn ->
yarn.version = getVersionByName("yarn")
yarn.yarnLockMismatchReport = YarnLockMismatchReport.FAIL
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
index 7521e49..c1a7e03 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -101,12 +101,12 @@
extra.set("projects", ConcurrentHashMap<String, String>())
/**
- * Copy PrivacySandbox related APKs into [getTestConfigDirectory] before zipping. Flatten
- * directory hierarchy as both TradeFed and FTL work with flat hierarchy.
+ * Copy App APKs (from ApkOutputProviders) into [getTestConfigDirectory] before zipping.
+ * Flatten directory hierarchy as both TradeFed and FTL work with flat hierarchy.
*/
val finalizeConfigsTask =
project.tasks.register(FINALIZE_TEST_CONFIGS_WITH_APKS_TASK, Copy::class.java) {
- it.from(project.getPrivacySandboxFilesDirectory())
+ it.from(project.getAppApksFilesDirectory())
it.into(project.getTestConfigDirectory())
it.eachFile { f -> f.relativePath = RelativePath(true, f.name) }
it.includeEmptyDirs = false
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/DeprecatedKotlinMultiplatformAndroidTargetAndCompilation.kt b/buildSrc/private/src/main/kotlin/androidx/build/DeprecatedKotlinMultiplatformAndroidTargetAndCompilation.kt
deleted file mode 100644
index 700c36c..0000000
--- a/buildSrc/private/src/main/kotlin/androidx/build/DeprecatedKotlinMultiplatformAndroidTargetAndCompilation.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright 2024 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
- *
- * http://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.
- */
-
-@file:Suppress("DEPRECATION", "UnstableApiUsage")
-
-package androidx.build
-
-import com.android.build.api.dsl.KotlinMultiplatformAndroidTarget
-import com.android.build.api.dsl.KotlinMultiplatformAndroidTestOnDeviceCompilation
-import com.android.build.api.dsl.KotlinMultiplatformAndroidTestOnJvmCompilation
-
-typealias DeprecatedKotlinMultiplatformAndroidTarget = KotlinMultiplatformAndroidTarget
-
-typealias DeprecatedKotlinMultiplatformAndroidTestOnDeviceCompilation =
- KotlinMultiplatformAndroidTestOnDeviceCompilation
-
-typealias DeprecatedKotlinMultiplatformAndroidTestOnJvmCompilation =
- KotlinMultiplatformAndroidTestOnJvmCompilation
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
index db4af57..d26533a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -15,6 +15,7 @@
*/
package androidx.build
+import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
import com.android.build.api.dsl.Lint
import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension
import com.android.build.gradle.AppPlugin
@@ -79,9 +80,8 @@
configureLint(extension.lint, isLibrary)
}
-@Suppress("TYPEALIAS_EXPANSION_DEPRECATION")
private fun Project.configureAndroidMultiplatformProjectForLint(
- extension: DeprecatedKotlinMultiplatformAndroidTarget,
+ extension: KotlinMultiplatformAndroidLibraryTarget,
componentsExtension: KotlinMultiplatformAndroidComponentsExtension
) {
componentsExtension.finalizeDsl {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
index 624b8af..6d244d1 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
@@ -163,7 +163,10 @@
}
configure<PublishingExtension> {
- repositories { it.maven { repo -> repo.setUrl(getRepositoryDirectory()) } }
+ repositories {
+ it.maven { repo -> repo.setUrl(getRepositoryDirectory()) }
+ it.maven { repo -> repo.setUrl(getPerProjectRepositoryDirectory()) }
+ }
publications {
if (appliesJavaGradlePluginPlugin()) {
// The 'java-gradle-plugin' will also add to the 'pluginMaven' publication
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt b/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt
index 7a9a8ce..0cb1030 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt
@@ -92,7 +92,7 @@
fun Project.parseBuildFile(buildFile: File): ProjectParser.ParsedProject {
val parserProvider =
- project.rootProject.gradle.sharedServices.registerIfAbsent(
+ project.gradle.sharedServices.registerIfAbsent(
"ProjectParser",
ProjectParser::class.java
) {}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/Release.kt b/buildSrc/private/src/main/kotlin/androidx/build/Release.kt
index 94707da..d3b8452 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/Release.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/Release.kt
@@ -15,102 +15,79 @@
*/
package androidx.build
-import androidx.build.uptodatedness.cacheEvenIfNoOutputs
import java.io.File
-import java.io.FileNotFoundException
-import org.gradle.api.Action
+import java.io.FileOutputStream
+import java.util.Calendar
+import java.util.GregorianCalendar
+import java.util.zip.ZipEntry
+import java.util.zip.ZipOutputStream
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Project
-import org.gradle.api.file.DuplicatesStrategy
-import org.gradle.api.provider.Provider
-import org.gradle.api.tasks.Input
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
-import org.gradle.api.tasks.bundling.Zip
-import org.gradle.plugin.devel.GradlePluginDevelopmentExtension
import org.gradle.work.DisableCachingByDefault
-/** Simple description for an artifact that is released from this project. */
-data class Artifact(
- @get:Input val mavenGroup: String,
- @get:Input val projectName: String,
- @get:Input val version: String
-) {
- override fun toString() = "$mavenGroup:$projectName:$version"
-}
-
-/** Zip task that zips all artifacts from given candidates. */
+/** Zips all artifacts to publish. */
@DisableCachingByDefault(because = "Zip tasks are not worth caching according to Gradle")
-// See
-// https://github.com/gradle/gradle/commit/7e5c5bc9b2c23d872e1c45c855f07ca223f6c270#diff-ce55b0f0cdcf2174eb47d333d348ff6fbd9dbe5cd8c3beeeaf633ea23b74ed9eR38
-open class GMavenZipTask : Zip() {
+abstract class GMavenZipTask : DefaultTask() {
- init {
- // multiple artifacts in the same group might have the same maven-metadata.xml
- duplicatesStrategy = DuplicatesStrategy.EXCLUDE
- }
-
- /** Set to true to include maven-metadata.xml */
- @get:Input var includeMetadata: Boolean = false
+ /** Whether this build adds automatic constraints between projects in the same group */
+ @Internal val shouldAddGroupConstraints = project.shouldAddGroupConstraints()
/** Repository containing artifacts to include */
- @get:Internal lateinit var androidxRepoOut: File
+ @get:InputDirectory
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val projectRepositoryDir: DirectoryProperty
- fun addCandidate(artifact: Artifact) {
- val groupSubdir = artifact.mavenGroup.replace('.', '/')
- val projectSubdir = File("$groupSubdir/${artifact.projectName}")
- val artifactSubdir = File("$projectSubdir/${artifact.version}")
- // We specifically pass the subdirectory and specific files into 'from' so that Gradle
- // knows that other directories aren't related:
- // 1. changes in other directories shouldn't cause this task to become out of date
- // 2. contents of other directories shouldn't be cached:
- // https://github.com/gradle/gradle/issues/24368
- from("$androidxRepoOut/$artifactSubdir") { spec ->
- spec.into("m2repository/$artifactSubdir")
+ /** Zip file to save artifacts to */
+ @get:OutputFile abstract val archiveFile: RegularFileProperty
+
+ @TaskAction
+ fun createZip() {
+ if (!shouldAddGroupConstraints.get() && !isSnapshotBuild()) {
+ throw GradleException(
+ """
+ Cannot publish artifacts without setting -P$ADD_GROUP_CONSTRAINTS=true
+
+ This property is required when building artifacts to publish
+
+ (but this property can reduce remote cache usage so it is disabled by default)
+
+ See AndroidXGradleProperties.kt for more information about this property
+ """
+ .trimIndent()
+ )
}
- if (includeMetadata) {
- val suffixes = setOf("", ".md5", ".sha1", ".sha256", ".sha512")
- for (suffix in suffixes) {
- val filename = "maven-metadata.xml$suffix"
- from("$androidxRepoOut/$projectSubdir/$filename") { spec ->
- spec.into("m2repository/$projectSubdir")
+ val sourceDir = projectRepositoryDir.get().asFile
+ ZipOutputStream(FileOutputStream(archiveFile.get().asFile)).use { zipOut ->
+ zipOut.putNextEntry(
+ // Top-level of the ZIP to align with Maven's expected repository structure
+ ZipEntry("m2repository/").also { it.time = CONSTANT_TIME_FOR_ZIP_ENTRIES }
+ )
+ zipOut.closeEntry()
+
+ sourceDir.walkTopDown().forEach { fileOrDir ->
+ if (fileOrDir == sourceDir) return@forEach
+
+ val relativePath = fileOrDir.relativeTo(sourceDir).invariantSeparatorsPath
+ val entryName =
+ "m2repository/$relativePath" + if (fileOrDir.isDirectory) "/" else ""
+
+ zipOut.putNextEntry(
+ ZipEntry(entryName).also { it.time = CONSTANT_TIME_FOR_ZIP_ENTRIES }
+ )
+ if (fileOrDir.isFile) {
+ fileOrDir.inputStream().use { it.copyTo(zipOut) }
}
- }
- }
- }
-
- /** Config action that configures the task when necessary. */
- class ConfigAction(private val params: Params) : Action<GMavenZipTask> {
- data class Params(
- /** Maven group for the task. "" if multiple groups or only one project */
- val mavenGroup: String,
- /** Set to true to include maven-metadata.xml */
- var includeMetadata: Boolean,
- /** The root of the repository where built libraries can be found */
- val androidxRepoOut: File,
- /** The out folder where the zip will be created */
- val distDir: File,
- /** Prefix of file name to create */
- val fileNamePrefix: String,
- /** The build number specified by the server */
- val buildNumber: String
- )
-
- override fun execute(task: GMavenZipTask) {
- params.apply {
- task.description =
- """
- Creates a maven repository that includes just the libraries compiled in
- this project.
- Group: ${if (mavenGroup != "") mavenGroup else "All"}
- """
- .trimIndent()
- task.androidxRepoOut = androidxRepoOut
- task.destinationDirectory.set(distDir)
- task.includeMetadata = params.includeMetadata
- task.archiveBaseName.set(getZipName(fileNamePrefix, mavenGroup))
+ zipOut.closeEntry()
}
}
}
@@ -126,9 +103,6 @@
const val PROJECT_ZIPS_FOLDER = "per-project-zips"
private const val GLOBAL_ZIP_PREFIX = "top-of-tree-m2repository"
- // lazily created config action params so that we don't keep re-creating them
- private var configActionParams: GMavenZipTask.ConfigAction.Params? = null
-
/**
* Registers the project to be included in its group's zip file as well as the global zip files.
*/
@@ -159,7 +133,6 @@
"Cannot register a project to release if it does not have a mavenVersion set up"
)
}
- val version = project.version
val projectZipTask =
getProjectZipTask(project, androidXExtension.isIsolatedProjectsEnabled())
@@ -169,80 +142,8 @@
getGlobalFullZipTask(project, androidXExtension.isIsolatedProjectsEnabled())
)
- val artifacts = androidXExtension.publishedArtifacts
val publishTask = project.tasks.named("publish")
- zipTasks.forEach {
- it.configure { zipTask ->
- artifacts.forEach { artifact -> zipTask.addCandidate(artifact) }
-
- // Add additional artifacts needed for Gradle Plugins
- if (androidXExtension.type == LibraryType.GRADLE_PLUGIN) {
- project.extensions
- .getByType(GradlePluginDevelopmentExtension::class.java)
- .plugins
- .forEach { plugin ->
- zipTask.addCandidate(
- Artifact(
- mavenGroup = plugin.id,
- projectName = "${plugin.id}.gradle.plugin",
- version = version.toString()
- )
- )
- }
- }
-
- zipTask.dependsOn(publishTask)
- }
- }
-
- val verifyInputs = getVerifyProjectZipInputsTask(project)
- verifyInputs.configure { verifyTask ->
- verifyTask.dependsOn(publishTask)
- artifacts.forEach { artifact -> verifyTask.addCandidate(artifact) }
- }
- val verifyOutputs = getVerifyProjectZipOutputsTask(project)
- verifyOutputs.configure { verifyTask ->
- verifyTask.dependsOn(projectZipTask)
- artifacts.forEach { artifact -> verifyTask.addCandidate(artifact) }
- }
- projectZipTask.configure { zipTask ->
- zipTask.dependsOn(verifyInputs)
- zipTask.finalizedBy(verifyOutputs)
- val verifyOutputsTask = verifyOutputs.get()
- verifyOutputsTask.addFile(zipTask.archiveFile.get().asFile)
- }
- }
-
- /**
- * Create config action parameters for the project and group. If group is `null`, parameters are
- * created for the global tasks.
- */
- private fun getParams(
- project: Project,
- distDir: File,
- fileNamePrefix: String = "",
- group: String? = null
- ): GMavenZipTask.ConfigAction.Params {
- // Make base params or reuse if already created
- val params =
- configActionParams
- ?: GMavenZipTask.ConfigAction.Params(
- mavenGroup = "",
- includeMetadata = false,
- androidxRepoOut = project.getRepositoryDirectory(),
- distDir = distDir,
- fileNamePrefix = fileNamePrefix,
- buildNumber = getBuildId()
- )
- .also { configActionParams = it }
- distDir.mkdirs()
-
- // Copy base params and apply any specific differences
- return params.copy(
- mavenGroup = group ?: "",
- distDir = distDir,
- fileNamePrefix = fileNamePrefix
- )
+ zipTasks.forEach { it.configure { zipTask -> zipTask.dependsOn(publishTask) } }
}
/** Registers an archive task as a dependency of the anchor task */
@@ -272,16 +173,11 @@
if (projectIsolationEnabled) return null
return project.rootProject.maybeRegister(
name = FULL_ARCHIVE_TASK_NAME,
- onConfigure = {
- GMavenZipTask.ConfigAction(
- getParams(
- project,
- project.getDistributionDirectory(),
- fileNamePrefix = GLOBAL_ZIP_PREFIX
- )
- .copy(includeMetadata = true)
- )
- .execute(it)
+ onConfigure = { task: GMavenZipTask ->
+ task.archiveFile.set(
+ File(project.getDistributionDirectory(), "${getZipName(GLOBAL_ZIP_PREFIX)}.zip")
+ )
+ task.projectRepositoryDir.set(project.getRepositoryDirectory())
},
onRegister = { taskProvider: TaskProvider<GMavenZipTask> ->
project.addToAnchorTask(taskProvider)
@@ -295,183 +191,35 @@
): TaskProvider<GMavenZipTask> {
val taskProvider =
project.tasks.register(PROJECT_ARCHIVE_ZIP_TASK_NAME, GMavenZipTask::class.java) {
- task: GMavenZipTask ->
- GMavenZipTask.ConfigAction(
- getParams(
- project = project,
- distDir =
- File(project.getDistributionDirectory(), PROJECT_ZIPS_FOLDER),
- fileNamePrefix = project.projectZipPrefix()
- )
- .copy(includeMetadata = true)
- )
- .execute(task)
+ it.archiveFile.set(
+ File(project.getDistributionDirectory(), project.getProjectZipPath())
+ )
+ it.projectRepositoryDir.set(project.getPerProjectRepositoryDirectory())
}
if (!projectIsolationEnabled) project.addToAnchorTask(taskProvider)
return taskProvider
}
-
- private fun getVerifyProjectZipInputsTask(project: Project): TaskProvider<VerifyGMavenZipTask> {
- return project.tasks.register(
- "verifyInputs$PROJECT_ARCHIVE_ZIP_TASK_NAME",
- VerifyGMavenZipTask::class.java
- )
- }
-
- private fun getVerifyProjectZipOutputsTask(
- project: Project
- ): TaskProvider<VerifyGMavenZipTask> {
- return project.tasks.register(
- "verifyOutputs$PROJECT_ARCHIVE_ZIP_TASK_NAME",
- VerifyGMavenZipTask::class.java
- )
- }
}
-// b/273294710
-@DisableCachingByDefault(
- because = "This task only checks the existence of files and isn't worth caching"
-)
-open class VerifyGMavenZipTask : DefaultTask() {
- @Input val filesToVerify = mutableListOf<File>()
-
- /** Whether this build adds automatic constraints between projects in the same group */
- @get:Input val shouldAddGroupConstraints: Provider<Boolean>
-
- init {
- cacheEvenIfNoOutputs()
- shouldAddGroupConstraints = project.shouldAddGroupConstraints()
- }
-
- fun addFile(file: File) {
- filesToVerify.add(file)
- }
-
- fun addCandidate(artifact: Artifact) {
- val groupSubdir = artifact.mavenGroup.replace('.', '/')
- val projectSubdir = File("$groupSubdir/${artifact.projectName}")
- val androidxRepoOut = project.getRepositoryDirectory()
- val fromDir = project.file("$androidxRepoOut/$projectSubdir")
- addFile(File(fromDir, artifact.version))
- }
-
- @TaskAction
- fun execute() {
- verifySettings()
- verifyFiles()
- }
-
- private fun verifySettings() {
- if (!shouldAddGroupConstraints.get() && !isSnapshotBuild()) {
- throw GradleException(
- """
- Cannot publish artifacts without setting -P$ADD_GROUP_CONSTRAINTS=true
-
- This property is required when building artifacts to publish
-
- (but this property can reduce remote cache usage so it is disabled by default)
-
- See AndroidXGradleProperties.kt for more information about this property
- """
- .trimIndent()
- )
- }
- }
-
- private fun verifyFiles() {
- val missingFiles = mutableListOf<String>()
- val emptyDirs = mutableListOf<String>()
- filesToVerify.forEach { file ->
- if (!file.exists()) {
- missingFiles.add(file.path)
- } else {
- if (file.isDirectory) {
- if (file.listFiles().isEmpty()) {
- emptyDirs.add(file.path)
- }
- }
- }
- }
-
- if (missingFiles.isNotEmpty() || emptyDirs.isNotEmpty()) {
- val checkedFilesString = filesToVerify.toString()
- val missingFileString = missingFiles.toString()
- val emptyDirsString = emptyDirs.toString()
- throw FileNotFoundException(
- "GMavenZip ${missingFiles.size} missing files: $missingFileString, " +
- "${emptyDirs.size} empty dirs: $emptyDirsString. " +
- "Checked files: $checkedFilesString"
- )
- }
- }
-}
-
-val AndroidXExtension.publishedArtifacts: List<Artifact>
- get() {
- val groupString = mavenGroup?.group!!
- val versionString = project.version.toString()
- val artifacts =
- mutableListOf(
- Artifact(
- mavenGroup = groupString,
- projectName = project.name,
- version = versionString
- )
- )
-
- // Add platform-specific artifacts, if necessary.
- artifacts +=
- publishPlatforms.map { suffix ->
- Artifact(
- mavenGroup = groupString,
- projectName = "${project.name}-$suffix",
- version = versionString
- )
- }
-
- return artifacts
- }
-
-private val AndroidXExtension.publishPlatforms: List<String>
- get() {
- val potentialTargets =
- project.multiplatformExtension
- ?.targets
- ?.asMap
- ?.filterValues { it.publishable }
- ?.keys
- ?.map {
- it.lowercase()
- // Remove when https://youtrack.jetbrains.com/issue/KT-70072 is fixed.
- // MultiplatformExtension.targets includes `wasmjs` in its list, however,
- // the publication folder for this target is named `wasm-js`. Not having
- // this replace causes the verifyInputscreateProjectZip task to fail
- // as it is looking for a file named wasmjs
- .replace("wasmjs", "wasm-js")
- } ?: emptySet()
- val declaredTargets = potentialTargets.filter { it != "metadata" }
- return declaredTargets.toList()
- }
-
private fun Project.projectZipPrefix(): String {
return "${project.group}-${project.name}"
}
-private fun getZipName(fileNamePrefix: String, mavenGroup: String): String {
- val fileSuffix =
- if (mavenGroup == "") {
- "all"
- } else {
- mavenGroup.split(".").joinToString("-")
- } + "-${getBuildId()}"
- return "$fileNamePrefix-$fileSuffix"
-}
+private fun getZipName(fileNamePrefix: String) = "$fileNamePrefix-all-${getBuildId()}"
fun Project.getProjectZipPath(): String {
return Release.PROJECT_ZIPS_FOLDER +
"/" +
// We pass in a "" because that mimics not passing the group to getParams() inside
// the getProjectZipTask function
- getZipName(projectZipPrefix(), "") +
+ getZipName(projectZipPrefix()) +
"-${project.version}.zip"
}
+
+/**
+ * Strip timestamps from the zip entries to generate consistent output. Set to be ths same as what
+ * Gradle uses:
+ * https://github.com/gradle/gradle/blob/master/platforms/core-runtime/files/src/main/java/org/gradle/api/internal/file/archive/ZipEntryConstants.java
+ */
+private val CONSTANT_TIME_FOR_ZIP_ENTRIES =
+ GregorianCalendar(1980, Calendar.FEBRUARY, 1, 0, 0, 0).timeInMillis
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
index d39c11b..4290ac9 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -252,6 +252,8 @@
// during the task execution, as that breaks configuration caching.
val localVar = archiveOperations
val tempDir = project.layout.buildDirectory.dir("tmp/JvmSources").get().asFile
+ // Get rid of stale files in the directory
+ tempDir.deleteRecursively()
task.into(sourcesDestinationDirectory)
task.from(
pairProvider
@@ -275,6 +277,8 @@
// during the task execution, as that breaks configuration caching.
val localVar = archiveOperations
val tempDir = project.layout.buildDirectory.dir("tmp/SampleSources").get().asFile
+ // Get rid of stale files in the directory
+ tempDir.deleteRecursively()
task.into(samplesDestinationDirectory)
task.from(
pairProvider
@@ -852,6 +856,9 @@
projectLayout.buildDirectory.dir("tmp/MultiplatformSources").get().asFile
val tempSamplesDir =
projectLayout.buildDirectory.dir("tmp/MultiplatformSamples").get().asFile
+ // Get rid of stale files in the directories
+ tempSourcesDir.deleteRecursively()
+ tempSamplesDir.deleteRecursively()
val (sources, samples) =
inputJars
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/java/JavaCompileInputs.kt b/buildSrc/private/src/main/kotlin/androidx/build/java/JavaCompileInputs.kt
index 706eaf9..cb3d5ae 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/java/JavaCompileInputs.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/java/JavaCompileInputs.kt
@@ -13,13 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-@file:Suppress("TYPEALIAS_EXPANSION_DEPRECATION")
package androidx.build.java
-import androidx.build.DeprecatedKotlinMultiplatformAndroidTarget
import androidx.build.getAndroidJar
import androidx.build.multiplatformExtension
+import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.api.variant.LibraryVariant
import org.gradle.api.Project
@@ -134,7 +133,7 @@
}
val target =
kmpExtension.targets
- .withType(DeprecatedKotlinMultiplatformAndroidTarget::class.java)
+ .withType(KotlinMultiplatformAndroidLibraryTarget::class.java)
.single()
val compilation = target.findCompilation(KotlinCompilation.MAIN_COMPILATION_NAME)
val sourceCollection = project.sourceFiles(compilation)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/resources/PublicResourcesStubHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/resources/PublicResourcesStubHelper.kt
index f8a7cb2..bcf9c37 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/resources/PublicResourcesStubHelper.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/resources/PublicResourcesStubHelper.kt
@@ -13,12 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-@file:Suppress("TYPEALIAS_EXPANSION_DEPRECATION")
package androidx.build.resources
-import androidx.build.DeprecatedKotlinMultiplatformAndroidTarget
import androidx.build.getSupportRootFolder
+import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
import com.android.build.api.variant.LibraryVariant
import java.io.File
import org.gradle.api.Project
@@ -48,7 +47,7 @@
}
val sourceSet =
kmpExtension.targets
- .withType(DeprecatedKotlinMultiplatformAndroidTarget::class.java)
+ .withType(KotlinMultiplatformAndroidLibraryTarget::class.java)
.single()
.compilations
.getByName(KotlinCompilation.MAIN_COMPILATION_NAME)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt b/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt
index 1bdf993..46396f1 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt
@@ -18,7 +18,6 @@
import androidx.build.AndroidXPlaygroundRootImplPlugin
import androidx.build.BundleInsideHelper
-import androidx.build.GMavenZipTask
import androidx.build.ProjectLayoutType
import androidx.build.addToBuildOnServer
import androidx.build.getDistributionDirectory
@@ -110,10 +109,6 @@
// klib zip tasks don't generally embed other dependencies in them
return listOf()
}
- if (task is GMavenZipTask) {
- // A GMavenZipTask just zips one or more artifacts we've already built
- return listOf()
- }
val projectPath = path
val taskName = task.name
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/sources/SourceJarTaskHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/sources/SourceJarTaskHelper.kt
index e87ad9e..40f7e20 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/sources/SourceJarTaskHelper.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/sources/SourceJarTaskHelper.kt
@@ -13,11 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-@file:Suppress("TYPEALIAS_EXPANSION_DEPRECATION")
package androidx.build.sources
-import androidx.build.DeprecatedKotlinMultiplatformAndroidTarget
import androidx.build.LazyInputsCopyTask
import androidx.build.capitalize
import androidx.build.dackka.DokkaAnalysisPlatform
@@ -25,6 +23,7 @@
import androidx.build.hasAndroidMultiplatformPlugin
import androidx.build.multiplatformExtension
import androidx.build.registerAsComponentForPublishing
+import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.api.variant.LibraryVariant
import com.google.gson.GsonBuilder
@@ -92,7 +91,7 @@
fun Project.configureMultiplatformSourcesForAndroid(
variantName: String,
- target: DeprecatedKotlinMultiplatformAndroidTarget,
+ target: KotlinMultiplatformAndroidLibraryTarget,
samplesProjects: MutableCollection<Project>
) {
val sourceJar =
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
index 5220b03b..1c9532b 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
@@ -21,13 +21,11 @@
class ConfigBuilder {
lateinit var configName: String
- var appApkName: String? = null
- var appApkSha256: String? = null
- val appSplits = mutableListOf<String>()
+ lateinit var configType: TestConfigType
+ var appApksModel: AppApksModel? = null
lateinit var applicationId: String
var isMicrobenchmark: Boolean = false
var isMacrobenchmark: Boolean = false
- var enablePrivacySandbox: Boolean = false
var isPostsubmit: Boolean = true
lateinit var minSdk: String
val tags = mutableListOf<String>()
@@ -35,16 +33,13 @@
lateinit var testApkSha256: String
lateinit var testRunner: String
val additionalApkKeys = mutableListOf<String>()
- val initialSetupApks = mutableListOf<String>()
val instrumentationArgsMap = mutableMapOf<String, String>()
fun configName(configName: String) = apply { this.configName = configName }
- fun appApkName(appApkName: String) = apply { this.appApkName = appApkName }
+ fun configType(configType: TestConfigType) = apply { this.configType = configType }
- fun appApkSha256(appApkSha256: String) = apply { this.appApkSha256 = appApkSha256 }
-
- fun appSplits(appSplits: List<String>) = apply { this.appSplits.addAll(appSplits) }
+ fun appApksModel(appApksModel: AppApksModel) = apply { this.appApksModel = appApksModel }
fun applicationId(applicationId: String) = apply { this.applicationId = applicationId }
@@ -58,18 +53,12 @@
fun isPostsubmit(isPostsubmit: Boolean) = apply { this.isPostsubmit = isPostsubmit }
- fun enablePrivacySandbox(enablePrivacySandbox: Boolean) = apply {
- this.enablePrivacySandbox = enablePrivacySandbox
- }
-
fun minSdk(minSdk: String) = apply { this.minSdk = minSdk }
fun tag(tag: String) = apply { this.tags.add(tag) }
fun additionalApkKeys(keys: List<String>) = apply { additionalApkKeys.addAll(keys) }
- fun initialSetupApks(apks: List<String>) = apply { initialSetupApks.addAll(apks) }
-
fun testApkName(testApkName: String) = apply { this.testApkName = testApkName }
fun testApkSha256(testApkSha256: String) = apply { this.testApkSha256 = testApkSha256 }
@@ -92,6 +81,12 @@
listOf(InstrumentationArg("notAnnotation", "androidx.test.filters.FlakyTest"))
}
)
+ if (configType.isAddedToInstrumentationArgs()) {
+ instrumentationArgsList.add(
+ InstrumentationArg("androidx.testConfigType", configType.toString())
+ )
+ }
+ val appApk = singleAppApk()
val values =
mapOf(
"name" to configName,
@@ -99,8 +94,8 @@
"testSuiteTags" to tags,
"testApk" to testApkName,
"testApkSha256" to testApkSha256,
- "appApk" to appApkName,
- "appApkSha256" to appApkSha256,
+ "appApk" to appApk?.name,
+ "appApkSha256" to appApk?.sha256,
"instrumentationArgs" to instrumentationArgsList,
"additionalApkKeys" to additionalApkKeys
)
@@ -132,7 +127,7 @@
listOf(
InstrumentationArg(
"androidx.benchmark.output.payload.appApkSha256",
- checkNotNull(appApkSha256) {
+ checkNotNull(appApksModel?.sha256()) {
"app apk sha should be provided for macrobenchmarks."
}
),
@@ -142,6 +137,11 @@
)
}
}
+ if (configType.isAddedToInstrumentationArgs()) {
+ instrumentationArgsList.add(
+ InstrumentationArg("androidx.testConfigType", configType.toString())
+ )
+ }
instrumentationArgsList.forEach { (key, value) ->
sb.append(
"""
@@ -152,14 +152,13 @@
)
}
sb.append(SETUP_INCLUDE).append(TARGET_PREPARER_OPEN.replace("CLEANUP_APKS", "true"))
- initialSetupApks.forEach { apk -> sb.append(APK_INSTALL_OPTION.replace("APK_NAME", apk)) }
sb.append(APK_INSTALL_OPTION.replace("APK_NAME", testApkName))
- if (!appApkName.isNullOrEmpty()) {
- if (appSplits.isEmpty()) {
- sb.append(APK_INSTALL_OPTION.replace("APK_NAME", appApkName!!))
- } else {
- val apkList = appApkName + "," + appSplits.joinToString(",")
+ appApksModel?.apkGroups?.forEach { group ->
+ if (group.isUsingApkSplits()) {
+ val apkList = group.apks.map(ApkFile::name).joinToString(",")
sb.append(APK_WITH_SPLITS_INSTALL_OPTION.replace("APK_LIST", apkList))
+ } else {
+ sb.append(APK_INSTALL_OPTION.replace("APK_NAME", group.apks.single().name))
}
}
sb.append(TARGET_PREPARER_CLOSE)
@@ -167,7 +166,7 @@
if (isMicrobenchmark) {
sb.append(benchmarkPostInstallCommandOption(applicationId))
}
- if (enablePrivacySandbox) {
+ if (configType == TestConfigType.PRIVACY_SANDBOX_MAIN) {
sb.append(PRIVACY_SANDBOX_ENABLE_PREPARER)
}
sb.append(TEST_BLOCK_OPEN)
@@ -187,6 +186,14 @@
sb.append(CONFIGURATION_CLOSE)
return sb.toString()
}
+
+ private fun singleAppApk(): ApkFile? {
+ val apkGroups = appApksModel?.apkGroups
+ if (apkGroups.isNullOrEmpty()) {
+ return null
+ }
+ return apkGroups.single().apks.single()
+ }
}
private fun mediaInstrumentationArgsForJson(
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AppApksModel.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AppApksModel.kt
new file mode 100644
index 0000000..52867c9
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AppApksModel.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.build.testConfiguration
+
+import com.google.common.hash.Hashing
+import com.google.common.io.BaseEncoding
+import com.google.gson.GsonBuilder
+
+/**
+ * List of App APKs required to install the app.
+ *
+ * APKs must be installed group by group in same order as listed.
+ */
+data class AppApksModel(val apkGroups: List<ApkFileGroup>) {
+
+ @Suppress("UnstableApiUsage") // guava Hashing is marked as @Beta
+ fun sha256(): String? {
+ val shaHashes = apkGroups.flatMap(ApkFileGroup::apks).map(ApkFile::sha256)
+ if (shaHashes.isEmpty()) {
+ return null
+ }
+
+ if (shaHashes.size == 1) {
+ return shaHashes[0]
+ }
+
+ val hasher = Hashing.sha256().newHasher()
+ shaHashes.forEach { hasher.putString(it, Charsets.UTF_8) }
+ return BaseEncoding.base16().lowerCase().encode(hasher.hash().asBytes())
+ }
+
+ fun toJson(): String = GsonBuilder().setPrettyPrinting().create().toJson(this)
+
+ companion object {
+ fun fromJson(json: String): AppApksModel =
+ GsonBuilder().create().fromJson(json, AppApksModel::class.java)
+ }
+}
+
+/** Group of APKs / Splits that needs to be installed together. */
+data class ApkFileGroup(val apks: List<ApkFile>) {
+ fun isUsingApkSplits(): Boolean = apks.size > 1
+}
+
+/** Single APK / Split file */
+data class ApkFile(val name: String, val sha256: String = "")
+
+internal fun singleFileAppApksModel(name: String, sha256: String = ""): AppApksModel =
+ AppApksModel(apkGroups = listOf(singleFileApkFileGroup(name, sha256)))
+
+internal fun singleFileApkFileGroup(name: String, sha256: String = ""): ApkFileGroup =
+ ApkFileGroup(apks = listOf(ApkFile(name, sha256)))
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AppApksTestConfigurationHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AppApksTestConfigurationHelper.kt
new file mode 100644
index 0000000..7060aae
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AppApksTestConfigurationHelper.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.build.testConfiguration
+
+import androidx.build.AndroidXImplPlugin.Companion.SUPPORTED_BUILD_ABIS
+import androidx.build.androidXExtension
+import androidx.build.asFilenamePrefix
+import androidx.build.dependencyTracker.AffectedModuleDetector
+import androidx.build.getAppApksFilesDirectory
+import com.android.build.api.variant.ApkOutputProviders
+import com.android.build.api.variant.DeviceSpec
+import com.android.build.api.variant.Variant
+import java.util.function.Consumer
+import kotlin.math.max
+import org.gradle.api.Project
+import org.gradle.api.file.RegularFile
+import org.gradle.api.provider.Provider
+
+@Suppress("UnstableApiUsage") // ApkOutputProviders
+internal fun Project.registerCopyPrivacySandboxMainAppApksTask(
+ variant: Variant,
+ outputProviders: ApkOutputProviders,
+ excludeTestApk: Provider<RegularFile>
+): Provider<CopyApksFromOutputProviderTask> =
+ registerCopyApksFromOutputProviderTask(
+ taskName = "CopyPrivacySandboxMainAppApks${variant.name}",
+ variant,
+ outputProviders,
+ excludeTestApk,
+ outputFileNamePrefix = "${path.asFilenamePrefix()}-${variant.name}-sandbox-enabled",
+ outputApkModelFileName = "PrivacySandboxMainAppApksModel${variant.name}.json",
+ deviceSpec = mainSandboxDeviceSpec(variant.minSdk.apiLevel)
+ )
+
+@Suppress("UnstableApiUsage") // ApkOutputProviders
+internal fun Project.registerCopyPrivacySandboxCompatAppApksTask(
+ variant: Variant,
+ outputProviders: ApkOutputProviders,
+ excludeTestApk: Provider<RegularFile>
+): Provider<CopyApksFromOutputProviderTask> =
+ registerCopyApksFromOutputProviderTask(
+ taskName = "CopyPrivacySandboxCompatAppApks${variant.name}",
+ variant,
+ outputProviders,
+ excludeTestApk,
+ outputFileNamePrefix = "${path.asFilenamePrefix()}-${variant.name}-sandbox-compat",
+ outputApkModelFileName = "PrivacySandboxCompatAppApksModel${variant.name}.json",
+ deviceSpec = compatSandboxDeviceSpec(variant.minSdk.apiLevel)
+ )
+
+internal fun Project.registerCopyAppApkFromArtifactsTask(
+ variant: Variant,
+ configureAction: Consumer<CopyApkFromArtifactsTask> // For lazy task init
+): Provider<CopyApkFromArtifactsTask> =
+ tasks.register("CopyAppApk${variant.name}", CopyApkFromArtifactsTask::class.java) { task ->
+ task.appLoader.set(variant.artifacts.getBuiltArtifactsLoader())
+
+ configureAction.accept(task)
+
+ task.outputAppApksModel.set(layout.buildDirectory.file("AppApksModel${variant.name}.json"))
+
+ task.androidTestSourceCode.from(getTestSourceSetsForAndroid(variant))
+ task.enabled = androidXExtension.deviceTests.enabled
+ AffectedModuleDetector.configureTaskGuard(task)
+ }
+
+@Suppress("UnstableApiUsage") // ApkOutputProviders
+private fun Project.registerCopyApksFromOutputProviderTask(
+ taskName: String,
+ variant: Variant,
+ outputProviders: ApkOutputProviders,
+ excludeTestApk: Provider<RegularFile>,
+ outputFileNamePrefix: String,
+ outputApkModelFileName: String,
+ deviceSpec: DeviceSpec
+): Provider<CopyApksFromOutputProviderTask> {
+ val copyApksTask =
+ tasks.register(taskName, CopyApksFromOutputProviderTask::class.java) { task ->
+ task.excludeTestApk.set(excludeTestApk)
+
+ task.outputFilenamesPrefix.set(outputFileNamePrefix)
+ task.outputDirectory.set(
+ getAppApksFilesDirectory().map { it.dir("${path.asFilenamePrefix()}-$taskName") }
+ )
+
+ task.outputAppApksModel.set(layout.buildDirectory.file(outputApkModelFileName))
+
+ task.androidTestSourceCode.from(getTestSourceSetsForAndroid(variant))
+ task.enabled = androidXExtension.deviceTests.enabled
+ AffectedModuleDetector.configureTaskGuard(task)
+ }
+
+ outputProviders.provideApkOutputToTask(
+ copyApksTask,
+ CopyApksFromOutputProviderTask::apkOutput,
+ deviceSpec
+ )
+
+ return copyApksTask
+}
+
+@Suppress("UnstableApiUsage") // DeviceSpec
+private fun mainSandboxDeviceSpec(minApiLevel: Int): DeviceSpec =
+ DeviceSpec.Builder()
+ .setApiLevel(max(minApiLevel, PRIVACY_SANDBOX_MIN_API_LEVEL))
+ .setSupportsPrivacySandbox(true)
+ .setAbis(SUPPORTED_BUILD_ABIS) // To pass filters in defaultConfig.ndk.abiFilters
+ .build()
+
+@Suppress("UnstableApiUsage") // DeviceSpec
+private fun compatSandboxDeviceSpec(minApiLevel: Int): DeviceSpec =
+ DeviceSpec.Builder()
+ .setApiLevel(minApiLevel)
+ .setSupportsPrivacySandbox(false)
+ .setAbis(SUPPORTED_BUILD_ABIS) // To pass filters in defaultConfig.ndk.abiFilters
+ .build()
+
+internal const val PRIVACY_SANDBOX_MIN_API_LEVEL = 34
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/CopyApkFromArtifactsTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/CopyApkFromArtifactsTask.kt
new file mode 100644
index 0000000..f707929
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/CopyApkFromArtifactsTask.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.build.testConfiguration
+
+import com.android.build.api.variant.BuiltArtifactsLoader
+import java.io.File
+import javax.inject.Inject
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.SkipWhenEmpty
+import org.gradle.api.tasks.TaskAction
+import org.gradle.work.DisableCachingByDefault
+
+/** Copy single APK (from AGP artifacts) needed for building androidTest.zip */
+@DisableCachingByDefault(because = "Only filesystem operations")
+abstract class CopyApkFromArtifactsTask @Inject constructor(private val objects: ObjectFactory) :
+ DefaultTask() {
+
+ /** File existence check to determine whether to run this task. */
+ @get:InputFiles
+ @get:SkipWhenEmpty
+ @get:PathSensitive(PathSensitivity.NONE)
+ abstract val androidTestSourceCode: ConfigurableFileCollection
+
+ @get:InputFiles
+ @get:Optional
+ @get:PathSensitive(PathSensitivity.NONE)
+ abstract val appFolder: DirectoryProperty
+
+ @get:InputFiles
+ @get:PathSensitive(PathSensitivity.NONE)
+ abstract val appFileCollection: ConfigurableFileCollection
+
+ @get:Internal abstract val appLoader: Property<BuiltArtifactsLoader>
+
+ @get:OutputFile abstract val outputAppApk: RegularFileProperty
+
+ @get:OutputFile abstract val outputAppApksModel: RegularFileProperty
+
+ @TaskAction
+ fun createApks() {
+ // Decides where to load the app apk from, depending on whether appFolder or
+ // appFileCollection has been set.
+ val appDir =
+ if (appFolder.isPresent && appFileCollection.files.isEmpty()) {
+ appFolder.get()
+ } else if (!appFolder.isPresent && appFileCollection.files.size == 1) {
+ objects.directoryProperty().also { it.set(appFileCollection.files.first()) }.get()
+ } else {
+ throw IllegalStateException(
+ """
+ App apk not specified or both appFileCollection and appFolder specified.
+ """
+ .trimIndent()
+ )
+ }
+
+ val appApk =
+ appLoader.get().load(appDir)
+ ?: throw RuntimeException("Cannot load required APK for task: $name")
+
+ val appApkBuiltArtifact = appApk.elements.single()
+ val destinationApk = outputAppApk.get().asFile
+ File(appApkBuiltArtifact.outputFile).copyTo(destinationApk, overwrite = true)
+
+ val model =
+ singleFileAppApksModel(name = destinationApk.name, sha256 = sha256(destinationApk))
+ outputAppApksModel.get().asFile.writeText(model.toJson())
+ }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/CopyApksFromOutputProviderTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/CopyApksFromOutputProviderTask.kt
new file mode 100644
index 0000000..1898a53
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/CopyApksFromOutputProviderTask.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.build.testConfiguration
+
+import com.android.build.api.variant.ApkInstallGroup
+import com.android.build.api.variant.ApkOutput
+import javax.inject.Inject
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.FileSystemOperations
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.SkipWhenEmpty
+import org.gradle.api.tasks.TaskAction
+import org.gradle.work.DisableCachingByDefault
+
+/** Copy APKs (from ApkOutputProviders) needed for building androidTest.zip */
+@Suppress("UnstableApiUsage") // Working with ApkOutputProviders
+@DisableCachingByDefault(because = "Only filesystem operations")
+abstract class CopyApksFromOutputProviderTask
+@Inject
+constructor(private val fileSystemOperations: FileSystemOperations) : DefaultTask() {
+
+ /** File existence check to determine whether to run this task. */
+ @get:InputFiles
+ @get:SkipWhenEmpty
+ @get:PathSensitive(PathSensitivity.NONE)
+ abstract val androidTestSourceCode: ConfigurableFileCollection
+
+ /** ApkOutputProviders output, contains all App APKs. */
+ @get:Internal abstract val apkOutput: Property<ApkOutput>
+
+ /** Some Variants includes test APK into ApkOutput, excluding it by using this parameter. */
+ @get:InputFile
+ @get:PathSensitive(PathSensitivity.NONE)
+ abstract val excludeTestApk: RegularFileProperty
+
+ /**
+ * Filename prefix for all output apks. Required for producing unique filenames over all
+ * projects.
+ *
+ * Resulting filename: <outputFilenamesPrefix>-<groupIndex>-<fileIndex>-<originalFileName>
+ */
+ @get:Input abstract val outputFilenamesPrefix: Property<String>
+
+ @get:OutputDirectory abstract val outputDirectory: DirectoryProperty
+
+ @get:OutputFile abstract val outputAppApksModel: RegularFileProperty
+
+ @TaskAction
+ fun createApks() {
+ val outputDir = outputDirectory.get()
+
+ // Cleanup old files - to remove stale APKs
+ fileSystemOperations.delete { it.delete(outputDir) }
+
+ val fileNamePrefix = outputFilenamesPrefix.get()
+ val testApkSha256 = sha256(excludeTestApk.get().asFile)
+
+ val resultApkGroups =
+ apkOutput.get().apkInstallGroups.mapIndexedNotNull { groupIndex, installGroup ->
+ processApkInstallGroup(
+ installGroup,
+ testApkSha256,
+ fileNamePrefix = "$fileNamePrefix-$groupIndex"
+ )
+ }
+
+ val model = AppApksModel(resultApkGroups)
+ outputAppApksModel.get().asFile.writeText(model.toJson())
+ }
+
+ private fun processApkInstallGroup(
+ installGroup: ApkInstallGroup,
+ excludeSha256: String,
+ fileNamePrefix: String
+ ): ApkFileGroup? {
+ val outputDir = outputDirectory.get()
+ val resultApkFiles =
+ installGroup.apks.mapIndexedNotNull { fileIndex, file ->
+ val inputFile = file.asFile
+ val fileSha256 = sha256(inputFile)
+ if (fileSha256 == excludeSha256) {
+ // Some Variants includes test APK into ApkOutput, filter it.
+ return@mapIndexedNotNull null
+ }
+
+ val outputFileName = "$fileNamePrefix-$fileIndex-${inputFile.name}"
+ val outputFile = outputDir.file(outputFileName).asFile
+
+ inputFile.copyTo(outputFile, overwrite = true)
+
+ return@mapIndexedNotNull ApkFile(name = outputFileName, sha256 = fileSha256)
+ }
+ return if (resultApkFiles.isEmpty()) {
+ null
+ } else {
+ ApkFileGroup(resultApkFiles)
+ }
+ }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/CopyTestApksTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/CopyTestApksTask.kt
index ffa92365..16986a0 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/CopyTestApksTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/CopyTestApksTask.kt
@@ -18,18 +18,13 @@
import com.android.build.api.variant.BuiltArtifactsLoader
import java.io.File
-import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
-import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
-import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
-import org.gradle.api.tasks.Optional
-import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
@@ -37,10 +32,14 @@
import org.gradle.api.tasks.TaskAction
import org.gradle.work.DisableCachingByDefault
-/** Copy APKs needed for building androidTest.zip */
+/**
+ * Copy test APK (androidTest or standalone test) needed for building androidTest.zip
+ *
+ * If test requires instrumented app apks, they will be copied separately in either
+ * [CopyApkFromArtifactsTask] or [CopyApksFromOutputProviderTask] depending on project type.
+ */
@DisableCachingByDefault(because = "Only filesystem operations")
-abstract class CopyTestApksTask @Inject constructor(private val objects: ObjectFactory) :
- DefaultTask() {
+abstract class CopyTestApksTask : DefaultTask() {
/** File existence check to determine whether to run this task. */
@get:InputFiles
@@ -52,121 +51,14 @@
@get:PathSensitive(PathSensitivity.NONE)
abstract val testFolder: DirectoryProperty
- @get:InputFiles
- @get:Optional
- @get:PathSensitive(PathSensitivity.NONE)
- abstract val appFolder: DirectoryProperty
-
- @get:InputFiles
- @get:PathSensitive(PathSensitivity.NONE)
- abstract val appFileCollection: ConfigurableFileCollection
-
- /**
- * Extracted APKs for PrivacySandbox SDKs dependencies. Produced by AGP.
- *
- * Should be set only for applications with PrivacySandbox SDKs dependencies.
- */
- @get:InputFiles
- @get:Optional
- @get:PathSensitive(PathSensitivity.RELATIVE) // We use parent folder for file name generation
- abstract val privacySandboxSdkApks: ConfigurableFileCollection
-
- /**
- * Extracted split with manifest containing <uses-sdk-library> tag. Produced by AGP.
- *
- * Should be set only for applications with PrivacySandbox SDKs dependencies.
- */
- @get:InputFiles
- @get:Optional
- @get:PathSensitive(PathSensitivity.NAME_ONLY)
- abstract val privacySandboxUsesSdkSplit: ConfigurableFileCollection
-
- /**
- * Extracted compat splits for PrivacySandbox SDKs dependencies. Produced by AGP.
- *
- * Should be set only for applications with PrivacySandbox SDKs dependencies.
- */
- @get:InputFiles
- @get:Optional
- @get:PathSensitive(PathSensitivity.NAME_ONLY)
- abstract val privacySandboxSdkCompatSplits: ConfigurableFileCollection
-
- /**
- * Filename prefix for all PrivacySandbox related output files. Required for producing unique
- * filenames over all projects.
- *
- * Should be set only for applications with PrivacySandbox SDKs dependencies.
- */
- @get:Input @get:Optional abstract val filenamePrefixForPrivacySandboxFiles: Property<String>
-
- @get:Internal abstract val appLoader: Property<BuiltArtifactsLoader>
-
@get:Internal abstract val testLoader: Property<BuiltArtifactsLoader>
@get:OutputFile abstract val outputApplicationId: RegularFileProperty
@get:OutputFile abstract val outputTestApk: RegularFileProperty
- @get:[OutputFile Optional]
- abstract val outputAppApk: RegularFileProperty
-
- /**
- * Output directory for PrivacySandbox SDKs APKs.
- *
- * Should be set only for applications with PrivacySandbox SDKs dependencies.
- */
- @get:[OutputDirectory Optional]
- abstract val outputPrivacySandboxSdkApks: DirectoryProperty
-
- /**
- * Output directory for App splits required for devices with PrivacySandbox support.
- *
- * Should be set only for applications with PrivacySandbox SDKs dependencies.
- */
- @get:[OutputDirectory Optional]
- abstract val outputPrivacySandboxAppSplits: DirectoryProperty
-
- /**
- * Output directory for App splits required for devices without PrivacySandbox support.
- *
- * Should be set only for applications with PrivacySandbox SDKs dependencies.
- */
- @get:[OutputDirectory Optional]
- abstract val outputPrivacySandboxCompatAppSplits: DirectoryProperty
-
@TaskAction
fun createApks() {
- if (appLoader.isPresent) {
- // Decides where to load the app apk from, depending on whether appFolder or
- // appFileCollection has been set.
- val appDir =
- if (appFolder.isPresent && appFileCollection.files.isEmpty()) {
- appFolder.get()
- } else if (!appFolder.isPresent && appFileCollection.files.size == 1) {
- objects
- .directoryProperty()
- .also { it.set(appFileCollection.files.first()) }
- .get()
- } else {
- throw IllegalStateException(
- """
- App apk not specified or both appFileCollection and appFolder specified.
- """
- .trimIndent()
- )
- }
-
- val appApk =
- appLoader.get().load(appDir)
- ?: throw RuntimeException("Cannot load required APK for task: $name")
- // We don't need to check hasBenchmarkPlugin because benchmarks shouldn't have test apps
- val appApkBuiltArtifact = appApk.elements.single()
- val destinationApk = outputAppApk.get().asFile
- File(appApkBuiltArtifact.outputFile).copyTo(destinationApk, overwrite = true)
-
- createPrivacySandboxFiles()
- }
-
val testApk =
testLoader.get().load(testFolder.get())
?: throw RuntimeException("Cannot load required APK for task: $name")
@@ -175,42 +67,6 @@
File(testApkBuiltArtifact.outputFile).copyTo(destinationApk, overwrite = true)
val outputApplicationIdFile = outputApplicationId.get().asFile
- outputApplicationIdFile.bufferedWriter().use { out -> out.write(testApk.applicationId) }
- }
-
- /**
- * Creates APKs required for running App with PrivacySandbox SDKs. Do nothing if project doesn't
- * have dependencies on PrivacySandbox SDKs.
- */
- private fun createPrivacySandboxFiles() {
- if (privacySandboxSdkApks.isEmpty) {
- return
- }
-
- val prefix = filenamePrefixForPrivacySandboxFiles.get()
-
- privacySandboxSdkApks.asFileTree.map { sdkApk ->
- // TODO (b/309610890): Remove after supporting unique filenames on bundletool side.
- val sdkProjectName = sdkApk.parentFile?.name
- val outputFileName = "$prefix-$sdkProjectName-${sdkApk.name}"
- val outputFile = outputPrivacySandboxSdkApks.get().file(outputFileName)
- sdkApk.copyTo(outputFile.asFile, overwrite = true)
- }
-
- val usesSdkSplitArtifact =
- appLoader.get().load(privacySandboxUsesSdkSplit)?.elements?.single()
- if (usesSdkSplitArtifact != null) {
- val splitApk = File(usesSdkSplitArtifact.outputFile)
- val outputFileName = "$prefix-${splitApk.name}"
- val outputFile = outputPrivacySandboxAppSplits.get().file(outputFileName)
- splitApk.copyTo(outputFile.asFile, overwrite = true)
- }
-
- appLoader.get().load(privacySandboxSdkCompatSplits)?.elements?.forEach { splitArtifact ->
- val splitApk = File(splitArtifact.outputFile)
- val outputFileName = "$prefix-${splitApk.name}"
- val outputFile = outputPrivacySandboxCompatAppSplits.get().file(outputFileName)
- splitApk.copyTo(outputFile.asFile, overwrite = true)
- }
+ outputApplicationIdFile.writeText(testApk.applicationId)
}
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
index 00bd055..49cade8 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
@@ -45,10 +45,13 @@
@DisableCachingByDefault(because = "Doesn't benefit from caching")
abstract class GenerateTestConfigurationTask : DefaultTask() {
+ @get:Input abstract val testConfigType: Property<TestConfigType>
+
+ /** File containing [AppApksModel] with list of App APKs to install */
@get:InputFile
@get:Optional
- @get:PathSensitive(PathSensitivity.NAME_ONLY)
- abstract val appApk: RegularFileProperty
+ @get:PathSensitive(PathSensitivity.NONE)
+ abstract val appApksModel: RegularFileProperty
/** File existence check to determine whether to run this task. */
@get:InputFiles
@@ -56,26 +59,6 @@
@get:PathSensitive(PathSensitivity.NONE)
abstract val androidTestSourceCodeCollection: ConfigurableFileCollection
- /**
- * Extracted APKs for PrivacySandbox SDKs dependencies. Produced by AGP.
- *
- * Should be set only for applications with PrivacySandbox SDKs dependencies.
- */
- @get:InputFiles
- @get:Optional
- @get:PathSensitive(PathSensitivity.NAME_ONLY)
- abstract val privacySandboxSdkApks: ConfigurableFileCollection
-
- /**
- * Extracted splits required for running app with PrivacySandbox SDKs. Produced by AGP.
- *
- * Should be set only for applications with PrivacySandbox SDKs dependencies.
- */
- @get:InputFiles
- @get:Optional
- @get:PathSensitive(PathSensitivity.NAME_ONLY)
- abstract val privacySandboxAppSplits: ConfigurableFileCollection
-
@get:InputFile
@get:PathSensitive(PathSensitivity.NAME_ONLY)
abstract val testApk: RegularFileProperty
@@ -117,22 +100,14 @@
configurations testing Android Application projects, so that both APKs get installed.
*/
val configBuilder = ConfigBuilder()
- configBuilder.configName = outputXml.asFile.get().name
- if (appApk.isPresent) {
- val appApkFile = appApk.get().asFile
- configBuilder.appApkName(appApkFile.name).appApkSha256(sha256(appApkFile))
+ configBuilder.configName(outputXml.asFile.get().name)
+ configBuilder.configType(testConfigType.get())
+ if (appApksModel.isPresent) {
+ val modelJson = appApksModel.get().asFile.readText()
+ val model = AppApksModel.fromJson(modelJson)
+ configBuilder.appApksModel(model)
}
- val privacySandboxSdkApksFileNames =
- privacySandboxSdkApks.asFileTree.map { f -> f.name }.sorted()
- if (privacySandboxSdkApksFileNames.isNotEmpty()) {
- configBuilder.enablePrivacySandbox(true)
- configBuilder.initialSetupApks(privacySandboxSdkApksFileNames)
- }
- val privacySandboxSplitsFileNames =
- privacySandboxAppSplits.asFileTree.map { f -> f.name }.sorted()
- configBuilder.appSplits(privacySandboxSplitsFileNames)
-
configBuilder.additionalApkKeys(additionalApkKeys.get())
val isPresubmit = presubmit.get()
configBuilder.isPostsubmit(!isPresubmit)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestConfigType.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestConfigType.kt
new file mode 100644
index 0000000..629dde3
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestConfigType.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.build.testConfiguration
+
+enum class TestConfigType {
+ /** Default test config type for all non privacy sandbox projects. */
+ DEFAULT,
+
+ /** Main test config type for privacy sandbox projects. SDK installed as separate package. */
+ PRIVACY_SANDBOX_MAIN,
+
+ /** Compat test config type for privacy sandbox projects. SDK installed as app split. */
+ PRIVACY_SANDBOX_COMPAT;
+
+ fun isAddedToInstrumentationArgs(): Boolean = this != DEFAULT
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSourceSetsHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSourceSetsHelper.kt
new file mode 100644
index 0000000..3cf5bb4
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSourceSetsHelper.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.build.testConfiguration
+
+import androidx.build.multiplatformExtension
+import com.android.build.api.variant.TestVariant
+import com.android.build.api.variant.Variant
+import org.gradle.api.Project
+import org.gradle.api.file.FileCollection
+import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
+import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
+
+internal fun Project.getTestSourceSetsForAndroid(variant: Variant?): List<FileCollection> {
+ val testSourceFileCollections = mutableListOf<FileCollection>()
+ when (variant) {
+ is TestVariant -> {
+ // com.android.test modules keep test code in main sourceset
+ variant.sources.java?.all?.let { sourceSet ->
+ testSourceFileCollections.add(files(sourceSet))
+ }
+ // Add kotlin-android main source set
+ extensions
+ .findByType(KotlinAndroidProjectExtension::class.java)
+ ?.sourceSets
+ ?.find { it.name == "main" }
+ ?.let { testSourceFileCollections.add(it.kotlin.sourceDirectories) }
+ // Note, don't have to add kotlin-multiplatform as it is not compatible with
+ // com.android.test modules
+ }
+ is com.android.build.api.variant.HasAndroidTest -> {
+ variant.androidTest?.sources?.java?.all?.let {
+ testSourceFileCollections.add(files(it))
+ }
+ }
+ }
+
+ // Add kotlin-android androidTest source set
+ extensions
+ .findByType(KotlinAndroidProjectExtension::class.java)
+ ?.sourceSets
+ ?.find { it.name == "androidTest" }
+ ?.let { testSourceFileCollections.add(it.kotlin.sourceDirectories) }
+
+ // Add kotlin-multiplatform androidInstrumentedTest target source sets
+ multiplatformExtension
+ ?.targets
+ ?.filterIsInstance<KotlinAndroidTarget>()
+ ?.mapNotNull { it.compilations.find { it.name == "releaseAndroidTest" } }
+ ?.flatMap { it.allKotlinSourceSets }
+ ?.mapTo(testSourceFileCollections) { it.kotlin.sourceDirectories }
+ return testSourceFileCollections
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index 498167c..32568c2 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -13,49 +13,43 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-@file:Suppress("TYPEALIAS_EXPANSION_DEPRECATION")
package androidx.build.testConfiguration
import androidx.build.AndroidXExtension
import androidx.build.AndroidXImplPlugin.Companion.FINALIZE_TEST_CONFIGS_WITH_APKS_TASK
-import androidx.build.DeprecatedKotlinMultiplatformAndroidTarget
import androidx.build.androidXExtension
import androidx.build.asFilenamePrefix
import androidx.build.dependencyTracker.AffectedModuleDetector
import androidx.build.getFileInTestConfigDirectory
-import androidx.build.getPrivacySandboxFilesDirectory
import androidx.build.hasBenchmarkPlugin
import androidx.build.isMacrobenchmark
import androidx.build.isPresubmitBuild
-import androidx.build.multiplatformExtension
import com.android.build.api.artifact.Artifacts
import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.attributes.BuildTypeAttr
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.CommonExtension
+import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
import com.android.build.api.dsl.TestExtension
import com.android.build.api.variant.AndroidComponentsExtension
+import com.android.build.api.variant.ApkOutputProviders
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.HasDeviceTests
import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.api.variant.TestAndroidComponentsExtension
-import com.android.build.api.variant.TestVariant
import com.android.build.api.variant.Variant
+import java.util.function.Consumer
import kotlin.math.max
import org.gradle.api.Project
import org.gradle.api.artifacts.type.ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE
import org.gradle.api.attributes.Usage
-import org.gradle.api.file.Directory
-import org.gradle.api.file.FileCollection
import org.gradle.api.file.RegularFile
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.TaskProvider
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.named
-import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
-import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
/**
* Creates and configures the test config generation task for a project. Configuration includes
@@ -80,13 +74,11 @@
*/
registerGenerateTestConfigurationTask(
"${GENERATE_PRIVACY_SANDBOX_MAIN_TEST_CONFIGURATION_TASK}$variantName",
+ TestConfigType.PRIVACY_SANDBOX_MAIN,
xmlName = "${path.asFilenamePrefix()}$variantName.xml",
jsonName = null, // Privacy sandbox not yet supported in JSON configs
copyTestApksTask.flatMap { it.outputApplicationId },
- copyTestApksTask.flatMap { it.outputAppApk },
copyTestApksTask.flatMap { it.outputTestApk },
- copyTestApksTask.flatMap { it.outputPrivacySandboxSdkApks },
- copyTestApksTask.flatMap { it.outputPrivacySandboxAppSplits },
minSdk = max(minSdk, PRIVACY_SANDBOX_MIN_API_LEVEL),
testRunner,
instrumentationRunnerArgs,
@@ -96,13 +88,11 @@
registerGenerateTestConfigurationTask(
"${GENERATE_PRIVACY_SANDBOX_COMPAT_TEST_CONFIGURATION_TASK}${variantName}",
+ TestConfigType.PRIVACY_SANDBOX_COMPAT,
xmlName = "${path.asFilenamePrefix()}${variantName}Compat.xml",
jsonName = null, // Privacy sandbox not yet supported in JSON configs
copyTestApksTask.flatMap { it.outputApplicationId },
- copyTestApksTask.flatMap { it.outputAppApk },
copyTestApksTask.flatMap { it.outputTestApk },
- privacySandboxApks = null,
- copyTestApksTask.flatMap { it.outputPrivacySandboxCompatAppSplits },
minSdk,
testRunner,
instrumentationRunnerArgs,
@@ -112,13 +102,11 @@
} else {
registerGenerateTestConfigurationTask(
"${GENERATE_TEST_CONFIGURATION_TASK}$variantName",
+ TestConfigType.DEFAULT,
xmlName = "${path.asFilenamePrefix()}$variantName.xml",
jsonName = "_${path.asFilenamePrefix()}$variantName.json",
copyTestApksTask.flatMap { it.outputApplicationId },
- copyTestApksTask.flatMap { it.outputAppApk },
copyTestApksTask.flatMap { it.outputTestApk },
- privacySandboxApks = null,
- privacySandboxSplits = null,
minSdk,
testRunner,
instrumentationRunnerArgs,
@@ -154,13 +142,11 @@
private fun Project.registerGenerateTestConfigurationTask(
taskName: String,
+ configType: TestConfigType,
xmlName: String,
jsonName: String?,
applicationIdFile: Provider<RegularFile>,
- appApk: Provider<RegularFile>,
testApk: Provider<RegularFile>,
- privacySandboxApks: Provider<Directory>?,
- privacySandboxSplits: Provider<Directory>?,
minSdk: Int,
testRunner: Provider<String>,
instrumentationRunnerArgs: Provider<Map<String, String>>,
@@ -169,12 +155,10 @@
) {
val generateTestConfigurationTask =
tasks.register(taskName, GenerateTestConfigurationTask::class.java) { task ->
- task.applicationId.set(project.providers.fileContents(applicationIdFile).asText)
- task.appApk.set(appApk)
- task.testApk.set(testApk)
+ task.testConfigType.set(configType)
- privacySandboxApks?.let { task.privacySandboxSdkApks.from(it) }
- privacySandboxSplits?.let { task.privacySandboxAppSplits.from(it) }
+ task.applicationId.set(project.providers.fileContents(applicationIdFile).asText)
+ task.testApk.set(testApk)
val androidXExtension = extensions.getByType<AndroidXExtension>()
task.additionalApkKeys.set(androidXExtension.additionalDeviceTestApkKeys)
@@ -222,65 +206,28 @@
return getFileInTestConfigDirectory(filename)
}
- fun addPrivacySandboxApksFor(variant: Variant, task: CopyTestApksTask) {
- // TODO (b/309610890): Replace for dependency on AGP artifact.
- val extractedPrivacySandboxSdkApksDir =
- layout.buildDirectory.dir("intermediates/extracted_apks_from_privacy_sandbox_sdks")
- task.privacySandboxSdkApks.from(
- files(extractedPrivacySandboxSdkApksDir) {
- it.builtBy("buildPrivacySandboxSdkApksForDebug")
- }
- )
- // TODO (b/309610890): Replace for dependency on AGP artifact.
- val usesSdkSplitDir =
- layout.buildDirectory.dir("intermediates/uses_sdk_library_split_for_local_deployment")
- task.privacySandboxUsesSdkSplit.from(
- files(usesSdkSplitDir) {
- it.builtBy("generateDebugAdditionalSplitForPrivacySandboxDeployment")
- }
- )
- // TODO (b/309610890): Replace for dependency on AGP artifact.
- val extractedPrivacySandboxCompatSplitsDir =
- layout.buildDirectory.dir("intermediates/extracted_sdk_apks")
- task.privacySandboxSdkCompatSplits.from(
- files(extractedPrivacySandboxCompatSplitsDir) {
- it.builtBy("extractApksFromSdkSplitsForDebug")
- }
- )
- task.filenamePrefixForPrivacySandboxFiles.set("${path.asFilenamePrefix()}-${variant.name}")
- task.outputPrivacySandboxSdkApks.set(
- getPrivacySandboxFilesDirectory().map {
- it.dir("${path.asFilenamePrefix()}-${variant.name}-sdks")
- }
- )
- task.outputPrivacySandboxAppSplits.set(
- getPrivacySandboxFilesDirectory().map {
- it.dir("${path.asFilenamePrefix()}-${variant.name}-app-splits")
- }
- )
- task.outputPrivacySandboxCompatAppSplits.set(
- getPrivacySandboxFilesDirectory().map {
- it.dir("${path.asFilenamePrefix()}-${variant.name}-compat-app-splits")
- }
- )
- }
-
// For application modules, the instrumentation apk is generated in the module itself
extensions.findByType(ApplicationAndroidComponentsExtension::class.java)?.apply {
onVariants(selector().withBuildType("debug")) { variant ->
- tasks.named(
- "${COPY_TEST_APKS_TASK}${variant.name}AndroidTest",
- CopyTestApksTask::class.java
- ) { task ->
- task.appFolder.set(variant.artifacts.get(SingleArtifact.APK))
- task.appLoader.set(variant.artifacts.getBuiltArtifactsLoader())
+ if (isPrivacySandboxEnabled()) {
+ @Suppress("UnstableApiUsage") // variant.outputProviders
+ addAppApksToPrivacySandboxTestConfigsGeneration(
+ testVariantName = "${variant.name}AndroidTest",
+ variant,
+ variant.outputProviders
+ )
+ } else {
+ // TODO(b/347956800): Migrate to ApkOutputProviders after testing on PrivacySandbox
+ addAppApkFromArtifactsToTestConfigGeneration(
+ testVariantName = "${variant.name}AndroidTest",
+ variant,
+ configureAction = { task ->
+ task.appFolder.set(variant.artifacts.get(SingleArtifact.APK))
- // The target project is the same being evaluated
- task.outputAppApk.set(outputAppApkFile(variant, path, null))
-
- if (isPrivacySandboxEnabled()) {
- addPrivacySandboxApksFor(variant, task)
- }
+ // The target project is the same being evaluated
+ task.outputAppApk.set(outputAppApkFile(variant, path, null))
+ }
+ )
}
}
}
@@ -291,33 +238,46 @@
// from the application one.
extensions.findByType(TestAndroidComponentsExtension::class.java)?.apply {
onVariants(selector().all()) { variant ->
- tasks.named("${COPY_TEST_APKS_TASK}${variant.name}", CopyTestApksTask::class.java) {
- task ->
- task.appLoader.set(variant.artifacts.getBuiltArtifactsLoader())
-
- // The target app path is defined in the targetProjectPath field in the android
- // extension of the test module
- val targetProjectPath =
- project.extensions.getByType(TestExtension::class.java).targetProjectPath
- ?: throw IllegalStateException(
+ if (isPrivacySandboxEnabled()) {
+ @Suppress("UnstableApiUsage") // variant.outputProviders
+ addAppApksToPrivacySandboxTestConfigsGeneration(
+ testVariantName = variant.name,
+ variant,
+ variant.outputProviders
+ )
+ } else {
+ // TODO(b/347956800): Migrate to ApkOutputProviders after b/378675038
+ addAppApkFromArtifactsToTestConfigGeneration(
+ testVariantName = variant.name,
+ variant,
+ configureAction = { task ->
+ // The target app path is defined in the targetProjectPath field in the
+ // android extension of the test module
+ val targetProjectPath =
+ project.extensions
+ .getByType(TestExtension::class.java)
+ .targetProjectPath
+ ?: throw IllegalStateException(
+ """
+ Module `$path` does not have a targetProjectPath defined.
"""
- Module `$path` does not have a targetProjectPath defined.
- """
- .trimIndent()
- )
- task.outputAppApk.set(outputAppApkFile(variant, targetProjectPath, path))
+ .trimIndent()
+ )
+ task.outputAppApk.set(outputAppApkFile(variant, targetProjectPath, path))
- task.appFileCollection.from(
- configurations
- .named("${variant.name}TestedApks")
- .get()
- .incoming
- .artifactView {
- it.attributes { container ->
- container.attribute(ARTIFACT_TYPE_ATTRIBUTE, "apk")
- }
- }
- .files
+ task.appFileCollection.from(
+ configurations
+ .named("${variant.name}TestedApks")
+ .get()
+ .incoming
+ .artifactView {
+ it.attributes { container ->
+ container.attribute(ARTIFACT_TYPE_ATTRIBUTE, "apk")
+ }
+ }
+ .files
+ )
+ }
)
}
}
@@ -349,27 +309,75 @@
config.dependencies.add(project.dependencyFactory.create(targetAppProject))
}
- tasks.named(
- "${COPY_TEST_APKS_TASK}${variant.name}AndroidTest",
- CopyTestApksTask::class.java
- ) { task ->
- task.appLoader.set(variant.artifacts.getBuiltArtifactsLoader())
+ addAppApkFromArtifactsToTestConfigGeneration(
+ testVariantName = "${variant.name}AndroidTest",
+ variant,
+ configureAction = { task ->
+ // The target app path is defined in the androidx extension
+ task.outputAppApk.set(outputAppApkFile(variant, targetAppProject.path, path))
- // The target app path is defined in the androidx extension
- task.outputAppApk.set(outputAppApkFile(variant, targetAppProject.path, path))
-
- task.appFileCollection.from(
- configuration.incoming
- .artifactView { view ->
- view.attributes { it.attribute(ARTIFACT_TYPE_ATTRIBUTE, "apk") }
- }
- .files
- )
- }
+ task.appFileCollection.from(
+ configuration.incoming
+ .artifactView { view ->
+ view.attributes { it.attribute(ARTIFACT_TYPE_ATTRIBUTE, "apk") }
+ }
+ .files
+ )
+ }
+ )
}
}
}
+private fun Project.addAppApkFromArtifactsToTestConfigGeneration(
+ testVariantName: String,
+ variant: Variant,
+ configureAction: Consumer<CopyApkFromArtifactsTask>
+) {
+ val copyApkTask = registerCopyAppApkFromArtifactsTask(variant, configureAction)
+ tasks.named(
+ "${GENERATE_TEST_CONFIGURATION_TASK}$testVariantName",
+ GenerateTestConfigurationTask::class.java
+ ) { t ->
+ t.appApksModel.set(copyApkTask.flatMap(CopyApkFromArtifactsTask::outputAppApksModel))
+ }
+}
+
+// TODO(b/347956800): Call from createTestConfigurationGenerationTask() after b/378674313
+// TODO(b/347956800): Use tasks providers directly instead of tasks.named after b/378674313
+@Suppress("UnstableApiUsage") // ApkOutputProviders
+private fun Project.addAppApksToPrivacySandboxTestConfigsGeneration(
+ testVariantName: String,
+ variant: Variant,
+ outputProviders: ApkOutputProviders,
+) {
+ val copyTestApksTask =
+ tasks.named("${COPY_TEST_APKS_TASK}${testVariantName}", CopyTestApksTask::class.java)
+ val excludeTestApk = copyTestApksTask.flatMap(CopyTestApksTask::outputTestApk)
+
+ val copyMainApksTask =
+ registerCopyPrivacySandboxMainAppApksTask(variant, outputProviders, excludeTestApk)
+ tasks.named(
+ "${GENERATE_PRIVACY_SANDBOX_MAIN_TEST_CONFIGURATION_TASK}${testVariantName}",
+ GenerateTestConfigurationTask::class.java
+ ) { t ->
+ t.appApksModel.set(
+ copyMainApksTask.flatMap(CopyApksFromOutputProviderTask::outputAppApksModel)
+ )
+ }
+
+ val copyCompatApksTask =
+ registerCopyPrivacySandboxCompatAppApksTask(variant, outputProviders, excludeTestApk)
+ tasks.named(
+ "${GENERATE_PRIVACY_SANDBOX_COMPAT_TEST_CONFIGURATION_TASK}${testVariantName}",
+ GenerateTestConfigurationTask::class.java
+ ) { t ->
+ t.appApksModel.set(
+ copyCompatApksTask.flatMap(CopyApksFromOutputProviderTask::outputAppApksModel)
+ )
+ }
+}
+
private fun getOrCreateMediaTestConfigTask(
project: Project
): TaskProvider<GenerateMediaTestConfigurationTask> {
@@ -564,7 +572,7 @@
@Suppress("UnstableApiUsage") // HasDeviceTests is @Incubating b/372495504
fun Project.configureTestConfigGeneration(
- kotlinMultiplatformAndroidTarget: DeprecatedKotlinMultiplatformAndroidTarget,
+ kotlinMultiplatformAndroidTarget: KotlinMultiplatformAndroidLibraryTarget,
componentsExtension: KotlinMultiplatformAndroidComponentsExtension,
projectIsolationEnabled: Boolean,
) {
@@ -584,49 +592,10 @@
}
}
-private fun Project.getTestSourceSetsForAndroid(variant: Variant?): List<FileCollection> {
- val testSourceFileCollections = mutableListOf<FileCollection>()
- when (variant) {
- is TestVariant -> {
- // com.android.test modules keep test code in main sourceset
- variant.sources.java?.all?.let { sourceSet ->
- testSourceFileCollections.add(files(sourceSet))
- }
- // Add kotlin-android main source set
- extensions
- .findByType(KotlinAndroidProjectExtension::class.java)
- ?.sourceSets
- ?.find { it.name == "main" }
- ?.let { testSourceFileCollections.add(it.kotlin.sourceDirectories) }
- // Note, don't have to add kotlin-multiplatform as it is not compatible with
- // com.android.test modules
- }
- is com.android.build.api.variant.HasAndroidTest -> {
- variant.androidTest?.sources?.java?.all?.let {
- testSourceFileCollections.add(files(it))
- }
- }
- }
-
- // Add kotlin-android androidTest source set
- extensions
- .findByType(KotlinAndroidProjectExtension::class.java)
- ?.sourceSets
- ?.find { it.name == "androidTest" }
- ?.let { testSourceFileCollections.add(it.kotlin.sourceDirectories) }
-
- // Add kotlin-multiplatform androidInstrumentedTest target source sets
- multiplatformExtension
- ?.targets
- ?.filterIsInstance<KotlinAndroidTarget>()
- ?.mapNotNull { it.compilations.find { it.name == "releaseAndroidTest" } }
- ?.flatMap { it.allKotlinSourceSets }
- ?.mapTo(testSourceFileCollections) { it.kotlin.sourceDirectories }
- return testSourceFileCollections
-}
-
private fun Project.isPrivacySandboxEnabled(): Boolean =
- extensions.findByType(ApplicationExtension::class.java)?.privacySandbox?.enable ?: false
+ extensions.findByType(ApplicationExtension::class.java)?.privacySandbox?.enable
+ ?: extensions.findByType(TestExtension::class.java)?.privacySandbox?.enable
+ ?: false
private const val COPY_TEST_APKS_TASK = "CopyTestApks"
private const val GENERATE_PRIVACY_SANDBOX_MAIN_TEST_CONFIGURATION_TASK =
@@ -634,4 +603,3 @@
private const val GENERATE_PRIVACY_SANDBOX_COMPAT_TEST_CONFIGURATION_TASK =
"GeneratePrivacySandboxCompatTestConfiguration"
private const val GENERATE_TEST_CONFIGURATION_TASK = "GenerateTestConfiguration"
-private const val PRIVACY_SANDBOX_MIN_API_LEVEL = 34
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/AndroidXExtension.kt b/buildSrc/public/src/main/kotlin/androidx/build/AndroidXExtension.kt
index e6e68f7..99b1d02 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/AndroidXExtension.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/AndroidXExtension.kt
@@ -16,7 +16,9 @@
package androidx.build
+import com.android.build.api.variant.LibraryAndroidComponentsExtension
import groovy.lang.Closure
+import java.io.File
import javax.inject.Inject
import org.gradle.api.GradleException
import org.gradle.api.Project
@@ -324,6 +326,7 @@
fun isPublishConfigured(): Boolean = (publish != Publish.UNSET || type.publish != Publish.UNSET)
fun shouldPublishSbom(): Boolean {
+ if (isIsolatedProjectsEnabled()) return false
// IDE plugins are used by and ship inside Studio
return shouldPublish() || type == LibraryType.IDE_PLUGIN
}
@@ -441,6 +444,20 @@
samplesProjects.add(samplesProject)
}
+ /** Adds golden image assets to Android test APKs to use for screenshot tests. */
+ fun addGoldenImageAssets() {
+ project.extensions.findByType(LibraryAndroidComponentsExtension::class.java)?.onVariants {
+ variant ->
+ val subdirectory = project.path.replace(":", "/")
+ variant.androidTest
+ ?.sources
+ ?.assets
+ ?.addStaticSourceDirectory(
+ File(project.rootDir, "../../golden$subdirectory").absolutePath
+ )
+ }
+ }
+
/** Locates a project by path. */
// This method is needed for Gradle project isolation to avoid calls to parent projects due to
// androidx { samples(project(":foo")) }
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/BuildServerConfiguration.kt b/buildSrc/public/src/main/kotlin/androidx/build/BuildServerConfiguration.kt
index 2e991a7..4b8281b 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/BuildServerConfiguration.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/BuildServerConfiguration.kt
@@ -78,9 +78,9 @@
fun Project.getTestConfigDirectory(): Provider<Directory> =
rootProject.layout.buildDirectory.dir("test-xml-configs")
-/** Directory for PrivacySandbox related APKs (SDKs, compat splits) used in device tests. */
-fun Project.getPrivacySandboxFilesDirectory(): Provider<Directory> =
- rootProject.layout.buildDirectory.dir("privacysandbox-files")
+/** Directory for App APKs (from ApkOutputProviders) used in device tests. */
+fun Project.getAppApksFilesDirectory(): Provider<Directory> =
+ rootProject.layout.buildDirectory.dir("app-apks-files")
/** A file within [getTestConfigDirectory] */
fun Project.getFileInTestConfigDirectory(name: String): Provider<RegularFile> =
@@ -106,3 +106,7 @@
directory.mkdirs()
return directory
}
+
+/** Directory in a maven format to put per project publishing artifacts. */
+fun Project.getPerProjectRepositoryDirectory(): Provider<Directory> =
+ project.layout.buildDirectory.dir("repository")
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
index 4d0e98c..b0be6fb 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
@@ -26,11 +26,8 @@
* purpose of the library needs to be set, rather than a variety of more arcane options.
*
* These properties are as follows: LibraryType.publish represents how the library is published to
- * GMaven LibraryType.sourceJars represents whether we publish the source code for the library to
- * GMaven in a way accessible to download, such as by Android Studio LibraryType.generateDocs
- * represents whether we generate documentation from the library to put on developer.android.com
- * LibraryType.checkApi represents whether we enforce API compatibility of the library according to
- * our semantic versioning protocol
+ * GMaven LibraryType.checkApi represents whether we enforce API compatibility of the library
+ * according to our semantic versioning protocol
*
* The possible values of LibraryType are as follows:
* - [PUBLISHED_LIBRARY]: a conventional library published, sourced, documented, and versioned.
@@ -60,7 +57,6 @@
*/
sealed class LibraryType(
val publish: Publish = Publish.NONE,
- val sourceJars: Boolean = false,
val checkApi: RunApiTasks = RunApiTasks.No("Unknown Library Type"),
val compilationTarget: CompilationTarget = CompilationTarget.DEVICE,
val allowCallingVisibleForTestsApis: Boolean = false,
@@ -131,7 +127,6 @@
) :
LibraryType(
publish = Publish.SNAPSHOT_AND_RELEASE,
- sourceJars = true,
checkApi = checkApi,
allowCallingVisibleForTestsApis = allowCallingVisibleForTestsApis,
targetsKotlinConsumersOnly = targetsKotlinConsumersOnly
@@ -160,14 +155,12 @@
class Samples :
LibraryType(
publish = Publish.SNAPSHOT_AND_RELEASE,
- sourceJars = true,
checkApi = RunApiTasks.No("Sample Library")
)
class Lint :
LibraryType(
publish = Publish.NONE,
- sourceJars = false,
checkApi = RunApiTasks.No("Lint Library"),
compilationTarget = CompilationTarget.HOST
)
@@ -175,39 +168,13 @@
class StandalonePublishedLint :
LibraryType(
publish = Publish.SNAPSHOT_AND_RELEASE,
- sourceJars = true,
checkApi = RunApiTasks.No("Lint Library"),
compilationTarget = CompilationTarget.HOST
)
- class CompilerDaemon :
- LibraryType(
- Publish.SNAPSHOT_AND_RELEASE,
- sourceJars = false,
- RunApiTasks.No("Compiler Daemon (Host-only)"),
- CompilationTarget.HOST
- )
-
- class CompilerDaemonTest :
- LibraryType(
- Publish.NONE,
- sourceJars = false,
- RunApiTasks.No("Compiler Daemon (Host-only) Test"),
- CompilationTarget.HOST
- )
-
- class CompilerPlugin :
- LibraryType(
- Publish.SNAPSHOT_AND_RELEASE,
- sourceJars = false,
- RunApiTasks.No("Compiler Plugin (Host-only)"),
- CompilationTarget.HOST
- )
-
class GradlePlugin :
LibraryType(
Publish.SNAPSHOT_AND_RELEASE,
- sourceJars = false,
RunApiTasks.No("Gradle Plugin (Host-only)"),
CompilationTarget.HOST
)
@@ -215,7 +182,6 @@
class AnnotationProcessor :
LibraryType(
publish = Publish.SNAPSHOT_AND_RELEASE,
- sourceJars = false,
checkApi = RunApiTasks.No("Annotation Processor"),
compilationTarget = CompilationTarget.HOST
)
@@ -223,7 +189,6 @@
class AnnotationProcessorUtils :
LibraryType(
publish = Publish.SNAPSHOT_AND_RELEASE,
- sourceJars = true,
checkApi = RunApiTasks.No("Annotation Processor Helper Library"),
compilationTarget = CompilationTarget.HOST
)
@@ -231,7 +196,6 @@
class OtherCodeProcessor(publish: Publish = Publish.SNAPSHOT_AND_RELEASE) :
LibraryType(
publish = publish,
- sourceJars = false,
checkApi = RunApiTasks.No("Code Processor (Host-only)"),
compilationTarget = CompilationTarget.HOST
)
@@ -239,7 +203,6 @@
class IdePlugin :
LibraryType(
publish = Publish.NONE,
- sourceJars = false,
// TODO: figure out a way to make sure we don't break Studio
checkApi = RunApiTasks.No("IDE Plugin (consumed only by Android Studio"),
// This is a bit complicated. IDE plugins usually have an on-device component installed
diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle
index a73e8cf..c84b8d1 100644
--- a/buildSrc/settings.gradle
+++ b/buildSrc/settings.gradle
@@ -35,6 +35,7 @@
include ":imports:room-gradle-plugin"
include ":imports:glance-layout-generator"
include ":imports:stableaidl-gradle-plugin"
+include ":imports:privacysandbox-gradle-plugin"
dependencyResolutionManagement {
versionCatalogs {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
index 5ad87d7..e0984c9 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -213,12 +213,10 @@
return streamConfigurationMapCompat.getOutputFormats()?.toSet() ?: emptySet()
}
- @SuppressLint("ClassVerificationFailure")
override fun getSupportedResolutions(format: Int): List<Size> {
return streamConfigurationMapCompat.getOutputSizes(format)?.toList() ?: emptyList()
}
- @SuppressLint("ClassVerificationFailure")
override fun getSupportedHighResolutions(format: Int): List<Size> {
return streamConfigurationMapCompat.getHighResolutionOutputSizes(format)?.toList()
?: emptyList()
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapter.kt
index 20be397..19370bd 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapter.kt
@@ -21,8 +21,11 @@
import androidx.annotation.OptIn
import androidx.camera.camera2.pipe.CameraMetadata.Companion.isHardwareLevelLegacy
import androidx.camera.camera2.pipe.FrameInfo
+import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.InputRequest
import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestFailure
+import androidx.camera.camera2.pipe.RequestMetadata
import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.integration.compat.workaround.TemplateParamsOverride
import androidx.camera.camera2.pipe.integration.config.UseCaseCameraScope
@@ -35,9 +38,11 @@
import androidx.camera.camera2.pipe.integration.impl.toParameters
import androidx.camera.camera2.pipe.media.AndroidImage
import androidx.camera.core.ExperimentalGetImage
+import androidx.camera.core.ImageProxy
import androidx.camera.core.impl.CameraCaptureResults
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.Config
+import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
/**
@@ -112,6 +117,7 @@
}
var inputRequest: InputRequest? = null
+ var captureCallback: Request.Listener? = null
var requestTemplateToSubmit = RequestTemplate(captureConfig.templateType)
if (
captureConfig.templateType == CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG &&
@@ -127,6 +133,13 @@
val imageWrapper = AndroidImage(checkNotNull(imageProxy.image))
val frameInfo = checkNotNull(cameraCaptureResult.unwrapAs(FrameInfo::class))
inputRequest = InputRequest(imageWrapper, frameInfo)
+
+ // It's essential to call ImageProxy#close().
+ // To ensure the ImageProxy is closed after the image is written to the output
+ // surface. This is crucial to prevent resource leaks, where images might not
+ // be closed properly if CameraX fails to propagate close events to its internal
+ // components.
+ captureCallback = buildImageClosingRequestListener(imageProxy)
}
}
}
@@ -140,10 +153,15 @@
val parameters =
templateParamsOverride.getOverrideParams(requestTemplateToSubmit) +
optionBuilder.build().toParameters()
+ val requestListeners = buildList {
+ add(callbacks)
+ captureCallback?.let { add(it) }
+ addAll(additionalListeners)
+ }
return Request(
streams = streamIdList,
- listeners = listOf(callbacks) + additionalListeners,
+ listeners = requestListeners,
parameters = parameters,
extras = mapOf(CAMERAX_TAG_BUNDLE to captureConfig.tagBundle),
template = requestTemplateToSubmit,
@@ -151,6 +169,44 @@
)
}
+ private fun buildImageClosingRequestListener(imageProxy: ImageProxy): Request.Listener {
+ val imageProxyToClose = AtomicReference(imageProxy)
+
+ fun closeImageProxy() {
+ imageProxyToClose.getAndSet(null)?.close()
+ }
+
+ return object : Request.Listener {
+ override fun onComplete(
+ requestMetadata: RequestMetadata,
+ frameNumber: FrameNumber,
+ result: FrameInfo
+ ) {
+ closeImageProxy()
+ }
+
+ override fun onFailed(
+ requestMetadata: RequestMetadata,
+ frameNumber: FrameNumber,
+ requestFailure: RequestFailure
+ ) {
+ closeImageProxy()
+ }
+
+ override fun onAborted(request: Request) {
+ closeImageProxy()
+ }
+
+ override fun onTotalCaptureResult(
+ requestMetadata: RequestMetadata,
+ frameNumber: FrameNumber,
+ totalCaptureResult: FrameInfo
+ ) {
+ closeImageProxy()
+ }
+ }
+ }
+
public companion object {
internal fun CaptureConfig.getStillCaptureTemplate(
sessionTemplate: RequestTemplate,
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
index 9f6da5a..0c7d723 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
@@ -16,7 +16,6 @@
package androidx.camera.camera2.pipe.integration.adapter
-import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.content.pm.PackageManager.FEATURE_CAMERA_CONCURRENT
@@ -1512,7 +1511,6 @@
* @param highResolutionIncluded whether high resolution output sizes are included
* @return the max supported output size for the image format
*/
- @SuppressLint("ClassVerificationFailure")
internal fun getMaxOutputSizeByFormat(
map: StreamConfigurationMap?,
imageFormat: Int,
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt
index 609fce5..de5c7a3 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt
@@ -117,6 +117,10 @@
Log.debug { "Prepare UseCaseCameraGraphConfig: $cameraGraph " }
+ // Start the CameraGraph first before setting up Surfaces. Surfaces can be closed, and we
+ // will close the CameraGraph when that happens, and we cannot start a closed CameraGraph.
+ cameraGraph.start()
+
if (!sessionConfigAdapter.isSessionProcessorEnabled) {
Log.debug { "Setting up Surfaces with UseCaseSurfaceManager" }
if (sessionConfigAdapter.isSessionConfigValid()) {
@@ -128,8 +132,7 @@
)
.invokeOnCompletion { throwable ->
// Only show logs for error cases, ignore CancellationException since the
- // task
- // could be cancelled by UseCaseSurfaceManager#stopAsync().
+ // task could be cancelled by UseCaseSurfaceManager#stopAsync().
if (throwable != null && throwable !is CancellationException) {
Log.error(throwable) { "Surface setup error!" }
}
@@ -139,8 +142,6 @@
}
}
- cameraGraph.start()
-
return UseCaseGraphConfig(
graph = cameraGraph,
surfaceToStreamMap = surfaceToStreamMap,
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CapturePipeline.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CapturePipeline.kt
index 642bf5d..e0d3060 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CapturePipeline.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CapturePipeline.kt
@@ -32,7 +32,6 @@
package androidx.camera.camera2.pipe.integration.impl
-import android.annotation.SuppressLint
import android.hardware.camera2.CameraCharacteristics.CONTROL_AE_STATE_FLASH_REQUIRED
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CaptureResult
@@ -616,7 +615,6 @@
completeSignal.complete(null)
}
- @SuppressLint("ClassVerificationFailure")
override fun onFailed(
requestMetadata: RequestMetadata,
frameNumber: FrameNumber,
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseSurfaceManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseSurfaceManager.kt
index aa4e070..9fd040b 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseSurfaceManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseSurfaceManager.kt
@@ -36,6 +36,7 @@
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
private const val TIMEOUT_GET_SURFACE_IN_MS = 5_000L
@@ -52,12 +53,12 @@
private val lock = Any()
+ @GuardedBy("lock") private var setupDeferred: Deferred<Unit>? = null
+
@GuardedBy("lock") private val activeSurfaceMap = mutableMapOf<Surface, DeferrableSurface>()
@GuardedBy("lock") private var configuredSurfaceMap: Map<Surface, DeferrableSurface>? = null
- private var setupSurfaceDeferred: Deferred<Unit>? = null
-
@GuardedBy("lock") private var stopDeferred: CompletableDeferred<Unit>? = null
@GuardedBy("lock") private var _sessionConfigAdapter: SessionConfigAdapter? = null
@@ -68,70 +69,100 @@
sessionConfigAdapter: SessionConfigAdapter,
surfaceToStreamMap: Map<DeferrableSurface, StreamId>,
timeoutMillis: Long = TIMEOUT_GET_SURFACE_IN_MS,
- ): Deferred<Unit> {
- check(setupSurfaceDeferred == null)
- check(synchronized(lock) { stopDeferred == null && configuredSurfaceMap == null })
+ ): Deferred<Unit> =
+ synchronized(lock) {
+ check(setupDeferred == null) { "Surfaces should only be set up once!" }
+ check(stopDeferred == null) { "Surfaces being setup after stopped!" }
+ check(configuredSurfaceMap == null)
- return threads.scope
- .async {
- check(sessionConfigAdapter.isSessionConfigValid())
-
- sessionConfigAdapter.useDeferrableSurfaces { deferrableSurfaces ->
- val surfaces = getSurfaces(deferrableSurfaces, timeoutMillis)
- if (!isActive) return@async
- if (surfaces.isEmpty()) {
- Log.error { "Surface list is empty" }
- return@async
- }
- if (surfaces.areValid()) {
- synchronized(lock) {
- configuredSurfaceMap =
- deferrableSurfaces.associateBy { deferrableSurface ->
- surfaces[deferrableSurfaces.indexOf(deferrableSurface)]!!
- }
- _sessionConfigAdapter = sessionConfigAdapter
- setSurfaceListener()
- }
-
- surfaceToStreamMap.forEach {
- val stream = it.value
- val surface = surfaces[deferrableSurfaces.indexOf(it.key)]
- Log.debug { "Configured $surface for $stream" }
- graph.setSurface(stream = stream, surface = surface)
- inactiveSurfaceCloser.configure(stream, it.key, graph)
- }
- } else {
- // Only handle the first failed Surface since subsequent calls to
- // CameraInternal#onUseCaseReset() will handle the other failed Surfaces if
- // there are any.
- sessionConfigAdapter.reportSurfaceInvalid(
- deferrableSurfaces[surfaces.indexOf(null)]
- )
- }
+ val deferrableSurfaces = sessionConfigAdapter.deferrableSurfaces
+ try {
+ DeferrableSurfaces.incrementAll(deferrableSurfaces)
+ } catch (e: SurfaceClosedException) {
+ Log.error { "Failed to increment DeferrableSurfaces: Surfaces closed" }
+ // Report Surface invalid by launching a coroutine to avoid cyclic Dagger injection.
+ threads.scope.launch {
+ sessionConfigAdapter.reportSurfaceInvalid(e.deferrableSurface)
}
+ return@synchronized CompletableDeferred(Unit)
}
- .also { completeDeferred ->
- setupSurfaceDeferred = completeDeferred
- completeDeferred.invokeOnCompletion { setupSurfaceDeferred = null }
- }
- }
+
+ val deferred =
+ threads.scope
+ .async {
+ check(sessionConfigAdapter.isSessionConfigValid())
+
+ val surfaces =
+ try {
+ getSurfaces(deferrableSurfaces, timeoutMillis)
+ } catch (e: SurfaceClosedException) {
+ Log.error { "Failed to get Surfaces: Surfaces closed" }
+ sessionConfigAdapter.reportSurfaceInvalid(e.deferrableSurface)
+ return@async
+ }
+ if (!isActive || surfaces.isEmpty()) {
+ Log.error {
+ "Failed to get Surfaces: isActive=$isActive, surfaces=$surfaces"
+ }
+ return@async
+ }
+ if (surfaces.areValid()) {
+ synchronized(lock) {
+ configuredSurfaceMap =
+ deferrableSurfaces.associateBy { deferrableSurface ->
+ checkNotNull(
+ surfaces[deferrableSurfaces.indexOf(deferrableSurface)]
+ )
+ }
+ _sessionConfigAdapter = sessionConfigAdapter
+ setSurfaceListener()
+ }
+
+ surfaceToStreamMap.forEach {
+ val stream = it.value
+ val surface = surfaces[deferrableSurfaces.indexOf(it.key)]
+ Log.debug { "Configured $surface for $stream" }
+ graph.setSurface(stream = stream, surface = surface)
+ inactiveSurfaceCloser.configure(stream, it.key, graph)
+ }
+ Log.info { "Surface setup complete" }
+ } else {
+ Log.error { "Surface setup failed: Some Surfaces are invalid" }
+ // Only handle the first failed Surface since subsequent calls to
+ // CameraInternal#onUseCaseReset() will handle the other failed Surfaces
+ // if there are any.
+ sessionConfigAdapter.reportSurfaceInvalid(
+ deferrableSurfaces[surfaces.indexOf(null)]
+ )
+ }
+ }
+ .apply {
+ // When setup is done or cancelled, decrement the DeferrableSurfaces.
+ invokeOnCompletion { DeferrableSurfaces.decrementAll(deferrableSurfaces) }
+ }
+ setupDeferred = deferred
+ return@synchronized deferred
+ }
/** Cancel the Surface set up and stop the monitoring of Surface usage. */
- public fun stopAsync(): Deferred<Unit> {
- setupSurfaceDeferred?.cancel()
-
- return synchronized(lock) {
- inactiveSurfaceCloser.closeAll()
- configuredSurfaceMap = null
- stopDeferred =
- stopDeferred
- ?: CompletableDeferred<Unit>().apply {
- invokeOnCompletion { synchronized(lock) { stopDeferred = null } }
- }
- stopDeferred!!
+ public fun stopAsync(): Deferred<Unit> =
+ synchronized(lock) {
+ val currentStopDeferred = stopDeferred
+ if (currentStopDeferred != null) {
+ Log.warn { "UseCaseSurfaceManager is already stopping!" }
+ return@synchronized currentStopDeferred
}
- .also { tryClearSurfaceListener() }
- }
+ setupDeferred?.cancel()
+ inactiveSurfaceCloser.closeAll()
+ configuredSurfaceMap = null
+
+ val deferred = CompletableDeferred<Unit>()
+ this.stopDeferred = deferred
+ // This may complete stopDeferred immediately
+ tryClearSurfaceListener()
+
+ return@synchronized deferred
+ }
override fun onSurfaceActive(surface: Surface) {
synchronized(lock) {
@@ -165,10 +196,12 @@
}
}
+ @GuardedBy("lock")
private fun setSurfaceListener() {
cameraPipe.cameraSurfaceManager().addListener(this)
}
+ @GuardedBy("lock")
private fun tryClearSurfaceListener() {
synchronized(lock) {
if (activeSurfaceMap.isEmpty() && configuredSurfaceMap == null) {
@@ -193,26 +226,6 @@
.orEmpty()
}
- /**
- * Set use count at the [DeferrableSurface]s when the specified function [block] is using the
- * [DeferrableSurface].
- *
- * If it cannot set the use count to the [DeferrableSurface], the [block] will not be called.
- */
- private inline fun SessionConfigAdapter.useDeferrableSurfaces(
- block: (List<DeferrableSurface>) -> Unit
- ) =
- try {
- DeferrableSurfaces.incrementAll(deferrableSurfaces)
- try {
- block(deferrableSurfaces)
- } finally {
- DeferrableSurfaces.decrementAll(deferrableSurfaces)
- }
- } catch (e: SurfaceClosedException) {
- reportSurfaceInvalid(e.deferrableSurface)
- }
-
private fun List<Surface?>.areValid(): Boolean {
// If a Surface in configuredSurfaces is null it means the
// Surface was not retrieved from the ListenableFuture.
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt
index e82afc7..c98a67a 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt
@@ -110,6 +110,8 @@
CLOSED
}
+ private val sessionDisconnected = CountDownLatch(1)
+
@GuardedBy("lock") private var hasAttemptedCaptureSession = false
private val captureSessionAttemptCompleted = CountDownLatch(1)
@@ -193,6 +195,13 @@
Log.debug { "$this session disconnecting" }
Debug.traceStart { "$this#onSessionDisconnected" }
disconnect()
+ // Important: CaptureSessionState can be disconnected on a separate path, and we may lose
+ // the race if another disconnect call was invoked in parallel. When that happens, we get
+ // an early return, but the winning disconnect call may still be in the process of
+ // disconnecting - notably doing stopRepeating() and abortCaptures(). We wait here such
+ // that we don't prematurely create a new capture session, which would close the capture
+ // session that is still being disconnected. See b/383434693 for details.
+ Debug.trace("$this#onSessionDisconnected Await") { sessionDisconnected.await() }
Debug.traceStop()
}
@@ -281,7 +290,7 @@
synchronized(lock) {
if (state == State.CLOSING || state == State.CLOSED) {
- return@synchronized
+ return
}
state = State.CLOSING
@@ -396,6 +405,9 @@
graphListener.onGraphStopped(null)
Debug.traceStop()
}
+
+ /** Do not remove - see [onSessionDisconnected] for details. */
+ sessionDisconnected.countDown()
}
/**
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
index da4c2fd..2b9b8a2 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
@@ -202,7 +202,7 @@
mZoomControl = new ZoomControl(this, mCameraCharacteristics, mExecutor);
mTorchControl = new TorchControl(this, mCameraCharacteristics, mExecutor);
if (Build.VERSION.SDK_INT >= 23) {
- mZslControl = new ZslControlImpl(mCameraCharacteristics);
+ mZslControl = new ZslControlImpl(mCameraCharacteristics, mExecutor);
} else {
mZslControl = new ZslControlNoOpImpl();
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
index 8a04dc8..d000354 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
@@ -360,15 +360,20 @@
if (captureConfig.getTemplateType() == CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG
&& !mCameraControl.getZslControl().isZslDisabledByFlashMode()
&& !mCameraControl.getZslControl().isZslDisabledByUserCaseConfig()) {
- ImageProxy imageProxy =
- mCameraControl.getZslControl().dequeueImageFromBuffer();
- boolean isSuccess = imageProxy != null
- && mCameraControl.getZslControl().enqueueImageToImageWriter(
- imageProxy);
- if (isSuccess) {
- cameraCaptureResult =
- CameraCaptureResults.retrieveCameraCaptureResult(
- imageProxy.getImageInfo());
+ ImageProxy imageProxy = mCameraControl.getZslControl().dequeueImageFromBuffer();
+ if (imageProxy != null) {
+ if (mCameraControl.getZslControl().enqueueImageToImageWriter(imageProxy)) {
+ cameraCaptureResult = CameraCaptureResults.retrieveCameraCaptureResult(
+ imageProxy.getImageInfo());
+ } else {
+ Logger.e(TAG, "Failed to enqueue image to image writer");
+ }
+
+ if (cameraCaptureResult == null) {
+ imageProxy.close();
+ }
+ } else {
+ Logger.d(TAG, "ZSL capture skipped due to no valid buffer image");
}
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZslControlImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZslControlImpl.java
index 4c834e8..f0921e4 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZslControlImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZslControlImpl.java
@@ -60,6 +60,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
+import java.util.concurrent.Executor;
/**
* Implementation for {@link ZslControl}.
@@ -76,6 +77,7 @@
static final int MAX_IMAGES = RING_BUFFER_CAPACITY * 3;
private final @NonNull CameraCharacteristicsCompat mCameraCharacteristicsCompat;
+ private final @NonNull Executor mExecutor;
@VisibleForTesting
@SuppressWarnings("WeakerAccess")
@@ -94,8 +96,10 @@
@Nullable ImageWriter mReprocessingImageWriter;
- ZslControlImpl(@NonNull CameraCharacteristicsCompat cameraCharacteristicsCompat) {
+ ZslControlImpl(@NonNull CameraCharacteristicsCompat cameraCharacteristicsCompat,
+ @NonNull Executor executor) {
mCameraCharacteristicsCompat = cameraCharacteristicsCompat;
+ mExecutor = executor;
mIsPrivateReprocessingSupported =
isCapabilitySupported(mCameraCharacteristicsCompat,
REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING);
@@ -104,7 +108,7 @@
mImageRingBuffer = new ZslRingBuffer(
RING_BUFFER_CAPACITY,
- imageProxy -> imageProxy.close());
+ ImageProxy::close);
}
@Override
@@ -240,6 +244,13 @@
if (Build.VERSION.SDK_INT >= 23 && mReprocessingImageWriter != null && image != null) {
try {
ImageWriterCompat.queueInputImage(mReprocessingImageWriter, image);
+ // It's essential to call ImageProxy#close().
+ // Add an OnImageReleasedListener to ensure the ImageProxy is closed after
+ // the image is written to the output surface. This is crucial to prevent
+ // resource leaks, where images might not be closed properly if CameraX fails
+ // to propagate close events to its internal components.
+ ImageWriterCompat.setOnImageReleasedListener(mReprocessingImageWriter,
+ writer -> imageProxy.close(), mExecutor);
} catch (IllegalStateException e) {
Logger.e(TAG, "enqueueImageToImageWriter throws IllegalStateException = "
+ e.getMessage());
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
index 96895c8..a9e2c9d 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
@@ -1438,7 +1438,8 @@
hasCapabilities = true,
isYuvReprocessingSupported = true,
isPrivateReprocessingSupported = true
- )
+ ),
+ executorService
)
// Only need to initialize when not disabled
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ZslControlImplTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ZslControlImplTest.kt
index 8e63102..71b15b5 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ZslControlImplTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ZslControlImplTest.kt
@@ -30,6 +30,7 @@
import androidx.camera.camera2.internal.ZslControlImpl.RING_BUFFER_CAPACITY
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat
import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@@ -73,7 +74,8 @@
isYuvReprocessingSupported = false,
isPrivateReprocessingSupported = true,
isJpegValidOutputFormat = true
- )
+ ),
+ CameraXExecutors.mainThreadExecutor()
)
zslControl.addZslConfig(sessionConfigBuilder)
@@ -98,7 +100,8 @@
isYuvReprocessingSupported = true,
isPrivateReprocessingSupported = false,
isJpegValidOutputFormat = true
- )
+ ),
+ CameraXExecutors.mainThreadExecutor()
)
zslControl.addZslConfig(sessionConfigBuilder)
@@ -116,7 +119,8 @@
isYuvReprocessingSupported = true,
isPrivateReprocessingSupported = false,
isJpegValidOutputFormat = false
- )
+ ),
+ CameraXExecutors.mainThreadExecutor()
)
zslControl.addZslConfig(sessionConfigBuilder)
@@ -134,7 +138,8 @@
isYuvReprocessingSupported = false,
isPrivateReprocessingSupported = false,
isJpegValidOutputFormat = false
- )
+ ),
+ CameraXExecutors.mainThreadExecutor()
)
zslControl.addZslConfig(sessionConfigBuilder)
@@ -152,7 +157,8 @@
isYuvReprocessingSupported = false,
isPrivateReprocessingSupported = true,
isJpegValidOutputFormat = true
- )
+ ),
+ CameraXExecutors.mainThreadExecutor()
)
zslControl.isZslDisabledByUserCaseConfig = true
@@ -171,7 +177,8 @@
isYuvReprocessingSupported = false,
isPrivateReprocessingSupported = true,
isJpegValidOutputFormat = true
- )
+ ),
+ CameraXExecutors.mainThreadExecutor()
)
zslControl.isZslDisabledByFlashMode = true
@@ -197,7 +204,8 @@
isYuvReprocessingSupported = false,
isPrivateReprocessingSupported = true,
isJpegValidOutputFormat = true
- )
+ ),
+ CameraXExecutors.mainThreadExecutor()
)
zslControl.addZslConfig(sessionConfigBuilder)
@@ -220,7 +228,8 @@
isYuvReprocessingSupported = false,
isPrivateReprocessingSupported = true,
isJpegValidOutputFormat = true
- )
+ ),
+ CameraXExecutors.mainThreadExecutor()
)
zslControl.addZslConfig(sessionConfigBuilder)
@@ -241,7 +250,8 @@
isYuvReprocessingSupported = false,
isPrivateReprocessingSupported = true,
isJpegValidOutputFormat = true
- )
+ ),
+ CameraXExecutors.mainThreadExecutor()
)
zslControl.addZslConfig(sessionConfigBuilder)
diff --git a/camera/camera-core/api/current.txt b/camera/camera-core/api/current.txt
index f50d9d5..1044082 100644
--- a/camera/camera-core/api/current.txt
+++ b/camera/camera-core/api/current.txt
@@ -291,6 +291,7 @@
field public static final int COORDINATE_SYSTEM_ORIGINAL = 0; // 0x0
field public static final int COORDINATE_SYSTEM_SENSOR = 2; // 0x2
field public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1; // 0x1
+ field public static final int OUTPUT_IMAGE_FORMAT_NV21 = 3; // 0x3
field public static final int OUTPUT_IMAGE_FORMAT_RGBA_8888 = 2; // 0x2
field public static final int OUTPUT_IMAGE_FORMAT_YUV_420_888 = 1; // 0x1
field public static final int STRATEGY_BLOCK_PRODUCER = 1; // 0x1
diff --git a/camera/camera-core/api/restricted_current.txt b/camera/camera-core/api/restricted_current.txt
index f50d9d5..1044082 100644
--- a/camera/camera-core/api/restricted_current.txt
+++ b/camera/camera-core/api/restricted_current.txt
@@ -291,6 +291,7 @@
field public static final int COORDINATE_SYSTEM_ORIGINAL = 0; // 0x0
field public static final int COORDINATE_SYSTEM_SENSOR = 2; // 0x2
field public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1; // 0x1
+ field public static final int OUTPUT_IMAGE_FORMAT_NV21 = 3; // 0x3
field public static final int OUTPUT_IMAGE_FORMAT_RGBA_8888 = 2; // 0x2
field public static final int OUTPUT_IMAGE_FORMAT_YUV_420_888 = 1; // 0x1
field public static final int STRATEGY_BLOCK_PRODUCER = 1; // 0x1
diff --git a/camera/camera-core/build.gradle b/camera/camera-core/build.gradle
index 7cccaf2..191b670 100644
--- a/camera/camera-core/build.gradle
+++ b/camera/camera-core/build.gradle
@@ -44,7 +44,7 @@
implementation("androidx.lifecycle:lifecycle-common:2.1.0")
implementation("androidx.tracing:tracing:1.2.0")
implementation(libs.autoValueAnnotations)
- androidTestImplementation project(":camera:camera-camera2")
+ androidTestImplementation(project(":camera:camera-camera2"))
compileOnly(project(":external:libyuv"))
annotationProcessor(libs.autoValue)
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageAnalysisAbstractAnalyzerTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageAnalysisAbstractAnalyzerTest.java
index c91255c..0825137 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageAnalysisAbstractAnalyzerTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageAnalysisAbstractAnalyzerTest.java
@@ -19,6 +19,7 @@
import static android.graphics.ImageFormat.YUV_420_888;
import static android.graphics.PixelFormat.RGBA_8888;
+import static androidx.camera.core.ImageAnalysis.OUTPUT_IMAGE_FORMAT_NV21;
import static androidx.camera.core.ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888;
import static androidx.camera.core.ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888;
import static androidx.camera.core.ImageAnalysisAbstractAnalyzer.getAdditionalTransformMatrixAppliedByProcessor;
@@ -149,58 +150,36 @@
}
@Test
- public void analysisRunWhenOutputImageYUV() throws ExecutionException, InterruptedException {
- // Arrange.
- mImageAnalysisAbstractAnalyzer.setOutputImageFormat(OUTPUT_IMAGE_FORMAT_YUV_420_888);
- mImageAnalysisAbstractAnalyzer.setProcessedImageReaderProxy(mYUVImageReaderProxy);
-
- // Act.
- ListenableFuture<Void> result =
- mImageAnalysisAbstractAnalyzer.analyzeImage(mImageProxy);
- result.get();
-
- // Assert.
- ArgumentCaptor<ImageProxy> imageProxyArgumentCaptor =
- ArgumentCaptor.forClass(ImageProxy.class);
- verify(mAnalyzer).analyze(imageProxyArgumentCaptor.capture());
- assertThat(imageProxyArgumentCaptor.getValue().getFormat()).isEqualTo(YUV_420_888);
- assertThat(imageProxyArgumentCaptor.getValue().getPlanes().length).isEqualTo(3);
- assertThat(imageProxyArgumentCaptor.getValue().getCropRect()).isEqualTo(
- new Rect(0, 0, WIDTH, HEIGHT));
+ public void analysisRunWhenOutputImage_YUV_420_888()
+ throws ExecutionException, InterruptedException {
+ analysisRunWhenOutputImage(OUTPUT_IMAGE_FORMAT_YUV_420_888, mYUVImageReaderProxy, 3,
+ YUV_420_888);
}
@Test
- public void analysisRunWhenOutputImageRGB() throws ExecutionException, InterruptedException {
- // Arrange.
- mImageAnalysisAbstractAnalyzer.setOutputImageFormat(OUTPUT_IMAGE_FORMAT_RGBA_8888);
- mImageAnalysisAbstractAnalyzer.setProcessedImageReaderProxy(mRGBImageReaderProxy);
-
- // Act.
- ListenableFuture<Void> result =
- mImageAnalysisAbstractAnalyzer.analyzeImage(mImageProxy);
- result.get();
-
- // Assert.
- ArgumentCaptor<ImageProxy> imageProxyArgumentCaptor =
- ArgumentCaptor.forClass(ImageProxy.class);
- verify(mAnalyzer).analyze(imageProxyArgumentCaptor.capture());
- assertThat(imageProxyArgumentCaptor.getValue().getFormat()).isEqualTo(RGBA_8888);
- assertThat(imageProxyArgumentCaptor.getValue().getPlanes().length).isEqualTo(1);
- assertThat(imageProxyArgumentCaptor.getValue().getCropRect()).isEqualTo(
- new Rect(0, 0, WIDTH, HEIGHT));
+ public void analysisRunWhenOutputImage_RGBA_8888()
+ throws ExecutionException, InterruptedException {
+ analysisRunWhenOutputImage(OUTPUT_IMAGE_FORMAT_RGBA_8888, mRGBImageReaderProxy, 1,
+ RGBA_8888);
}
- @SdkSuppress(minSdkVersion = 23)
@Test
- public void analysisRunWhenRotateYUVMinSdk23() throws ExecutionException, InterruptedException {
- // Arrange.
- mImageAnalysisAbstractAnalyzer.setOutputImageFormat(OUTPUT_IMAGE_FORMAT_YUV_420_888);
- mImageAnalysisAbstractAnalyzer.setProcessedImageReaderProxy(mRotatedYUVImageReaderProxy);
- mImageAnalysisAbstractAnalyzer.setOutputImageRotationEnabled(true);
- mImageAnalysisAbstractAnalyzer.setSensorToBufferTransformMatrix(mSensorToBufferMatrix);
- mImageAnalysisAbstractAnalyzer.setRelativeRotation(/*rotation=*/90);
+ public void analysisRunWhenOutputImage_NV21()
+ throws ExecutionException, InterruptedException {
+ analysisRunWhenOutputImage(OUTPUT_IMAGE_FORMAT_NV21, null, 3, YUV_420_888);
+ }
- Matrix original = new Matrix(mSensorToBufferMatrix);
+ private void analysisRunWhenOutputImage(
+ int outputFormat,
+ @Nullable SafeCloseImageReaderProxy processedImageReaderProxy,
+ int expectedPlaneCount,
+ int expectedFormat)
+ throws ExecutionException, InterruptedException {
+ // Arrange.
+ mImageAnalysisAbstractAnalyzer.setOutputImageFormat(outputFormat);
+ if (processedImageReaderProxy != null) {
+ mImageAnalysisAbstractAnalyzer.setProcessedImageReaderProxy(processedImageReaderProxy);
+ }
// Act.
ListenableFuture<Void> result =
@@ -211,17 +190,15 @@
ArgumentCaptor<ImageProxy> imageProxyArgumentCaptor =
ArgumentCaptor.forClass(ImageProxy.class);
verify(mAnalyzer).analyze(imageProxyArgumentCaptor.capture());
-
- Matrix target = new Matrix();
- target.setConcat(original, getAdditionalTransformMatrixAppliedByProcessor(
- WIDTH, HEIGHT, HEIGHT, WIDTH, 90));
- assertThat(imageProxyArgumentCaptor.getValue().getImageInfo()
- .getSensorToBufferTransformMatrix()).isEqualTo(target);
-
- assertThat(mImageAnalysisAbstractAnalyzer.getRGBConverterBuffer()).isNull();
- assertThat(mImageAnalysisAbstractAnalyzer.getYRotatedBuffer()).isNotNull();
- assertThat(mImageAnalysisAbstractAnalyzer.getURotatedBuffer()).isNotNull();
- assertThat(mImageAnalysisAbstractAnalyzer.getVRotatedBuffer()).isNotNull();
+ assertThat(imageProxyArgumentCaptor.getValue().getFormat()).isEqualTo(expectedFormat);
+ assertThat(imageProxyArgumentCaptor.getValue().getPlanes().length).isEqualTo(
+ expectedPlaneCount);
+ assertThat(imageProxyArgumentCaptor.getValue().getCropRect()).isEqualTo(
+ new Rect(0, 0, WIDTH, HEIGHT));
+ if (outputFormat == OUTPUT_IMAGE_FORMAT_NV21) {
+ assertThat(ImageProcessingUtil.isNV21FormatImage(
+ imageProxyArgumentCaptor.getValue())).isTrue();
+ }
}
@SdkSuppress(maxSdkVersion = 22, minSdkVersion = 21)
@@ -253,14 +230,56 @@
assertThat(mImageAnalysisAbstractAnalyzer.getYRotatedBuffer()).isNotNull();
assertThat(mImageAnalysisAbstractAnalyzer.getURotatedBuffer()).isNotNull();
assertThat(mImageAnalysisAbstractAnalyzer.getVRotatedBuffer()).isNotNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getNV21YDelegatedBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getNV21UVDelegatedBuffer()).isNull();
+ }
+
+ @SdkSuppress(minSdkVersion = 23)
+ @Test
+ public void analysisRunWhenRotateYUVMinSdk23() throws ExecutionException, InterruptedException {
+ analysisRunWhenRotate(OUTPUT_IMAGE_FORMAT_YUV_420_888, mRotatedYUVImageReaderProxy);
+
+ assertThat(mImageAnalysisAbstractAnalyzer.getRGBConverterBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getYRotatedBuffer()).isNotNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getURotatedBuffer()).isNotNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getVRotatedBuffer()).isNotNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getNV21YDelegatedBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getNV21UVDelegatedBuffer()).isNull();
}
@Test
- public void analysisRunWhenRotateRGB() throws ExecutionException,
- InterruptedException {
+ public void analysisRunWhenRotate_RGBA_8888() throws ExecutionException, InterruptedException {
+ analysisRunWhenRotate(OUTPUT_IMAGE_FORMAT_RGBA_8888, mRotatedRGBImageReaderProxy);
+
+ assertThat(mImageAnalysisAbstractAnalyzer.getRGBConverterBuffer()).isNotNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getYRotatedBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getURotatedBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getVRotatedBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getNV21YDelegatedBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getNV21UVDelegatedBuffer()).isNull();
+ }
+
+ @Test
+ public void analysisRunWhenRotate_NV21() throws ExecutionException, InterruptedException {
+ analysisRunWhenRotate(OUTPUT_IMAGE_FORMAT_NV21, null);
+
+ assertThat(mImageAnalysisAbstractAnalyzer.getRGBConverterBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getYRotatedBuffer()).isNotNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getURotatedBuffer()).isNotNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getVRotatedBuffer()).isNotNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getNV21YDelegatedBuffer()).isNotNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getNV21UVDelegatedBuffer()).isNotNull();
+ }
+
+ private void analysisRunWhenRotate(int outputFormat,
+ @Nullable SafeCloseImageReaderProxy processedImageReaderProxy)
+ throws ExecutionException, InterruptedException {
// Arrange.
- mImageAnalysisAbstractAnalyzer.setOutputImageFormat(OUTPUT_IMAGE_FORMAT_RGBA_8888);
- mImageAnalysisAbstractAnalyzer.setProcessedImageReaderProxy(mRotatedRGBImageReaderProxy);
+ mImageAnalysisAbstractAnalyzer.setOutputImageFormat(outputFormat);
+ if (processedImageReaderProxy != null) {
+ mImageAnalysisAbstractAnalyzer.setProcessedImageReaderProxy(
+ processedImageReaderProxy);
+ }
mImageAnalysisAbstractAnalyzer.setOutputImageRotationEnabled(true);
mImageAnalysisAbstractAnalyzer.setSensorToBufferTransformMatrix(mSensorToBufferMatrix);
mImageAnalysisAbstractAnalyzer.setRelativeRotation(/*rotation=*/90);
@@ -284,20 +303,41 @@
.getSensorToBufferTransformMatrix()).isEqualTo(target);
assertThat(imageProxyArgumentCaptor.getValue().getCropRect())
.isEqualTo(new Rect(0, 0, HEIGHT, WIDTH));
-
- assertThat(mImageAnalysisAbstractAnalyzer.getRGBConverterBuffer()).isNotNull();
- assertThat(mImageAnalysisAbstractAnalyzer.getYRotatedBuffer()).isNull();
- assertThat(mImageAnalysisAbstractAnalyzer.getURotatedBuffer()).isNull();
- assertThat(mImageAnalysisAbstractAnalyzer.getVRotatedBuffer()).isNull();
+ if (outputFormat == OUTPUT_IMAGE_FORMAT_NV21) {
+ assertThat(ImageProcessingUtil.isNV21FormatImage(
+ imageProxyArgumentCaptor.getValue())).isTrue();
+ }
}
@SdkSuppress(minSdkVersion = 23)
@Test
- public void analysisRunWhenYUVSetTargetRotationMultipleTimes() throws ExecutionException,
- InterruptedException {
+ public void analysisRunWhenSetTargetRotationMultipleTimes_YUV_420_888()
+ throws ExecutionException, InterruptedException {
+ analysisRunWhenSetTargetRotationMultipleTimes(OUTPUT_IMAGE_FORMAT_YUV_420_888,
+ mRotatedYUVImageReaderProxy);
+ }
+
+ @Test
+ public void analysisRunWhenSetTargetRotationMultipleTimes_RGBA_8888()
+ throws ExecutionException, InterruptedException {
+ analysisRunWhenSetTargetRotationMultipleTimes(OUTPUT_IMAGE_FORMAT_RGBA_8888,
+ mRotatedRGBImageReaderProxy);
+ }
+
+ @Test
+ public void analysisRunWhenSetTargetRotationMultipleTimes_NV21()
+ throws ExecutionException, InterruptedException {
+ analysisRunWhenSetTargetRotationMultipleTimes(OUTPUT_IMAGE_FORMAT_NV21, null);
+ }
+
+ private void analysisRunWhenSetTargetRotationMultipleTimes(int outputFormat,
+ @Nullable SafeCloseImageReaderProxy processedImageReaderProxy)
+ throws ExecutionException, InterruptedException {
// Arrange.
- mImageAnalysisAbstractAnalyzer.setOutputImageFormat(OUTPUT_IMAGE_FORMAT_YUV_420_888);
- mImageAnalysisAbstractAnalyzer.setProcessedImageReaderProxy(mRotatedYUVImageReaderProxy);
+ mImageAnalysisAbstractAnalyzer.setOutputImageFormat(outputFormat);
+ if (processedImageReaderProxy != null) {
+ mImageAnalysisAbstractAnalyzer.setProcessedImageReaderProxy(processedImageReaderProxy);
+ }
mImageAnalysisAbstractAnalyzer.setOutputImageRotationEnabled(true);
mImageAnalysisAbstractAnalyzer.setSensorToBufferTransformMatrix(mSensorToBufferMatrix);
mImageAnalysisAbstractAnalyzer.setRelativeRotation(/*rotation=*/90);
@@ -328,86 +368,78 @@
WIDTH, HEIGHT, WIDTH, HEIGHT, 180));
assertThat(imageProxyArgumentCaptor.getValue().getImageInfo()
.getSensorToBufferTransformMatrix()).isEqualTo(target);
+ if (outputFormat == OUTPUT_IMAGE_FORMAT_NV21) {
+ assertThat(ImageProcessingUtil.isNV21FormatImage(
+ imageProxyArgumentCaptor.getValue())).isTrue();
+ }
}
@Test
- public void analysisRunWhenRGBSetTargetRotationMultipleTimes() throws ExecutionException,
+ public void analysisRunWhenNoRotate_RGBA_8888() throws ExecutionException,
InterruptedException {
+ analysisRunWhenNoRotateYUV(OUTPUT_IMAGE_FORMAT_RGBA_8888, mRotatedRGBImageReaderProxy);
+
+ assertThat(mImageAnalysisAbstractAnalyzer.getRGBConverterBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getYRotatedBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getURotatedBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getVRotatedBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getNV21YDelegatedBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getNV21UVDelegatedBuffer()).isNull();
+ }
+
+ @Test
+ public void analysisRunWhenNoRotate_YUV_420_888() throws ExecutionException,
+ InterruptedException {
+ analysisRunWhenNoRotateYUV(OUTPUT_IMAGE_FORMAT_YUV_420_888, mRotatedRGBImageReaderProxy);
+
+ assertThat(mImageAnalysisAbstractAnalyzer.getRGBConverterBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getYRotatedBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getURotatedBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getVRotatedBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getNV21YDelegatedBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getNV21UVDelegatedBuffer()).isNull();
+ }
+
+ @Test
+ public void analysisRunWhenNoRotate_NV21() throws ExecutionException,
+ InterruptedException {
+ analysisRunWhenNoRotateYUV(OUTPUT_IMAGE_FORMAT_NV21, null);
+
+ assertThat(mImageAnalysisAbstractAnalyzer.getRGBConverterBuffer()).isNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getYRotatedBuffer()).isNotNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getURotatedBuffer()).isNotNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getVRotatedBuffer()).isNotNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getNV21YDelegatedBuffer()).isNotNull();
+ assertThat(mImageAnalysisAbstractAnalyzer.getNV21UVDelegatedBuffer()).isNotNull();
+ }
+
+ private void analysisRunWhenNoRotateYUV(int outputFormat,
+ @Nullable SafeCloseImageReaderProxy processedImageReaderProxy)
+ throws ExecutionException, InterruptedException {
// Arrange.
- mImageAnalysisAbstractAnalyzer.setOutputImageFormat(OUTPUT_IMAGE_FORMAT_RGBA_8888);
- mImageAnalysisAbstractAnalyzer.setProcessedImageReaderProxy(mRotatedRGBImageReaderProxy);
- mImageAnalysisAbstractAnalyzer.setOutputImageRotationEnabled(true);
+ mImageAnalysisAbstractAnalyzer.setOutputImageFormat(outputFormat);
+ if (processedImageReaderProxy != null) {
+ mImageAnalysisAbstractAnalyzer.setProcessedImageReaderProxy(
+ mRotatedRGBImageReaderProxy);
+ }
+ mImageAnalysisAbstractAnalyzer.setOutputImageRotationEnabled(false);
mImageAnalysisAbstractAnalyzer.setSensorToBufferTransformMatrix(mSensorToBufferMatrix);
- mImageAnalysisAbstractAnalyzer.setRelativeRotation(/*rotation=*/90);
-
- Matrix original = new Matrix(mSensorToBufferMatrix);
+ mImageAnalysisAbstractAnalyzer.setRelativeRotation(/*rotation=*/270);
// Act.
- ListenableFuture<Void> result1 =
+ ListenableFuture<Void> result =
mImageAnalysisAbstractAnalyzer.analyzeImage(mImageProxy);
- result1.get();
-
- reset(mAnalyzer);
-
- // Act.
- mImageAnalysisAbstractAnalyzer.setRelativeRotation(/*rotation=*/180);
- ListenableFuture<Void> result2 =
- mImageAnalysisAbstractAnalyzer.analyzeImage(mSecondImageProxy);
- result2.get();
+ result.get();
// Assert.
ArgumentCaptor<ImageProxy> imageProxyArgumentCaptor =
ArgumentCaptor.forClass(ImageProxy.class);
verify(mAnalyzer).analyze(imageProxyArgumentCaptor.capture());
- // Verify that additional transform matrix will only be applied to original matrix once.
- Matrix target = new Matrix();
- target.setConcat(original, getAdditionalTransformMatrixAppliedByProcessor(
- WIDTH, HEIGHT, WIDTH, HEIGHT, 180));
- assertThat(imageProxyArgumentCaptor.getValue().getImageInfo()
- .getSensorToBufferTransformMatrix()).isEqualTo(target);
- }
-
- @Test
- public void analysisRunWhenNoRotateRGB() throws ExecutionException,
- InterruptedException {
- // Arrange.
- mImageAnalysisAbstractAnalyzer.setOutputImageFormat(OUTPUT_IMAGE_FORMAT_RGBA_8888);
- mImageAnalysisAbstractAnalyzer.setProcessedImageReaderProxy(mRotatedRGBImageReaderProxy);
- mImageAnalysisAbstractAnalyzer.setOutputImageRotationEnabled(false);
- mImageAnalysisAbstractAnalyzer.setSensorToBufferTransformMatrix(mSensorToBufferMatrix);
- mImageAnalysisAbstractAnalyzer.setRelativeRotation(/*rotation=*/270);
-
- // Act.
- ListenableFuture<Void> result =
- mImageAnalysisAbstractAnalyzer.analyzeImage(mImageProxy);
- result.get();
-
- assertThat(mImageAnalysisAbstractAnalyzer.getRGBConverterBuffer()).isNull();
- assertThat(mImageAnalysisAbstractAnalyzer.getYRotatedBuffer()).isNull();
- assertThat(mImageAnalysisAbstractAnalyzer.getURotatedBuffer()).isNull();
- assertThat(mImageAnalysisAbstractAnalyzer.getVRotatedBuffer()).isNull();
- }
-
- @Test
- public void analysisRunWhenNoRotateYUV() throws ExecutionException,
- InterruptedException {
- // Arrange.
- mImageAnalysisAbstractAnalyzer.setOutputImageFormat(OUTPUT_IMAGE_FORMAT_YUV_420_888);
- mImageAnalysisAbstractAnalyzer.setProcessedImageReaderProxy(mRotatedRGBImageReaderProxy);
- mImageAnalysisAbstractAnalyzer.setOutputImageRotationEnabled(false);
- mImageAnalysisAbstractAnalyzer.setSensorToBufferTransformMatrix(mSensorToBufferMatrix);
- mImageAnalysisAbstractAnalyzer.setRelativeRotation(/*rotation=*/270);
-
- // Act.
- ListenableFuture<Void> result =
- mImageAnalysisAbstractAnalyzer.analyzeImage(mImageProxy);
- result.get();
-
- assertThat(mImageAnalysisAbstractAnalyzer.getRGBConverterBuffer()).isNull();
- assertThat(mImageAnalysisAbstractAnalyzer.getYRotatedBuffer()).isNull();
- assertThat(mImageAnalysisAbstractAnalyzer.getURotatedBuffer()).isNull();
- assertThat(mImageAnalysisAbstractAnalyzer.getVRotatedBuffer()).isNull();
+ if (outputFormat == OUTPUT_IMAGE_FORMAT_NV21) {
+ assertThat(ImageProcessingUtil.isNV21FormatImage(
+ imageProxyArgumentCaptor.getValue())).isTrue();
+ }
}
@Test
@@ -561,5 +593,13 @@
@Nullable ByteBuffer getVRotatedBuffer() {
return mImageAnalysisNonBlockingAnalyzer.mVRotatedBuffer;
}
+
+ @Nullable ByteBuffer getNV21YDelegatedBuffer() {
+ return mImageAnalysisNonBlockingAnalyzer.mNV21YDelegatedBuffer;
+ }
+
+ @Nullable ByteBuffer getNV21UVDelegatedBuffer() {
+ return mImageAnalysisNonBlockingAnalyzer.mNV21UVDelegatedBuffer;
+ }
}
}
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageProcessingUtilTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageProcessingUtilTest.java
index c5f565d1..89ed840 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageProcessingUtilTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageProcessingUtilTest.java
@@ -16,14 +16,20 @@
package androidx.camera.core;
+import static androidx.camera.core.ImageAnalysis.OUTPUT_IMAGE_FORMAT_NV21;
+import static androidx.camera.core.ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888;
import static androidx.camera.core.ImageProcessingUtil.convertJpegBytesToImage;
import static androidx.camera.core.ImageProcessingUtil.rotateYUV;
+import static androidx.camera.core.ImageProcessingUtil.rotateYUVAndConvertToNV21;
import static androidx.camera.core.ImageProcessingUtil.writeJpegBytesToSurface;
+import static androidx.camera.testing.impl.IgnoreProblematicDeviceRule.Companion;
import static androidx.camera.testing.impl.ImageProxyUtil.createYUV420ImagePlanes;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assume.assumeFalse;
+
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
@@ -31,9 +37,12 @@
import android.graphics.ImageFormat;
import android.graphics.PixelFormat;
import android.media.ImageWriter;
+import android.os.Build;
import androidx.annotation.IntRange;
import androidx.camera.core.impl.utils.Exif;
+import androidx.camera.core.internal.utils.ImageUtil;
+import androidx.camera.testing.impl.TestImageUtil;
import androidx.camera.testing.impl.fakes.FakeImageInfo;
import androidx.camera.testing.impl.fakes.FakeImageProxy;
import androidx.core.math.MathUtils;
@@ -76,6 +85,8 @@
private ByteBuffer mYRotatedBuffer;
private ByteBuffer mURotatedBuffer;
private ByteBuffer mVRotatedBuffer;
+ private ByteBuffer mNV21YDelegatedByteBuffer;
+ private ByteBuffer mNV21UVDelegatedByteBuffer;
private static final int[] YUV_WHITE_STUDIO_SWING_BT601 = {/*y=*/235, /*u=*/128, /*v=*/128};
private static final int[] YUV_BLACK_STUDIO_SWING_BT601 = {/*y=*/16, /*u=*/128, /*v=*/128};
private static final int[] YUV_BLUE_STUDIO_SWING_BT601 = {/*y=*/16, /*u=*/240, /*v=*/128};
@@ -89,51 +100,65 @@
@Before
public void setUp() {
- mYUVImageProxy = new FakeImageProxy(new FakeImageInfo());
- mYUVImageProxy.setWidth(WIDTH);
- mYUVImageProxy.setHeight(HEIGHT);
+ createTestResources(WIDTH, HEIGHT, 90);
+ }
+
+ @After
+ public void tearDown() {
+ closeTestResources();
+ }
+
+ private void createTestResources(int width, int height, int rotation) {
+ mYUVImageProxy = TestImageUtil.createYuvFakeImageProxy(new FakeImageInfo(), width, height,
+ true, true);
+ mYUVImageProxy.setWidth(width);
+ mYUVImageProxy.setHeight(height);
mYUVImageProxy.setFormat(ImageFormat.YUV_420_888);
+ boolean flipWh = rotation % 180 != 0;
+
// rgb image reader proxy should not be mocked for JNI native code
mRGBImageReaderProxy = new SafeCloseImageReaderProxy(
ImageReaderProxys.createIsolatedReader(
- WIDTH,
- HEIGHT,
+ width,
+ height,
PixelFormat.RGBA_8888,
MAX_IMAGES));
// rotated image reader proxy with width and height flipped
mRotatedRGBImageReaderProxy = new SafeCloseImageReaderProxy(
ImageReaderProxys.createIsolatedReader(
- HEIGHT,
- WIDTH,
+ flipWh ? height : width,
+ flipWh ? width : height,
PixelFormat.RGBA_8888,
MAX_IMAGES));
mRotatedYUVImageReaderProxy = new SafeCloseImageReaderProxy(
ImageReaderProxys.createIsolatedReader(
- HEIGHT,
- WIDTH,
+ flipWh ? height : width,
+ flipWh ? width : height,
ImageFormat.YUV_420_888,
MAX_IMAGES));
mJpegImageReaderProxy = new SafeCloseImageReaderProxy(
ImageReaderProxys.createIsolatedReader(
- WIDTH,
- HEIGHT,
+ flipWh ? height : width,
+ flipWh ? width : height,
ImageFormat.JPEG,
MAX_IMAGES));
- mRgbConvertedBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT * 4);
- mYRotatedBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT);
- mURotatedBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT / 2);
- mVRotatedBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT / 2);
+ mRgbConvertedBuffer = ByteBuffer.allocateDirect(width * height * 4);
+ mYRotatedBuffer = ByteBuffer.allocateDirect(width * height);
+ mURotatedBuffer = ByteBuffer.allocateDirect(width * height / 2);
+ mVRotatedBuffer = ByteBuffer.allocateDirect(width * height / 2);
+ mNV21YDelegatedByteBuffer = ByteBuffer.allocateDirect(width * height);
+ mNV21UVDelegatedByteBuffer = ByteBuffer.allocateDirect(width * height / 2);
}
- @After
- public void tearDown() {
+ private void closeTestResources() {
mRGBImageReaderProxy.safeClose();
mRotatedRGBImageReaderProxy.safeClose();
+ mRotatedYUVImageReaderProxy.safeClose();
mJpegImageReaderProxy.safeClose();
}
@@ -388,34 +413,106 @@
@SdkSuppress(minSdkVersion = 23)
@Test
- public void rotateYUV_imageRotated() {
+ public void rotateYUV_imageRotated_0() {
+ rotateYUV_imageRotated(OUTPUT_IMAGE_FORMAT_YUV_420_888, 0, true);
+ }
+
+ @SdkSuppress(minSdkVersion = 23)
+ @Test
+ public void rotateYUV_imageRotated_90() {
+ rotateYUV_imageRotated(OUTPUT_IMAGE_FORMAT_YUV_420_888, 90, false);
+ }
+
+ @SdkSuppress(minSdkVersion = 23)
+ @Test
+ public void rotateYUV_imageRotated_180() {
+ rotateYUV_imageRotated(OUTPUT_IMAGE_FORMAT_YUV_420_888, 180, false);
+ }
+
+ @SdkSuppress(minSdkVersion = 23)
+ @Test
+ public void rotateYUV_imageRotated_270() {
+ rotateYUV_imageRotated(OUTPUT_IMAGE_FORMAT_YUV_420_888, 270, false);
+ }
+
+ @Test
+ public void rotateYUV_imageRotated_0_outputNV21() {
+ rotateYUV_imageRotated(OUTPUT_IMAGE_FORMAT_NV21, 0, false);
+ }
+
+ @Test
+ public void rotateYUV_imageRotated_90_outputNV21() {
+ rotateYUV_imageRotated(OUTPUT_IMAGE_FORMAT_NV21, 90, false);
+ }
+
+ @Test
+ public void rotateYUV_imageRotated_180_outputNV21() {
+ rotateYUV_imageRotated(OUTPUT_IMAGE_FORMAT_NV21, 180, false);
+ }
+
+ @Test
+ public void rotateYUV_imageRotated_270_outputNV21() {
+ rotateYUV_imageRotated(OUTPUT_IMAGE_FORMAT_NV21, 270, false);
+ }
+
+ private void rotateYUV_imageRotated(
+ int outputImageFormat,
+ int rotation,
+ boolean outputShouldBeNull) {
+ // Pixel2 API28 emulator has problem to run the test
+ assumeFalse(Companion.isPixel2Api28Emulator());
// Arrange.
- mYUVImageProxy.setPlanes(createYUV420ImagePlanes(
- WIDTH,
- HEIGHT,
- PIXEL_STRIDE_Y,
- PIXEL_STRIDE_UV,
- /*flipUV=*/true,
- /*incrementValue=*/false));
+ int width = 640;
+ int height = 480;
+ closeTestResources();
+ createTestResources(width, height, rotation);
// Act.
- ImageProxy yuvImageProxy = rotateYUV(
- mYUVImageProxy,
- mRotatedYUVImageReaderProxy,
- ImageWriter.newInstance(
- mRotatedYUVImageReaderProxy.getSurface(),
- mRotatedYUVImageReaderProxy.getMaxImages()),
- mYRotatedBuffer,
- mURotatedBuffer,
- mVRotatedBuffer,
- /*rotation=*/90);
+ ImageProxy yuvImageProxy = null;
+
+ if (outputImageFormat == OUTPUT_IMAGE_FORMAT_YUV_420_888 && Build.VERSION.SDK_INT >= 23) {
+ yuvImageProxy = rotateYUV(
+ mYUVImageProxy,
+ mRotatedYUVImageReaderProxy,
+ ImageWriter.newInstance(
+ mRotatedYUVImageReaderProxy.getSurface(),
+ mRotatedYUVImageReaderProxy.getMaxImages()),
+ mYRotatedBuffer,
+ mURotatedBuffer,
+ mVRotatedBuffer,
+ /*rotation=*/rotation);
+ } else if (outputImageFormat == OUTPUT_IMAGE_FORMAT_NV21) {
+ yuvImageProxy = rotateYUVAndConvertToNV21(
+ mYUVImageProxy,
+ mYRotatedBuffer,
+ mURotatedBuffer,
+ mVRotatedBuffer,
+ mNV21YDelegatedByteBuffer,
+ mNV21UVDelegatedByteBuffer,
+ /*rotation=*/rotation);
+ }
// Assert.
+ if (outputShouldBeNull) {
+ assertThat(yuvImageProxy).isNull();
+ return;
+ }
+
+ boolean flipWh = rotation % 180 != 0;
assertThat(yuvImageProxy).isNotNull();
assertThat(yuvImageProxy.getFormat()).isEqualTo(ImageFormat.YUV_420_888);
assertThat(yuvImageProxy.getPlanes().length).isEqualTo(3);
- assertThat(yuvImageProxy.getWidth()).isEqualTo(HEIGHT);
- assertThat(yuvImageProxy.getHeight()).isEqualTo(WIDTH);
+ assertThat(yuvImageProxy.getWidth()).isEqualTo(flipWh ? height : width);
+ assertThat(yuvImageProxy.getHeight()).isEqualTo(flipWh ? width : height);
+
+ // Verifies the color value diff between the rotated input image and the decoded output
+ // image proxy.
+ Bitmap inputDecodedBitmap = ImageUtil.createBitmapFromImageProxy(mYUVImageProxy);
+ Bitmap inputRotatedBitmap = TestImageUtil.rotateBitmap(inputDecodedBitmap, rotation);
+ Bitmap outputBitmap = ImageUtil.createBitmapFromImageProxy(yuvImageProxy);
+ assertThat(TestImageUtil.getAverageDiff(inputRotatedBitmap, outputBitmap))
+ .isEqualTo(0);
+
yuvImageProxy.close();
}
diff --git a/camera/camera-core/src/main/cpp/image_processing_util_jni.cc b/camera/camera-core/src/main/cpp/image_processing_util_jni.cc
index b95be97..66d9c5b 100644
--- a/camera/camera-core/src/main/cpp/image_processing_util_jni.cc
+++ b/camera/camera-core/src/main/cpp/image_processing_util_jni.cc
@@ -28,6 +28,7 @@
#include "libyuv/convert_argb.h"
#include "libyuv/rotate_argb.h"
#include "libyuv/convert.h"
+#include "libyuv/planar_functions.h"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "YuvToRgbJni", __VA_ARGS__)
@@ -517,7 +518,6 @@
int halfwidth = (width + 1) >> 1;
int halfheight = (height + 1) >> 1;
- // TODO(b/203141655): avoid unnecessary memory copy by merging libyuv API for rotation.
uint8_t *rotated_y_ptr =
static_cast<uint8_t *>(env->GetDirectBufferAddress(rotated_buffer_y));
uint8_t *rotated_u_ptr =
@@ -532,90 +532,100 @@
int rotated_stride_u = flip_wh ? halfheight : halfwidth;
int rotated_stride_v = flip_wh ? halfheight : halfwidth;
+ int result = 0;
+
+ // Converts Android420 to I420 format with rotation applied
+ result = libyuv::Android420ToI420Rotate(
+ src_y_ptr,
+ src_stride_y,
+ src_u_ptr,
+ src_stride_u,
+ src_v_ptr,
+ src_stride_v,
+ src_pixel_stride_uv,
+ rotated_y_ptr,
+ rotated_stride_y,
+ rotated_u_ptr,
+ rotated_stride_u,
+ rotated_v_ptr,
+ rotated_stride_v,
+ width,
+ height,
+ mode);
+
+ if (result != 0) {
+ return result;
+ }
+
+ // Convert to the required output format
int rotated_width = flip_wh ? height : width;
int rotated_height = flip_wh ? width : height;
int rotated_halfwidth = flip_wh ? halfheight : halfwidth;
int rotated_halfheight = flip_wh ? halfwidth : halfheight;
+ const ptrdiff_t vu_off = dst_v_ptr - dst_u_ptr;
- int result = 0;
- const ptrdiff_t vu_off = src_v_ptr - src_u_ptr;
-
- if (src_pixel_stride_uv == 1) {
- // I420
- result = libyuv::I420Rotate(src_y_ptr,
- src_stride_y,
- src_u_ptr,
- src_stride_u,
- src_v_ptr,
- src_stride_v,
- rotated_y_ptr,
- rotated_stride_y,
- rotated_u_ptr,
- rotated_stride_u,
- rotated_v_ptr,
- rotated_stride_v,
- width,
- height,
- mode);
- } else if (src_pixel_stride_uv == 2 && vu_off == -1 &&
- src_stride_u == src_stride_v) {
+ if (dst_pixel_stride_u == 2 && vu_off == -1) {
// NV21
- result = libyuv::NV12ToI420Rotate(src_y_ptr,
- src_stride_y,
- src_v_ptr,
- src_stride_v,
- rotated_y_ptr,
- rotated_stride_y,
- rotated_v_ptr,
- rotated_stride_v,
- rotated_u_ptr,
- rotated_stride_u,
- width,
- height,
- mode);
- } else if (src_pixel_stride_uv == 2 && vu_off == 1 && src_stride_u == src_stride_v) {
+ result = libyuv::I420ToNV21(
+ /* src_y= */ rotated_y_ptr,
+ /* src_stride_y= */ rotated_width,
+ /* src_u= */ rotated_u_ptr,
+ /* src_stride_u= */ rotated_halfwidth,
+ /* src_v= */ rotated_v_ptr,
+ /* src_stride_v= */ rotated_halfwidth,
+ /* dst_y= */ dst_y_ptr,
+ /* dst_stride_y= */ dst_stride_y,
+ /* dst_uv= */ dst_v_ptr,
+ /* dst_stride_uv= */ dst_stride_v,
+ /* width= */ rotated_width,
+ /* height= */ rotated_height
+ );
+ } else if (dst_pixel_stride_u == 2 && vu_off == 1) {
// NV12
- result = libyuv::NV12ToI420Rotate(src_y_ptr,
- src_stride_y,
- src_u_ptr,
- src_stride_u,
- rotated_y_ptr,
- rotated_stride_y,
- rotated_u_ptr,
- rotated_stride_u,
- rotated_v_ptr,
- rotated_stride_v,
- width,
- height,
- mode);
+ result = libyuv::I420ToNV12(
+ /* src_y= */ rotated_y_ptr,
+ /* src_stride_y= */ rotated_width,
+ /* src_u= */ rotated_u_ptr,
+ /* src_stride_u= */ rotated_halfwidth,
+ /* src_v= */ rotated_v_ptr,
+ /* src_stride_v= */ rotated_halfwidth,
+ /* dst_y= */ dst_y_ptr,
+ /* dst_stride_y= */ dst_stride_y,
+ /* dst_uv= */ dst_u_ptr,
+ /* dst_stride_uv= */ dst_stride_u,
+ /* width= */ rotated_width,
+ /* height= */ rotated_height
+ );
+ } else if (dst_pixel_stride_u == 1 && dst_pixel_stride_v == 1) {
+ // I420
+ // Copies Y plane
+ libyuv::CopyPlane(
+ /* src_y= */ rotated_y_ptr,
+ /* src_stride_y= */ rotated_width,
+ /* dst_y= */ dst_y_ptr,
+ /* dst_stride_y= */ dst_stride_y,
+ /* width= */ rotated_width,
+ /* height= */ rotated_height);
+ // Copies U plane
+ libyuv::CopyPlane(
+ /* src_y= */ rotated_u_ptr,
+ /* src_stride_y= */ rotated_halfwidth,
+ /* dst_y= */ dst_u_ptr,
+ /* dst_stride_y= */ dst_stride_u,
+ /* width= */ rotated_halfwidth,
+ /* height= */ rotated_halfheight);
+ // Copies V plane
+ libyuv::CopyPlane(
+ /* src_y= */ rotated_v_ptr,
+ /* src_stride_y= */ rotated_halfwidth,
+ /* dst_y= */ dst_v_ptr,
+ /* dst_stride_y= */ dst_stride_v,
+ /* width= */ rotated_halfwidth,
+ /* height= */ rotated_halfheight);
} else {
- // General case fallback creates NV12
- align_buffer_64(plane_uv, halfwidth * 2 * halfheight);
- uint8_t* dst_uv = plane_uv;
- for (int y = 0; y < halfheight; y++) {
- weave_pixels(src_u_ptr, src_v_ptr, src_pixel_stride_uv, dst_uv, halfwidth);
- src_u_ptr += src_stride_u;
- src_v_ptr += src_stride_v;
- dst_uv += halfwidth * 2;
- }
+ // Fall backs to directly copy according to the dst stride and pixel_stride values if
+ // it is none of the above cases.
- result = libyuv::NV12ToI420Rotate(src_y_ptr,
- src_stride_y,
- plane_uv,
- halfwidth * 2,
- rotated_y_ptr,
- rotated_stride_y,
- rotated_u_ptr,
- rotated_stride_u,
- rotated_v_ptr,
- rotated_stride_v,
- width,
- height,
- mode);
- free_aligned_buffer_64(plane_uv);
- }
-
- if (result == 0) {
// Y
uint8_t *dst_y = rotated_y_ptr;
int rotated_pixel_stride_y = 1;
@@ -625,7 +635,6 @@
dst_y[i * rotated_stride_y + j * rotated_pixel_stride_y];
}
}
-
// U
uint8_t *dst_u = rotated_u_ptr;
int rotated_pixel_stride_u = 1;
@@ -635,7 +644,6 @@
dst_u[i * rotated_stride_u + j * rotated_pixel_stride_u];
}
}
-
// V
uint8_t *dst_v = rotated_v_ptr;
int rotated_pixel_stride_v = 1;
@@ -650,4 +658,36 @@
return result;
}
+JNIEXPORT jint Java_androidx_camera_core_ImageProcessingUtil_nativeGetYUVImageVUOff(
+ JNIEnv* env,
+ jclass,
+ jobject byte_buffer_v,
+ jobject byte_buffer_u) {
+ uint8_t *byte_buffer_v_ptr =
+ static_cast<uint8_t *>(env->GetDirectBufferAddress(byte_buffer_v));
+ uint8_t *byte_buffer_u_ptr =
+ static_cast<uint8_t *>(env->GetDirectBufferAddress(byte_buffer_u));
+ return byte_buffer_v_ptr - byte_buffer_u_ptr;
+}
+
+JNIEXPORT jobject Java_androidx_camera_core_ImageProcessingUtil_nativeCreateNV21ByteBuffers(
+ JNIEnv *env,
+ jclass,
+ jobject byte_buffer,
+ jint vu_data_length) {
+
+ uint8_t *byte_buffer_ptr =
+ static_cast<uint8_t *>(env->GetDirectBufferAddress(byte_buffer));
+
+ // Create the ByteBuffers
+ jobject vByteBuffer = env->NewDirectByteBuffer(byte_buffer_ptr, vu_data_length);
+ jobject uByteBuffer = env->NewDirectByteBuffer(byte_buffer_ptr + 1, vu_data_length);
+
+ jclass pairClass = env->FindClass("android/util/Pair");
+ jmethodID pairConstructor = env->GetMethodID(pairClass, "<init>",
+ "(Ljava/lang/Object;Ljava/lang/Object;)V");
+
+ return env->NewObject(pairClass, pairConstructor, uByteBuffer, vByteBuffer);
+}
+
} // extern "C"
\ No newline at end of file
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
index 8059ca7..31a690e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
@@ -185,6 +185,35 @@
public static final int OUTPUT_IMAGE_FORMAT_RGBA_8888 = 2;
/**
+ * Images sent to the analyzer will be formatted in NV21.
+ *
+ * <p>All {@link ImageProxy} sent to {@link Analyzer#analyze(ImageProxy)} will be in
+ * {@link ImageFormat#YUV_420_888} format with their image data formatted in NV21.
+ *
+ * <p>The output {@link ImageProxy} has three planes with the order of Y, U, V. The pixel
+ * stride of U or V planes are 2. The byte buffer pointer position of V plane will be ahead
+ * of the position of the U plane. Applications can directly read the <code>plane[2]</code>
+ * to get all the VU interleaved data.
+ *
+ * <p>Due to limitations on some Android devices in producing images in NV21 format, the
+ * {@link android.media.Image} object obtained from {@link ImageProxy#getImage()} will be the
+ * original image produced by the camera capture pipeline. This may result in discrepancies
+ * between the {@link android.media.Image} and the {@link ImageProxy}, such as:
+ *
+ * <ul>
+ * <li>Plane data may differ.
+ * <li>Width and height may differ.
+ * <li>Other properties may also differ.
+ * </ul>
+ *
+ * <p>Developers should be aware of these potential differences and use the properties from the
+ * {@link ImageProxy} when necessary.
+ *
+ * @see Builder#setOutputImageFormat(int)
+ */
+ public static final int OUTPUT_IMAGE_FORMAT_NV21 = 3;
+
+ /**
* Provides a static configuration with implementation-agnostic options.
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@@ -376,6 +405,8 @@
boolean isYuv2Rgb = getImageFormat() == ImageFormat.YUV_420_888
&& getOutputImageFormat() == OUTPUT_IMAGE_FORMAT_RGBA_8888;
+ boolean isYuv2Nv21 = getImageFormat() == ImageFormat.YUV_420_888
+ && getOutputImageFormat() == OUTPUT_IMAGE_FORMAT_NV21;
boolean isYuvRotationOrPixelShift = getImageFormat() == ImageFormat.YUV_420_888
&& ((getCamera() != null && getRelativeRotation(getCamera()) != 0)
|| Boolean.TRUE.equals(getOnePixelShiftEnabled()));
@@ -384,7 +415,7 @@
// supporting RGB natively. The logic here will check if the specific configured size is
// available in RGB and if not, fall back to YUV-RGB conversion.
final SafeCloseImageReaderProxy processedImageReaderProxy =
- (isYuv2Rgb || isYuvRotationOrPixelShift)
+ (isYuv2Rgb || (isYuvRotationOrPixelShift && !isYuv2Nv21))
? new SafeCloseImageReaderProxy(
ImageReaderProxys.createIsolatedReader(
width,
@@ -658,8 +689,9 @@
* Gets output image format.
*
* <p>The returned image format will be
- * {@link ImageAnalysis#OUTPUT_IMAGE_FORMAT_YUV_420_888} or
- * {@link ImageAnalysis#OUTPUT_IMAGE_FORMAT_RGBA_8888}.
+ * {@link ImageAnalysis#OUTPUT_IMAGE_FORMAT_YUV_420_888},
+ * {@link ImageAnalysis#OUTPUT_IMAGE_FORMAT_RGBA_8888} or
+ * {@link ImageAnalysis#OUTPUT_IMAGE_FORMAT_NV21}.
*
* @return output image format.
* @see ImageAnalysis.Builder#setOutputImageFormat(int)
@@ -834,14 +866,16 @@
* Supported output image format for image analysis.
*
* <p>The supported output image format
- * is {@link ImageAnalysis#OUTPUT_IMAGE_FORMAT_YUV_420_888} and
- * {@link ImageAnalysis#OUTPUT_IMAGE_FORMAT_RGBA_8888}.
+ * is {@link ImageAnalysis#OUTPUT_IMAGE_FORMAT_YUV_420_888},
+ * {@link ImageAnalysis#OUTPUT_IMAGE_FORMAT_RGBA_8888} and
+ * {@link ImageAnalysis#OUTPUT_IMAGE_FORMAT_NV21}.
*
* <p>By default, {@link ImageAnalysis#OUTPUT_IMAGE_FORMAT_YUV_420_888} will be used.
*
* @see Builder#setOutputImageFormat(int)
*/
- @IntDef({OUTPUT_IMAGE_FORMAT_YUV_420_888, OUTPUT_IMAGE_FORMAT_RGBA_8888})
+ @IntDef({OUTPUT_IMAGE_FORMAT_YUV_420_888, OUTPUT_IMAGE_FORMAT_RGBA_8888,
+ OUTPUT_IMAGE_FORMAT_NV21})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(Scope.LIBRARY_GROUP)
public @interface OutputImageFormat {
@@ -1161,13 +1195,15 @@
* Sets output image format.
*
* <p>The supported output image format
- * is {@link OutputImageFormat#OUTPUT_IMAGE_FORMAT_YUV_420_888} and
- * {@link OutputImageFormat#OUTPUT_IMAGE_FORMAT_RGBA_8888}.
+ * is {@link OutputImageFormat#OUTPUT_IMAGE_FORMAT_YUV_420_888},
+ * {@link OutputImageFormat#OUTPUT_IMAGE_FORMAT_RGBA_8888} and
+ * {@link OutputImageFormat#OUTPUT_IMAGE_FORMAT_NV21}.
*
* <p>If not set, {@link OutputImageFormat#OUTPUT_IMAGE_FORMAT_YUV_420_888} will be used.
*
- * Requesting {@link OutputImageFormat#OUTPUT_IMAGE_FORMAT_RGBA_8888} will have extra
- * overhead because format conversion takes time.
+ * Requesting {@link OutputImageFormat#OUTPUT_IMAGE_FORMAT_RGBA_8888} or
+ * {@link OutputImageFormat#OUTPUT_IMAGE_FORMAT_NV21} will have extra overhead because
+ * format conversion takes time.
*
* @param outputImageFormat The output image format.
* @return The current Builder.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysisAbstractAnalyzer.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysisAbstractAnalyzer.java
index c82db31..afbb654 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysisAbstractAnalyzer.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysisAbstractAnalyzer.java
@@ -16,10 +16,12 @@
package androidx.camera.core;
+import static androidx.camera.core.ImageAnalysis.OUTPUT_IMAGE_FORMAT_NV21;
import static androidx.camera.core.ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888;
import static androidx.camera.core.ImageProcessingUtil.applyPixelShiftForYUV;
import static androidx.camera.core.ImageProcessingUtil.convertYUVToRGB;
import static androidx.camera.core.ImageProcessingUtil.rotateYUV;
+import static androidx.camera.core.ImageProcessingUtil.rotateYUVAndConvertToNV21;
import static androidx.camera.core.impl.utils.TransformUtils.NORMALIZED_RECT;
import static androidx.camera.core.impl.utils.TransformUtils.getNormalizedToBuffer;
@@ -109,6 +111,12 @@
@GuardedBy("mAnalyzerLock")
@VisibleForTesting @Nullable ByteBuffer mVRotatedBuffer;
+ @GuardedBy("mAnalyzerLock")
+ @VisibleForTesting @Nullable ByteBuffer mNV21YDelegatedBuffer;
+
+ @GuardedBy("mAnalyzerLock")
+ @VisibleForTesting @Nullable ByteBuffer mNV21UVDelegatedBuffer;
+
// Lock that synchronizes the access to mSubscribedAnalyzer/mUserExecutor to prevent mismatch.
private final Object mAnalyzerLock = new Object();
@@ -168,6 +176,8 @@
ByteBuffer yRotatedBuffer;
ByteBuffer uRotatedBuffer;
ByteBuffer vRotatedBuffer;
+ ByteBuffer nv21YDelegatedBuffer;
+ ByteBuffer nv21UVDelegatedBuffer;
int currentBufferRotationDegrees = mOutputImageRotationEnabled ? mRelativeRotation : 0;
boolean outputImageDirty;
@@ -187,7 +197,7 @@
}
// Cache memory buffer for image rotation
- if (mOutputImageRotationEnabled) {
+ if (mOutputImageRotationEnabled || mOutputImageFormat == OUTPUT_IMAGE_FORMAT_NV21) {
createHelperBuffer(imageProxy);
}
@@ -197,6 +207,8 @@
yRotatedBuffer = mYRotatedBuffer;
uRotatedBuffer = mURotatedBuffer;
vRotatedBuffer = mVRotatedBuffer;
+ nv21YDelegatedBuffer = mNV21YDelegatedBuffer;
+ nv21UVDelegatedBuffer = mNV21UVDelegatedBuffer;
}
ListenableFuture<Void> future;
@@ -232,6 +244,25 @@
currentBufferRotationDegrees);
}
}
+ } else if (mOutputImageFormat == OUTPUT_IMAGE_FORMAT_NV21) {
+ // Apply one pixel shift before other processing, e.g. rotation.
+ if (mOnePixelShiftEnabled) {
+ applyPixelShiftForYUV(imageProxy);
+ }
+ if (yRotatedBuffer != null
+ && uRotatedBuffer != null
+ && vRotatedBuffer != null
+ && nv21YDelegatedBuffer != null
+ && nv21UVDelegatedBuffer != null) {
+ processedImageProxy = rotateYUVAndConvertToNV21(
+ imageProxy,
+ yRotatedBuffer,
+ uRotatedBuffer,
+ vRotatedBuffer,
+ nv21YDelegatedBuffer,
+ nv21UVDelegatedBuffer,
+ currentBufferRotationDegrees);
+ }
}
// Flag to indicate YUV2RGB conversion or YUV/RGB rotation failed, not including one
@@ -346,7 +377,6 @@
synchronized (mAnalyzerLock) {
mProcessedImageReaderProxy = processedImageReaderProxy;
}
-
}
void setAnalyzer(@Nullable Executor userExecutor,
@@ -378,7 +408,8 @@
@GuardedBy("mAnalyzerLock")
private void createHelperBuffer(@NonNull ImageProxy imageProxy) {
- if (mOutputImageFormat == ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888) {
+ if (mOutputImageFormat == ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888
+ || mOutputImageFormat == OUTPUT_IMAGE_FORMAT_NV21) {
if (mYRotatedBuffer == null) {
mYRotatedBuffer = ByteBuffer.allocateDirect(
imageProxy.getWidth() * imageProxy.getHeight());
@@ -396,6 +427,20 @@
imageProxy.getWidth() * imageProxy.getHeight() / 4);
}
mVRotatedBuffer.position(0);
+ if (mOutputImageFormat == OUTPUT_IMAGE_FORMAT_NV21) {
+ if (mNV21YDelegatedBuffer == null) {
+ mNV21YDelegatedBuffer = ByteBuffer.allocateDirect(
+ imageProxy.getWidth() * imageProxy.getHeight());
+ }
+ mNV21YDelegatedBuffer.position(0);
+ if (mNV21UVDelegatedBuffer == null) {
+ // This will be converted into U, V child ByteBuffers for NV21 format
+ // conversion process
+ mNV21UVDelegatedBuffer = ByteBuffer.allocateDirect(
+ imageProxy.getWidth() * imageProxy.getHeight() / 2);
+ }
+ mNV21UVDelegatedBuffer.position(0);
+ }
} else if (mOutputImageFormat == OUTPUT_IMAGE_FORMAT_RGBA_8888) {
if (mRGBConvertedBuffer == null) {
mRGBConvertedBuffer = ByteBuffer.allocateDirect(
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
index a7159e1..432d856 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
@@ -25,6 +25,7 @@
import android.media.ImageWriter;
import android.os.Build;
import android.util.Log;
+import android.util.Pair;
import android.view.Surface;
import androidx.annotation.IntRange;
@@ -388,6 +389,92 @@
return wrappedRotatedImageProxy;
}
+
+ /**
+ * Rotates YUV image proxy and output the NV21 format image proxy with the delegated byte
+ * buffers.
+ *
+ * @param imageProxy input image proxy.
+ * @param yRotatedBuffer intermediate image buffer for y plane rotation.
+ * @param uRotatedBuffer intermediate image buffer for u plane rotation.
+ * @param vRotatedBuffer intermediate image buffer for v plane rotation.
+ * @param nv21YDelegatedBuffer delegated image buffer for y plane.
+ * @param nv21UVDelegatedBuffer delegated image buffer for u/v plane.
+ * @param rotationDegrees output image rotation degrees.
+ * @return rotated image proxy or null if rotation fails or format is not supported.
+ */
+ public static @Nullable ImageProxy rotateYUVAndConvertToNV21(
+ @NonNull ImageProxy imageProxy,
+ @NonNull ByteBuffer yRotatedBuffer,
+ @NonNull ByteBuffer uRotatedBuffer,
+ @NonNull ByteBuffer vRotatedBuffer,
+ @NonNull ByteBuffer nv21YDelegatedBuffer,
+ @NonNull ByteBuffer nv21UVDelegatedBuffer,
+ @IntRange(from = 0, to = 359) int rotationDegrees) {
+ if (!isSupportedYUVFormat(imageProxy)) {
+ Logger.e(TAG, "Unsupported format for rotate YUV");
+ return null;
+ }
+
+ if (!isSupportedRotationDegrees(rotationDegrees)) {
+ Logger.e(TAG, "Unsupported rotation degrees for rotate YUV");
+ return null;
+ }
+
+ // If both rotation and format conversion processing are unnecessary, directly return here.
+ if (rotationDegrees == 0 && isNV21FormatImage(imageProxy)) {
+ return null;
+ }
+
+ int rotatedWidth =
+ (rotationDegrees % 180 == 0) ? imageProxy.getWidth() : imageProxy.getHeight();
+ int rotatedHeight =
+ (rotationDegrees % 180 == 0) ? imageProxy.getHeight() : imageProxy.getWidth();
+
+ Pair<ByteBuffer, ByteBuffer> nv21UVByteBuffers = nativeCreateNV21ByteBuffers(
+ nv21UVDelegatedBuffer, nv21UVDelegatedBuffer.capacity());
+
+ int result = nativeRotateYUV(
+ imageProxy.getPlanes()[0].getBuffer(),
+ imageProxy.getPlanes()[0].getRowStride(),
+ imageProxy.getPlanes()[1].getBuffer(),
+ imageProxy.getPlanes()[1].getRowStride(),
+ imageProxy.getPlanes()[2].getBuffer(),
+ imageProxy.getPlanes()[2].getRowStride(),
+ imageProxy.getPlanes()[2].getPixelStride(),
+ nv21YDelegatedBuffer,
+ rotatedWidth,
+ 1,
+ nv21UVByteBuffers.first,
+ rotatedWidth,
+ 2,
+ nv21UVByteBuffers.second,
+ rotatedWidth,
+ 2,
+ yRotatedBuffer,
+ uRotatedBuffer,
+ vRotatedBuffer,
+ imageProxy.getWidth(),
+ imageProxy.getHeight(),
+ rotationDegrees);
+
+ if (result != 0) {
+ Logger.e(TAG, "rotate YUV failure");
+ return null;
+ }
+
+ // Wraps to NV21ImageProxy to make sure that the returned v plane position is in front of u
+ // plane position.
+ return new SingleCloseImageProxy(
+ new NV21ImageProxy(imageProxy,
+ nv21YDelegatedBuffer,
+ nv21UVByteBuffers.first,
+ nv21UVByteBuffers.second,
+ rotatedWidth,
+ rotatedHeight,
+ rotationDegrees));
+ }
+
private static boolean isSupportedYUVFormat(@NonNull ImageProxy imageProxy) {
return imageProxy.getFormat() == ImageFormat.YUV_420_888
&& imageProxy.getPlanes().length == 3;
@@ -527,6 +614,116 @@
return SUCCESS;
}
+ /**
+ * Checks whether the image proxy data is formatted in NV21.
+ */
+ public static boolean isNV21FormatImage(@NonNull ImageProxy imageProxy) {
+ if (imageProxy.getPlanes().length != 3) {
+ return false;
+ }
+ if (imageProxy.getPlanes()[1].getPixelStride() != 2) {
+ return false;
+ }
+ return nativeGetYUVImageVUOff(
+ imageProxy.getPlanes()[2].getBuffer(),
+ imageProxy.getPlanes()[1].getBuffer()) == -1;
+ }
+
+ /**
+ * A wrapper to make sure that the returned v plane position (getPlanes()[2]) is in front of
+ * u plane position (getPlanes()[1]). So that the following operations can correctly check
+ * whether the format is NV12 or NV21 by the plane buffers' pointer positions.
+ *
+ * <p>The callers need to ensure that the v data is put in the plane with former position and v
+ * data is put in the plane with the later position in the associated image proxy.
+ */
+ private static class NV21ImageProxy extends ForwardingImageProxy {
+ private final ImageProxy.PlaneProxy[] mPlanes;
+ private final int mWidth;
+ private final int mHeight;
+
+ NV21ImageProxy(@NonNull ImageProxy imageProxy,
+ @NonNull ByteBuffer delegateBufferY,
+ @NonNull ByteBuffer delegateBufferU,
+ @NonNull ByteBuffer delegateBufferV,
+ int width, int height,
+ @IntRange(from = 0, to = 359) int rotatedRotationDegrees) {
+ super(imageProxy);
+ mPlanes = createPlanes(delegateBufferY, delegateBufferU, delegateBufferV, width);
+ mWidth = width;
+ mHeight = height;
+ }
+
+ @Override
+ public int getHeight() {
+ return mHeight;
+ }
+
+ @Override
+ public int getWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public ImageProxy.PlaneProxy @NonNull [] getPlanes() {
+ return mPlanes;
+ }
+
+ private ImageProxy.PlaneProxy @NonNull [] createPlanes(
+ @NonNull ByteBuffer delegateBufferY,
+ @NonNull ByteBuffer delegateBufferU,
+ @NonNull ByteBuffer delegateBufferV,
+ int rowStride
+ ) {
+ ImageProxy.PlaneProxy[] planes = new ImageProxy.PlaneProxy[3];
+ planes[0] = new ImageProxy.PlaneProxy() {
+ @Override
+ public int getRowStride() {
+ return rowStride;
+ }
+
+ @Override
+ public int getPixelStride() {
+ return 1;
+ }
+
+ @Override
+ public @NonNull ByteBuffer getBuffer() {
+ return delegateBufferY;
+ }
+ };
+ planes[1] = new NV21PlaneProxy(delegateBufferU, rowStride);
+ planes[2] = new NV21PlaneProxy(delegateBufferV, rowStride);
+
+ return planes;
+ }
+ }
+
+ private static class NV21PlaneProxy implements ImageProxy.PlaneProxy {
+ private final ByteBuffer mByteBuffer;
+ private final int mRowStride;
+
+ NV21PlaneProxy(@NonNull ByteBuffer byteBuffer, int rowStride) {
+ mByteBuffer = byteBuffer;
+ mRowStride = rowStride;
+ }
+
+ @Override
+ public int getRowStride() {
+ return mRowStride;
+ }
+
+ @Override
+ public int getPixelStride() {
+ // Force return pixel stride value 2
+ return 2;
+ }
+
+ @Override
+ public @NonNull ByteBuffer getBuffer() {
+ return mByteBuffer;
+ }
+ }
private static native int nativeCopyBetweenByteBufferAndBitmap(Bitmap bitmap,
ByteBuffer byteBuffer,
@@ -607,4 +804,14 @@
int width,
int height,
@ImageOutputConfig.RotationDegreesValue int rotationDegrees);
+
+ private static native int nativeGetYUVImageVUOff(
+ @NonNull ByteBuffer srcByteBufferV,
+ @NonNull ByteBuffer srcByteBufferU
+ );
+
+ private static native Pair<ByteBuffer, ByteBuffer> nativeCreateNV21ByteBuffers(
+ @NonNull ByteBuffer byteBuffer,
+ int vuDataLength
+ );
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageAnalysisConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageAnalysisConfig.java
index 8581ffb..ebaf412 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageAnalysisConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageAnalysisConfig.java
@@ -135,8 +135,9 @@
* Returns the output image format for image analysis.
*
* <p>The supported output image format
- * is {@link ImageAnalysis.OutputImageFormat#OUTPUT_IMAGE_FORMAT_YUV_420_888} and
- * {@link ImageAnalysis.OutputImageFormat#OUTPUT_IMAGE_FORMAT_RGBA_8888}.
+ * is {@link ImageAnalysis.OutputImageFormat#OUTPUT_IMAGE_FORMAT_YUV_420_888},
+ * {@link ImageAnalysis.OutputImageFormat#OUTPUT_IMAGE_FORMAT_RGBA_8888} and
+ * {@link ImageAnalysis#OUTPUT_IMAGE_FORMAT_NV21}.
*
* @param valueIfMissing The value to return if this configuration option has not been set.
* @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompat.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompat.java
index 5774ad7..ed59b2a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompat.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompat.java
@@ -19,6 +19,7 @@
import android.media.Image;
import android.media.ImageWriter;
import android.os.Build;
+import android.os.Handler;
import android.view.Surface;
import androidx.annotation.IntRange;
@@ -26,6 +27,8 @@
import org.jspecify.annotations.NonNull;
+import java.util.concurrent.Executor;
+
/**
* Helper for accessing features of {@link ImageWriter} in a backwards compatible fashion.
*/
@@ -157,6 +160,25 @@
}
/**
+ * Sets an {@link ImageWriter.OnImageReleasedListener} to be notified when an {@link Image} is
+ * released from the {@link ImageWriter}.
+ *
+ * <p>This method is a compatibility wrapper for
+ * {@link ImageWriter#setOnImageReleasedListener(ImageWriter.OnImageReleasedListener, Handler)}.
+ *
+ * @param imageWriter The {@link ImageWriter} to set the listener on.
+ * @param releasedListener The {@link ImageWriter.OnImageReleasedListener} to be notified when
+ * an image is released.
+ * @param executor The {@link Executor} on which the listener should be invoked.
+ */
+ public static void setOnImageReleasedListener(@NonNull ImageWriter imageWriter,
+ ImageWriter.@NonNull OnImageReleasedListener releasedListener, @NonNull
+ Executor executor) {
+ ImageWriterCompatApi23Impl.setOnImageReleasedListener(imageWriter, releasedListener,
+ executor);
+ }
+
+ /**
* Close the existing ImageWriter instance.
*
* @param imageWriter ImageWriter instance.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompatApi23Impl.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompatApi23Impl.java
index af28bfdd..f083c14 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompatApi23Impl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompatApi23Impl.java
@@ -22,9 +22,12 @@
import androidx.annotation.IntRange;
import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.utils.MainThreadAsyncHandler;
import org.jspecify.annotations.NonNull;
+import java.util.concurrent.Executor;
+
@RequiresApi(23)
final class ImageWriterCompatApi23Impl {
@@ -45,6 +48,14 @@
imageWriter.close();
}
+ static void setOnImageReleasedListener(@NonNull ImageWriter imageWriter,
+ ImageWriter.@NonNull OnImageReleasedListener releasedListener,
+ @NonNull Executor executor) {
+ imageWriter.setOnImageReleasedListener(
+ writer -> executor.execute(() -> releasedListener.onImageReleased(writer)),
+ MainThreadAsyncHandler.getInstance());
+ }
+
// Class should not be instantiated.
private ImageWriterCompatApi23Impl() {
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ZslRingBuffer.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ZslRingBuffer.java
index c7bbcae..f799573 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ZslRingBuffer.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ZslRingBuffer.java
@@ -56,6 +56,9 @@
private boolean isValidZslFrame(@NonNull ImageInfo imageInfo) {
CameraCaptureResult cameraCaptureResult =
CameraCaptureResults.retrieveCameraCaptureResult(imageInfo);
+ if (cameraCaptureResult == null) {
+ return false;
+ }
if (cameraCaptureResult.getAfState() != AfState.LOCKED_FOCUSED
&& cameraCaptureResult.getAfState() != AfState.PASSIVE_FOCUSED) {
diff --git a/camera/camera-effects-still-portrait/api/1.3.0-beta01.txt b/camera/camera-effects-still-portrait/api/1.3.0-beta01.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/camera/camera-effects-still-portrait/api/1.3.0-beta01.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/camera/camera-effects-still-portrait/api/1.3.0-beta02.txt b/camera/camera-effects-still-portrait/api/1.3.0-beta02.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/camera/camera-effects-still-portrait/api/1.3.0-beta02.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/camera/camera-effects-still-portrait/api/1.3.0-beta03.txt b/camera/camera-effects-still-portrait/api/1.3.0-beta03.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/camera/camera-effects-still-portrait/api/1.3.0-beta03.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/camera/camera-effects-still-portrait/api/1.4.0-beta01.txt b/camera/camera-effects-still-portrait/api/1.4.0-beta01.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/camera/camera-effects-still-portrait/api/1.4.0-beta01.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/camera/camera-effects-still-portrait/api/1.4.0-beta02.txt b/camera/camera-effects-still-portrait/api/1.4.0-beta02.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/camera/camera-effects-still-portrait/api/1.4.0-beta02.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/camera/camera-effects-still-portrait/api/1.4.0-beta03.txt b/camera/camera-effects-still-portrait/api/1.4.0-beta03.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/camera/camera-effects-still-portrait/api/1.4.0-beta03.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/camera/camera-effects-still-portrait/api/res-1.3.0-beta01.txt b/camera/camera-effects-still-portrait/api/res-1.3.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/camera/camera-effects-still-portrait/api/res-1.3.0-beta01.txt
+++ /dev/null
diff --git a/camera/camera-effects-still-portrait/api/res-1.3.0-beta02.txt b/camera/camera-effects-still-portrait/api/res-1.3.0-beta02.txt
deleted file mode 100644
index e69de29..0000000
--- a/camera/camera-effects-still-portrait/api/res-1.3.0-beta02.txt
+++ /dev/null
diff --git a/camera/camera-effects-still-portrait/api/res-1.3.0-beta03.txt b/camera/camera-effects-still-portrait/api/res-1.3.0-beta03.txt
deleted file mode 100644
index e69de29..0000000
--- a/camera/camera-effects-still-portrait/api/res-1.3.0-beta03.txt
+++ /dev/null
diff --git a/camera/camera-effects-still-portrait/api/res-1.4.0-beta01.txt b/camera/camera-effects-still-portrait/api/res-1.4.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/camera/camera-effects-still-portrait/api/res-1.4.0-beta01.txt
+++ /dev/null
diff --git a/camera/camera-effects-still-portrait/api/res-1.4.0-beta02.txt b/camera/camera-effects-still-portrait/api/res-1.4.0-beta02.txt
deleted file mode 100644
index e69de29..0000000
--- a/camera/camera-effects-still-portrait/api/res-1.4.0-beta02.txt
+++ /dev/null
diff --git a/camera/camera-effects-still-portrait/api/res-1.4.0-beta03.txt b/camera/camera-effects-still-portrait/api/res-1.4.0-beta03.txt
deleted file mode 100644
index e69de29..0000000
--- a/camera/camera-effects-still-portrait/api/res-1.4.0-beta03.txt
+++ /dev/null
diff --git a/camera/camera-effects-still-portrait/api/restricted_1.3.0-beta01.txt b/camera/camera-effects-still-portrait/api/restricted_1.3.0-beta01.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/camera/camera-effects-still-portrait/api/restricted_1.3.0-beta01.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/camera/camera-effects-still-portrait/api/restricted_1.3.0-beta02.txt b/camera/camera-effects-still-portrait/api/restricted_1.3.0-beta02.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/camera/camera-effects-still-portrait/api/restricted_1.3.0-beta02.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/camera/camera-effects-still-portrait/api/restricted_1.3.0-beta03.txt b/camera/camera-effects-still-portrait/api/restricted_1.3.0-beta03.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/camera/camera-effects-still-portrait/api/restricted_1.3.0-beta03.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/camera/camera-effects-still-portrait/api/restricted_1.4.0-beta01.txt b/camera/camera-effects-still-portrait/api/restricted_1.4.0-beta01.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/camera/camera-effects-still-portrait/api/restricted_1.4.0-beta01.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/camera/camera-effects-still-portrait/api/restricted_1.4.0-beta02.txt b/camera/camera-effects-still-portrait/api/restricted_1.4.0-beta02.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/camera/camera-effects-still-portrait/api/restricted_1.4.0-beta02.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/camera/camera-effects-still-portrait/api/restricted_1.4.0-beta03.txt b/camera/camera-effects-still-portrait/api/restricted_1.4.0-beta03.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/camera/camera-effects-still-portrait/api/restricted_1.4.0-beta03.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/camera/camera-effects-still-portrait/api/restricted_current.txt b/camera/camera-effects-still-portrait/api/restricted_current.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/camera/camera-effects-still-portrait/api/restricted_current.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/camera/camera-effects-still-portrait/build.gradle b/camera/camera-effects-still-portrait/build.gradle
deleted file mode 100644
index 4217887..0000000
--- a/camera/camera-effects-still-portrait/build.gradle
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright 2022 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
- *
- * http://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.
- */
-import androidx.build.Publish
-import androidx.build.RunApiTasks
-
-plugins {
- id("AndroidXPlugin")
- id("com.android.library")
- id("kotlin-android")
-}
-dependencies {
- api(libs.jspecify)
- api(project(":camera:camera-core"))
-}
-android {
- testOptions.unitTests.includeAndroidResources = true
- namespace = "androidx.camera.effects.stillportrait"
-}
-androidx {
- name = "Camera Effects: Still Portrait"
- publish = Publish.SNAPSHOT_ONLY
- inceptionYear = "2022"
- runApiTasks = new RunApiTasks.Yes()
- description = "A post-processing effect that works with CameraX Library, providing a portrait" +
- " mode effect that applies to still image captures."
-}
diff --git a/camera/camera-effects-still-portrait/src/main/java/androidx/camera/effects/stillportrait/StillPortrait.java b/camera/camera-effects-still-portrait/src/main/java/androidx/camera/effects/stillportrait/StillPortrait.java
deleted file mode 100644
index 399dd04..0000000
--- a/camera/camera-effects-still-portrait/src/main/java/androidx/camera/effects/stillportrait/StillPortrait.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2022 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
- *
- * http://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 androidx.camera.effects.stillportrait;
-
-import androidx.annotation.RestrictTo;
-import androidx.camera.core.CameraEffect;
-import androidx.camera.core.SurfaceProcessor;
-import androidx.camera.core.UseCase;
-
-import org.jspecify.annotations.NonNull;
-
-import java.util.concurrent.Executor;
-
-/**
- * Provides a portrait post-processing effect.
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class StillPortrait extends CameraEffect {
-
- /**
- * @param targets the target {@link UseCase} to which this effect should be applied.
- * @param processorExecutor the {@link Executor} on which the processor will be invoked.
- * @param surfaceProcessor a {@link SurfaceProcessor} implementation.
- */
- protected StillPortrait(int targets,
- @NonNull Executor processorExecutor,
- @NonNull SurfaceProcessor surfaceProcessor) {
- super(targets, processorExecutor, surfaceProcessor, throwable -> {
- });
- // TODO: implement this.
- }
-}
diff --git a/camera/camera-feature-combination-query-play-services/build.gradle b/camera/camera-feature-combination-query-play-services/build.gradle
index ef70110..0074f43 100644
--- a/camera/camera-feature-combination-query-play-services/build.gradle
+++ b/camera/camera-feature-combination-query-play-services/build.gradle
@@ -25,7 +25,6 @@
dependencies {
api(libs.jspecify)
api(libs.androidx.annotation)
- project(":camera:camera-feature-combination-query")
implementation(project(":camera:camera-feature-combination-query"))
testImplementation(libs.testRunner)
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CameraUtil.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CameraUtil.java
index d7bd592..a6be267 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CameraUtil.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CameraUtil.java
@@ -430,7 +430,6 @@
mCameraCaptureSession = openFuture.get(5, TimeUnit.SECONDS);
}
- @SuppressLint("ClassVerificationFailure")
@SuppressWarnings({"deprecation", "newApi", "unchecked"})
private @NonNull ListenableFuture<CameraCaptureSession> openCaptureSession(
@NonNull CameraDevice cameraDevice,
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/IgnoreProblematicDeviceRule.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/IgnoreProblematicDeviceRule.kt
index 416c89b..c3c69de 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/IgnoreProblematicDeviceRule.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/IgnoreProblematicDeviceRule.kt
@@ -71,6 +71,10 @@
isEmulator &&
avdName.contains("Pixel2", ignoreCase = true) &&
Build.VERSION.SDK_INT == Build.VERSION_CODES.O
+ public val isPixel2Api28Emulator: Boolean =
+ isEmulator &&
+ avdName.contains("Pixel2", ignoreCase = true) &&
+ Build.VERSION.SDK_INT == Build.VERSION_CODES.P
public val isPixel2Api30Emulator: Boolean =
isEmulator &&
avdName.contains("Pixel2", ignoreCase = true) &&
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/TestImageUtil.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/TestImageUtil.java
index a01450a..0c689dc 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/TestImageUtil.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/TestImageUtil.java
@@ -46,6 +46,7 @@
import org.jspecify.annotations.NonNull;
import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
/**
* Generates images for testing.
@@ -90,22 +91,78 @@
private TestImageUtil() {
}
-
/**
* Creates a [FakeImageProxy] with YUV format.
- *
- * TODO(b/245940015): fix the content of the image to match the value of {@link #createBitmap}.
*/
public static @NonNull FakeImageProxy createYuvFakeImageProxy(@NonNull ImageInfo imageInfo,
int width, int height) {
+ return createYuvFakeImageProxy(imageInfo, width, height, false, false);
+ }
+
+ /**
+ * Creates a [FakeImageProxy] with YUV format with the content of the image to match the
+ * value of {@link #createBitmap}.
+ */
+ @NonNull
+ public static FakeImageProxy createYuvFakeImageProxy(@NonNull ImageInfo imageInfo,
+ int width, int height, boolean flipUV, boolean insertRgbTestData) {
FakeImageProxy image = new FakeImageProxy(imageInfo);
image.setFormat(YUV_420_888);
- image.setPlanes(createYUV420ImagePlanes(width, height, 1, 1, false, false));
+ image.setPlanes(createYUV420ImagePlanes(width, height, 1, 1, flipUV, false));
image.setWidth(width);
image.setHeight(height);
+
+ // Directly returns the image if RGB test data is not needed.
+ if (!insertRgbTestData) {
+ return image;
+ }
+
+ Bitmap rgbBitmap = createBitmap(width, height);
+ writeBitmapToYuvByteBuffers(rgbBitmap,
+ image.getPlanes()[0].getBuffer(),
+ image.getPlanes()[1].getBuffer(),
+ image.getPlanes()[2].getBuffer());
+
return image;
}
+ private static void writeBitmapToYuvByteBuffers(
+ @NonNull Bitmap bitmap,
+ @NonNull ByteBuffer yByteBuffer,
+ @NonNull ByteBuffer uByteBuffer,
+ @NonNull ByteBuffer vByteBuffer) {
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ int[] argb = new int[width * height];
+ bitmap.getPixels(argb, 0, width, 0, 0, width, height);
+
+ int yIndex = 0;
+ int uvIndex = 0;
+
+ for (int j = 0; j < height; j++) {
+ for (int i = 0; i < width; i++) {
+ int rgb = argb[j * width + i];
+ int r = (rgb >> 16) & 0xFF;
+ int g = (rgb >> 8) & 0xFF;
+ int b = rgb & 0xFF;
+
+ int y = (int) (0.299 * r + 0.587 * g + 0.114 * b);
+ int u = (int) (-0.169 * r - 0.331 * g + 0.5 * b + 128);
+ int v = (int) (0.5 * r - 0.419 * g - 0.081 * b + 128);
+
+ yByteBuffer.put(yIndex++, (byte) y);
+ if (j % 2 == 0 && i % 2 == 0) {
+ uByteBuffer.put(uvIndex, (byte) u);
+ vByteBuffer.put(uvIndex, (byte) v);
+ uvIndex++;
+ }
+ }
+ }
+ yByteBuffer.rewind();
+ uByteBuffer.rewind();
+ vByteBuffer.rewind();
+ }
+
/**
* Creates a [FakeImageProxy] with [RAW_SENSOR] format.
*/
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index 2e3fb06..f720b8e 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -1421,8 +1421,9 @@
* <p>If not set, {@link ImageAnalysis.OutputImageFormat#OUTPUT_IMAGE_FORMAT_YUV_420_888}
* will be used.
*
- * <p>Requesting {@link ImageAnalysis.OutputImageFormat#OUTPUT_IMAGE_FORMAT_RGBA_8888}
- * causes extra overhead because format conversion takes time.
+ * <p>Requesting {@link ImageAnalysis.OutputImageFormat#OUTPUT_IMAGE_FORMAT_RGBA_8888} or
+ * {@link ImageAnalysis.OutputImageFormat#OUTPUT_IMAGE_FORMAT_NV21} causes extra overhead
+ * because format conversion takes time.
*
* <p>Changing the value will reconfigure the camera, which may cause additional latency. To
* avoid this, set the value before controller is bound to lifecycle. If the value is changed
@@ -1449,8 +1450,9 @@
* Gets the output image format for {@link ImageAnalysis}.
*
* <p>The returned image format can be either
- * {@link ImageAnalysis#OUTPUT_IMAGE_FORMAT_YUV_420_888} or
- * {@link ImageAnalysis#OUTPUT_IMAGE_FORMAT_RGBA_8888}.
+ * {@link ImageAnalysis#OUTPUT_IMAGE_FORMAT_YUV_420_888},
+ * {@link ImageAnalysis#OUTPUT_IMAGE_FORMAT_RGBA_8888} or
+ * {@link ImageAnalysis#OUTPUT_IMAGE_FORMAT_NV21}.
*
* @see ImageAnalysis.Builder#setOutputImageFormat(int)
* @see ImageAnalysis.Builder#getOutputImageFormat()
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FlashTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FlashTest.kt
index 2e8fc9a..ce17ca5 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FlashTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FlashTest.kt
@@ -195,16 +195,19 @@
)
}
+ @LabTestRule.LabTestRearCamera
@Test
fun requestAeModeIsOnAlwaysFlash_whenCapturedWithFlashOn() {
verifyRequestAeOrFlashModeForFlashModeCapture(ImageCapture.FLASH_MODE_ON)
}
+ @LabTestRule.LabTestRearCamera
@Test
fun requestAeModeIsOnAutoFlash_whenCapturedWithFlashAuto() {
verifyRequestAeOrFlashModeForFlashModeCapture(ImageCapture.FLASH_MODE_AUTO)
}
+ @LabTestRule.LabTestRearCamera
@Test
fun flashEnabledInRequest_whenCapturedWithFlashOnAndSharedEffect() {
verifyRequestAeOrFlashModeForFlashModeCapture(
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageProcessingLatencyTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageProcessingLatencyTest.kt
index 83f9d1d..ecd51a6 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageProcessingLatencyTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageProcessingLatencyTest.kt
@@ -21,6 +21,7 @@
import androidx.camera.camera2.Camera2Config
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageAnalysis.OUTPUT_IMAGE_FORMAT_NV21
import androidx.camera.core.ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888
import androidx.camera.core.Logger
import androidx.camera.core.internal.CameraUseCaseAdapter
@@ -90,18 +91,30 @@
@LabTestRule.LabTestRearCamera
@Test
- fun imageProcessingMeasurementViaRearCamera() {
- measureImageProcessing(CameraSelector.LENS_FACING_BACK)
+ fun imageProcessingMeasurementViaRearCamera_RGBA_8888() {
+ measureImageProcessing(CameraSelector.LENS_FACING_BACK, OUTPUT_IMAGE_FORMAT_RGBA_8888)
}
@LabTestRule.LabTestFrontCamera
@Test
- fun imageProcessingMeasurementViaFrontCamera() {
- measureImageProcessing(CameraSelector.LENS_FACING_FRONT)
+ fun imageProcessingMeasurementViaFrontCamera_RGBA_8888() {
+ measureImageProcessing(CameraSelector.LENS_FACING_FRONT, OUTPUT_IMAGE_FORMAT_RGBA_8888)
+ }
+
+ @LabTestRule.LabTestRearCamera
+ @Test
+ fun imageProcessingMeasurementViaRearCamera_NV21() {
+ measureImageProcessing(CameraSelector.LENS_FACING_BACK, OUTPUT_IMAGE_FORMAT_NV21)
+ }
+
+ @LabTestRule.LabTestFrontCamera
+ @Test
+ fun imageProcessingMeasurementViaFrontCamera_NV21() {
+ measureImageProcessing(CameraSelector.LENS_FACING_FRONT, OUTPUT_IMAGE_FORMAT_NV21)
}
@Suppress("DEPRECATION") // legacy resolution API
- private fun measureImageProcessing(lensFacing: Int): Unit = runBlocking {
+ private fun measureImageProcessing(lensFacing: Int, outputFormat: Int): Unit = runBlocking {
// The log is used to profile the ImageProcessing performance. The log parser identifies
// the log pattern "Image processing performance profiling" in the device output log.
Logger.d(
@@ -113,7 +126,7 @@
val countDownLatch = CountDownLatch(200)
val imageAnalyzer =
ImageAnalysis.Builder()
- .setOutputImageFormat(OUTPUT_IMAGE_FORMAT_RGBA_8888)
+ .setOutputImageFormat(outputFormat)
.setTargetResolution(targetResolution)
.build()
.also {
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
index f8a913b..defa125 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
@@ -17,6 +17,7 @@
import android.Manifest
import android.content.Context
+import android.util.Log
import androidx.camera.camera2.Camera2Config
import androidx.camera.camera2.pipe.integration.CameraPipeConfig
import androidx.camera.core.Camera
@@ -435,6 +436,28 @@
imageCapture.waitForCapturing()
}
+ @Test
+ fun previewImageCaptureZSL() {
+ // Arrange.
+ assumeTrue(cameraInfo.isZslSupported) // Only test when ZSL is supported
+
+ val imageCaptureZSL =
+ ImageCapture.Builder()
+ .apply { setCaptureMode(ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG) }
+ .build()
+ assumeTrue(camera.isUseCasesCombinationSupported(preview, imageCaptureZSL))
+ bindUseCases(preview, imageCaptureZSL)
+
+ // Capture images with ZSL and verify each capture.
+ for (i in 10 downTo 0) {
+ previewMonitor.waitForStream()
+ imageCaptureZSL.waitForCapturing()
+ Log.d("UseCaseCombinationTest", "Test ZSL capture round: $i")
+ // Verifies the preview is still outputting after capture
+ previewMonitor.waitForStream()
+ }
+ }
+
// Possible for QR code scanning use case.
@Test
fun sequentialBindPreviewAndImageAnalysis() {
diff --git a/camera/integration-tests/diagnosetestapp/build.gradle b/camera/integration-tests/diagnosetestapp/build.gradle
index a3e6917..70a9f1d 100644
--- a/camera/integration-tests/diagnosetestapp/build.gradle
+++ b/camera/integration-tests/diagnosetestapp/build.gradle
@@ -56,7 +56,7 @@
implementation(libs.material)
testImplementation(libs.robolectric)
testImplementation(libs.truth)
- testImplementation project(":camera:camera-testing")
+ testImplementation(project(":camera:camera-testing"))
testImplementation("androidx.test:core:1.4.0")
testImplementation("junit:junit:4.12")
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/Camera2ExtensionsUtil.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/Camera2ExtensionsUtil.kt
index 22ad5ce..df1fcba 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/Camera2ExtensionsUtil.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/Camera2ExtensionsUtil.kt
@@ -16,7 +16,6 @@
package androidx.camera.integration.extensions.utils
-import android.annotation.SuppressLint
import android.content.Context
import android.graphics.ImageFormat
import android.graphics.Point
@@ -100,7 +99,6 @@
throw IllegalArgumentException("Can't find camera of lens facing $lensFacing")
}
- @SuppressLint("ClassVerificationFailure")
@RequiresApi(31)
@JvmStatic
fun isCamera2ExtensionModeSupported(
@@ -117,7 +115,6 @@
* Picks a preview resolution that is both close/same as the display size and supported by
* camera and extensions.
*/
- @SuppressLint("ClassVerificationFailure")
@RequiresApi(Build.VERSION_CODES.S)
@JvmStatic
fun pickPreviewResolution(
@@ -208,7 +205,6 @@
}
/** Picks a resolution for still image capture. */
- @SuppressLint("ClassVerificationFailure")
@RequiresApi(Build.VERSION_CODES.S)
@JvmStatic
fun pickStillImageResolution(
diff --git a/camera/viewfinder/viewfinder-compose/build.gradle b/camera/viewfinder/viewfinder-compose/build.gradle
index d20e198..b28d873 100644
--- a/camera/viewfinder/viewfinder-compose/build.gradle
+++ b/camera/viewfinder/viewfinder-compose/build.gradle
@@ -51,9 +51,6 @@
android {
compileSdk = 35
namespace = "androidx.camera.viewfinder.compose"
-
- sourceSets.androidTest.assets.srcDirs +=
- project.rootDir.absolutePath + "/../../golden/camera/viewfinder/viewfinder-compose"
}
androidx {
@@ -62,4 +59,5 @@
inceptionYear = "2023"
description = "Standalone Composable Viewfinder for Camera"
legacyDisableKotlinStrictApiMode = true
+ addGoldenImageAssets()
}
diff --git a/camera/viewfinder/viewfinder-core/src/main/java/androidx/camera/viewfinder/core/ViewfinderSurfaceRequestExt.kt b/camera/viewfinder/viewfinder-core/src/main/java/androidx/camera/viewfinder/core/ViewfinderSurfaceRequestExt.kt
index 5379f22..a5eeb04 100644
--- a/camera/viewfinder/viewfinder-core/src/main/java/androidx/camera/viewfinder/core/ViewfinderSurfaceRequestExt.kt
+++ b/camera/viewfinder/viewfinder-core/src/main/java/androidx/camera/viewfinder/core/ViewfinderSurfaceRequestExt.kt
@@ -17,7 +17,6 @@
package androidx.camera.viewfinder.core
-import android.annotation.SuppressLint
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraMetadata
import androidx.camera.viewfinder.core.ViewfinderSurfaceRequest.Companion.MIRROR_MODE_HORIZONTAL
@@ -30,7 +29,6 @@
* orientation and [ImplementationMode]. If the hardware level is legacy, the [ImplementationMode]
* will be set to [ImplementationMode.EMBEDDED].
*/
-@SuppressLint("ClassVerificationFailure")
fun ViewfinderSurfaceRequest.Builder.populateFromCharacteristics(
cameraCharacteristics: CameraCharacteristics
): ViewfinderSurfaceRequest.Builder {
diff --git a/car/app/app-automotive/build.gradle b/car/app/app-automotive/build.gradle
index ebde05b..d9ce614 100644
--- a/car/app/app-automotive/build.gradle
+++ b/car/app/app-automotive/build.gradle
@@ -53,7 +53,7 @@
testImplementation(libs.robolectric)
testImplementation(libs.truth)
testImplementation("androidx.fragment:fragment-testing:1.2.3")
- testImplementation project(":car:app:app-testing")
+ testImplementation(project(":car:app:app-testing"))
}
android {
diff --git a/car/app/app-projected/build.gradle b/car/app/app-projected/build.gradle
index 557acde..746b59a 100644
--- a/car/app/app-projected/build.gradle
+++ b/car/app/app-projected/build.gradle
@@ -43,7 +43,7 @@
testImplementation(libs.mockitoCore4)
testImplementation(libs.robolectric)
testImplementation(libs.truth)
- testImplementation project(":car:app:app-testing")
+ testImplementation(project(":car:app:app-testing"))
}
android {
diff --git a/car/app/app-samples/navigation/common/build.gradle b/car/app/app-samples/navigation/common/build.gradle
index 5a4c90d..b3be643 100644
--- a/car/app/app-samples/navigation/common/build.gradle
+++ b/car/app/app-samples/navigation/common/build.gradle
@@ -40,7 +40,7 @@
implementation(project(":car:app:app"))
implementation("androidx.core:core:1.7.0")
- implementation project(":annotation:annotation-experimental")
+ implementation(project(":annotation:annotation-experimental"))
implementation("androidx.lifecycle:lifecycle-livedata:2.3.1")
implementation("androidx.activity:activity:1.2.3")
}
diff --git a/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/MicrophoneRecorder.java b/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/MicrophoneRecorder.java
index 8734b4f..a8144f0 100644
--- a/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/MicrophoneRecorder.java
+++ b/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/MicrophoneRecorder.java
@@ -27,7 +27,6 @@
import static androidx.car.app.media.CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE;
import static androidx.car.app.media.CarAudioRecord.AUDIO_CONTENT_SAMPLING_RATE;
-import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
@@ -87,7 +86,6 @@
recordingThread.start();
}
- @SuppressLint("ClassVerificationFailure") // runtime check for < API 26
@RequiresPermission(RECORD_AUDIO)
private void play(AudioFocusRequest audioFocusRequest) {
if (SDK_INT < VERSION_CODES.O) {
@@ -135,7 +133,6 @@
audioFocusRequest);
}
- @SuppressLint("ClassVerificationFailure") // runtime check for < API 26
@RequiresPermission(RECORD_AUDIO)
private void doRecord(CarAudioRecord record) {
if (SDK_INT < VERSION_CODES.O) {
diff --git a/car/app/app-samples/showcase/common/build.gradle b/car/app/app-samples/showcase/common/build.gradle
index c06017e..195e8bd 100644
--- a/car/app/app-samples/showcase/common/build.gradle
+++ b/car/app/app-samples/showcase/common/build.gradle
@@ -43,7 +43,7 @@
implementation(libs.kotlinStdlibCommon)
implementation("androidx.core:core:1.7.0")
- implementation project(":annotation:annotation-experimental")
+ implementation(project(":annotation:annotation-experimental"))
}
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/audio/VoiceInteraction.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/audio/VoiceInteraction.java
index 45dad85..093b4ff 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/audio/VoiceInteraction.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/audio/VoiceInteraction.java
@@ -27,7 +27,6 @@
import static androidx.car.app.media.CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE;
import static androidx.car.app.media.CarAudioRecord.AUDIO_CONTENT_SAMPLING_RATE;
-import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
@@ -69,7 +68,6 @@
* Starts recording the car microphone, then plays it back.
*/
@RequiresPermission(RECORD_AUDIO)
- @SuppressLint("ClassVerificationFailure") // runtime check for < API 26
public void voiceInteractionDemo() {
// Some of the functions for recording require API level 26, so verify that first
if (SDK_INT < VERSION_CODES.O) {
@@ -93,7 +91,6 @@
*/
@RequiresApi(api = VERSION_CODES.O)
@RequiresPermission(RECORD_AUDIO)
- @SuppressLint("ClassVerificationFailure") // runtime check for < API 26
public @NonNull Thread createRecordingThread() {
Thread recordingThread = new Thread(
() -> {
@@ -183,7 +180,6 @@
}
@RequiresApi(api = VERSION_CODES.O)
- @SuppressLint("ClassVerificationFailure") // runtime check for < API 26
@RequiresPermission(RECORD_AUDIO)
private void recordAudio(CarAudioRecord record) {
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/navigationdemos/NavigationNotificationsDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/navigationdemos/NavigationNotificationsDemoScreen.java
index fc10361..9390e7e 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/navigationdemos/NavigationNotificationsDemoScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/navigationdemos/NavigationNotificationsDemoScreen.java
@@ -43,7 +43,7 @@
// Suppressing 'ObsoleteSdkInt' as this code is shared between APKs with different min SDK
// levels
- @SuppressLint({"ObsoleteSdkInt", "ClassVerificationFailure"})
+ @SuppressLint("ObsoleteSdkInt")
@Override
public @NonNull Template onGetTemplate() {
ItemList.Builder listBuilder = new ItemList.Builder();
diff --git a/car/app/app/build.gradle b/car/app/app/build.gradle
index c262bae..f1b3cd5 100644
--- a/car/app/app/build.gradle
+++ b/car/app/app/build.gradle
@@ -80,7 +80,7 @@
testImplementation(libs.mockitoKotlin4)
testImplementation(libs.robolectric)
testImplementation(libs.truth)
- testImplementation project(":car:app:app-testing")
+ testImplementation(project(":car:app:app-testing"))
}
project.ext {
diff --git a/compose/animation/animation-core/benchmark/build.gradle b/compose/animation/animation-core/benchmark/build.gradle
index 838f0fe..e9ef268 100644
--- a/compose/animation/animation-core/benchmark/build.gradle
+++ b/compose/animation/animation-core/benchmark/build.gradle
@@ -24,10 +24,10 @@
dependencies {
- androidTestImplementation project(":compose:animation:animation-core")
- androidTestImplementation project(":benchmark:benchmark-junit4")
- androidTestImplementation project(":compose:runtime:runtime")
- androidTestImplementation project(":compose:benchmark-utils")
+ androidTestImplementation(project(":compose:animation:animation-core"))
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:benchmark-utils"))
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.kotlinTestCommon)
diff --git a/compose/animation/animation-core/build.gradle b/compose/animation/animation-core/build.gradle
index 4a6cdf1..ad24266 100644
--- a/compose/animation/animation-core/build.gradle
+++ b/compose/animation/animation-core/build.gradle
@@ -137,7 +137,7 @@
}
dependencies {
- lintPublish project(":compose:animation:animation-core-lint")
+ lintPublish(project(":compose:animation:animation-core-lint"))
}
androidx {
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index 56fd517..ab61e25 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -110,7 +110,7 @@
implementation(libs.truth)
implementation(libs.leakcanary)
implementation(libs.leakcanaryInstrumentation)
- implementation("androidx.compose.foundation:foundation:1.2.1")
+ implementation(project(":compose:foundation:foundation"))
implementation(project(":compose:material3:material3"))
implementation("androidx.compose.ui:ui-test-junit4:1.2.1")
implementation(project(":compose:test-utils"))
@@ -129,7 +129,7 @@
}
dependencies {
- lintPublish project(":compose:animation:animation-lint")
+ lintPublish(project(":compose:animation:animation-lint"))
}
androidx {
diff --git a/compose/animation/animation/integration-tests/animation-demos/build.gradle b/compose/animation/animation/integration-tests/animation-demos/build.gradle
index 5aaa416..014f1a5 100644
--- a/compose/animation/animation/integration-tests/animation-demos/build.gradle
+++ b/compose/animation/animation/integration-tests/animation-demos/build.gradle
@@ -31,8 +31,8 @@
implementation(project(":compose:material:material"))
implementation("androidx.compose.material:material-icons-core:1.6.7")
implementation(project(":compose:ui:ui-tooling-preview"))
- implementation project(":compose:material3:material3")
- implementation project(":navigation:navigation-compose")
+ implementation(project(":compose:material3:material3"))
+ implementation(project(":navigation:navigation-compose"))
implementation(project(":compose:ui:ui-tooling"))
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsInLazyGrid.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsInLazyGrid.kt
new file mode 100644
index 0000000..9878d2a
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsInLazyGrid.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.compose.animation.demos.lookahead
+
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.animateBounds
+import androidx.compose.animation.demos.layoutanimation.turquoiseColors
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.produceState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.delay
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Preview
+@Composable
+fun AnimateBoundsInLazyGrid() {
+ val width by
+ produceState(300.dp) {
+ while (true) {
+ delay(1000)
+ // Toggle between 300.dp and 400.dp every 1000ms
+ value = (700 - value.value).dp
+ }
+ }
+ LookaheadScope {
+ LazyVerticalGrid(
+ GridCells.Adaptive(160.dp),
+ Modifier.padding(3.dp).fillMaxHeight().width(width).border(2.dp, Color.Blue)
+ ) {
+ items(40, key = { it }) { id ->
+ Box(
+ Modifier.animateBounds(this@LookaheadScope)
+ .animateItem()
+ .padding(10.dp)
+ .background(
+ turquoiseColors[id % turquoiseColors.size],
+ RoundedCornerShape(10.dp)
+ )
+ .fillMaxWidth()
+ .aspectRatio(1f)
+ )
+ }
+ }
+ }
+}
diff --git a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimateBoundsTest.kt b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimateBoundsTest.kt
index 2b199c0..0e42b63 100644
--- a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimateBoundsTest.kt
+++ b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimateBoundsTest.kt
@@ -24,13 +24,18 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentWithReceiverOf
import androidx.compose.runtime.mutableStateOf
@@ -45,12 +50,15 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.boundsInParent
+import androidx.compose.ui.layout.findRootCoordinates
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.compose.ui.util.fastRoundToInt
import androidx.compose.ui.util.lerp
@@ -453,6 +461,109 @@
assertEquals(IntSize(itemBSizePx, itemBSizePx), boxSize)
}
+ /**
+ * Test that animateBounds constrains its returned measure size to the incoming constraints, so
+ * that parent layout does not center it.
+ */
+ @Test
+ fun animateBounds_inLazyLayout() {
+ var width by mutableStateOf(200.dp)
+ val positions = mutableListOf<Offset>()
+ var size: IntSize = IntSize.Zero
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides androidx.compose.ui.unit.Density(1f)) {
+ LookaheadScope {
+ LazyVerticalGrid(GridCells.Fixed(2), Modifier.fillMaxHeight().width(width)) {
+ items(5, key = { it }) { id ->
+ Box(
+ Modifier.animateBounds(this@LookaheadScope)
+ .then(
+ if (id == 0) {
+ Modifier.onGloballyPositioned {
+ positions.add(it.positionInRoot())
+ size = it.size
+ }
+ } else Modifier
+ )
+ .fillMaxWidth()
+ .aspectRatio(1f)
+ )
+ }
+ }
+ }
+ }
+ }
+ rule.waitForIdle()
+ rule.mainClock.autoAdvance = false
+ width = 400.dp
+ assertEquals(IntSize(100, 100), size)
+
+ while (size != IntSize(200, 200)) {
+ rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle()
+ assertEquals(Offset(0f, 0f), positions.last())
+ }
+ }
+
+ /**
+ * Test that items in LazyGrid keeps a consistent MFR throughout item node replacement triggered
+ * by animation.
+ */
+ @Test
+ fun motionFrameOfReferenceOfItemsInLazyGrid() {
+ var width by mutableStateOf(200.dp)
+ val MFRs = mutableListOf<Offset>()
+ var size: IntSize = IntSize.Zero
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides androidx.compose.ui.unit.Density(1f)) {
+ LookaheadScope {
+ LazyVerticalGrid(GridCells.Fixed(2), Modifier.fillMaxHeight().width(width)) {
+ items(5, key = { it }) { id ->
+ Box(
+ Modifier.animateBounds(this@LookaheadScope)
+ .then(
+ if (id == 1) {
+ Modifier.onGloballyPositioned {
+ val MFR =
+ it.findRootCoordinates()
+ .localPositionOf(
+ it,
+ includeMotionFrameOfReference = true
+ ) -
+ it.findRootCoordinates()
+ .localPositionOf(
+ it,
+ includeMotionFrameOfReference =
+ false
+ )
+ MFRs.add(MFR)
+ size = it.size
+ }
+ } else Modifier
+ )
+ .fillMaxWidth()
+ .aspectRatio(1f)
+ )
+ }
+ }
+ }
+ }
+ }
+ rule.waitForIdle()
+ rule.mainClock.autoAdvance = false
+ width = 400.dp
+ assertEquals(IntSize(100, 100), size)
+ MFRs.clear()
+
+ while (size != IntSize(200, 200)) {
+ rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle()
+
+ // This MFR should never change until the next width change
+ assertEquals(Offset(200f, 0f), MFRs.last())
+ }
+ }
+
@Test
fun animateBounds_scrollBehavior() =
with(rule.density) {
diff --git a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/SharedTransitionTest.kt b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/SharedTransitionTest.kt
index c02221fe..0593207 100644
--- a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/SharedTransitionTest.kt
+++ b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/SharedTransitionTest.kt
@@ -76,6 +76,7 @@
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.ScaleFactor
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.layout.approachLayout
@@ -2896,6 +2897,27 @@
rule.mainClock.advanceTimeByFrame()
rule.waitForIdle()
}
+
+ @Test
+ fun intrinsicsQueryComingFromAboveLookaheadRoot() {
+ var intrinsicWidth = 0
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Layout(
+ content = {
+ SharedTransitionLayout { Box(Modifier.skipToLookaheadSize().size(100.dp)) }
+ },
+ ) { measurables, constraints ->
+ val measurable = measurables[0]
+ intrinsicWidth = measurable.maxIntrinsicWidth(constraints.maxHeight)
+ val placeable = measurable.measure(constraints)
+ layout(constraints.maxWidth, constraints.maxHeight) { placeable.place(0, 0) }
+ }
+ }
+ }
+ rule.waitForIdle()
+ assertEquals(100, intrinsicWidth)
+ }
}
private fun assertEquals(a: IntSize, b: IntSize, delta: IntSize) {
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimateBoundsModifier.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimateBoundsModifier.kt
index 388b244..49ebf4c 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimateBoundsModifier.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimateBoundsModifier.kt
@@ -44,6 +44,7 @@
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.constrain
import androidx.compose.ui.unit.round
import androidx.compose.ui.unit.roundToIntSize
import androidx.compose.ui.unit.toSize
@@ -196,6 +197,8 @@
(animatedSize: IntSize, constraints: Constraints) -> Constraints,
var animateMotionFrameOfReference: Boolean,
) : ApproachLayoutModifierNode, Modifier.Node() {
+
+ private var directManipulationParentsDirty = true
private val boundsAnimation = BoundsTransformDeferredAnimation()
override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
@@ -205,6 +208,10 @@
return !boundsAnimation.isIdle
}
+ override fun onAttach() {
+ directManipulationParentsDirty = true
+ }
+
override fun Placeable.PlacementScope.isPlacementApproachInProgress(
lookaheadCoordinates: LayoutCoordinates
): Boolean {
@@ -213,9 +220,11 @@
lookaheadScope = lookaheadScope,
placementScope = this,
coroutineScope = coroutineScope,
+ directManipulationParentsDirty = directManipulationParentsDirty,
includeMotionFrameOfReference = animateMotionFrameOfReference,
boundsTransform = boundsTransform,
)
+ directManipulationParentsDirty = animateMotionFrameOfReference
return !boundsAnimation.isIdle
}
@@ -237,7 +246,13 @@
val chosenConstraints = onChooseMeasureConstraints(animatedSize, constraints)
val placeable = measurable.measure(chosenConstraints)
- return layout(animatedSize.width, animatedSize.height) {
+ // Constrain the animated size to the chosen constraints. This is important, particularly
+ // for the outer AnimateBoundsModifierElement, because we want to avoid parent layout
+ // placing this node in its center due to the non-conforming size. In that scenario, we'd
+ // have no guarantee that the parent's center-placement is animated, esp if the
+ // parent layout places this node using `withMotionFrameOfReferencePlacement`.
+ val (w, h) = chosenConstraints.constrain(animatedSize)
+ return layout(w, h) {
val animatedBounds = boundsAnimation.value
val positionInScope =
with(lookaheadScope) {
@@ -249,13 +264,12 @@
)
}
}
-
val topLeft =
if (animatedBounds != null) {
boundsAnimation.updateCurrentBounds(animatedBounds.topLeft, animatedBounds.size)
animatedBounds.topLeft
} else {
- boundsAnimation.currentBounds?.topLeft ?: Offset.Zero
+ (boundsAnimation.currentBounds?.topLeft ?: Offset.Zero)
}
val (x, y) = positionInScope?.let { topLeft - it } ?: Offset.Zero
placeable.place(x.fastRoundToInt(), y.fastRoundToInt())
@@ -339,6 +353,7 @@
lookaheadScope: LookaheadScope,
placementScope: Placeable.PlacementScope,
coroutineScope: CoroutineScope,
+ directManipulationParentsDirty: Boolean,
includeMotionFrameOfReference: Boolean,
boundsTransform: BoundsTransform,
) {
@@ -347,7 +362,7 @@
val lookaheadScopeCoordinates = placementScope.lookaheadScopeCoordinates
var delta = Offset.Zero
- if (!includeMotionFrameOfReference) {
+ if (!includeMotionFrameOfReference && directManipulationParentsDirty) {
// As the Layout changes, we need to keep track of the accumulated offset up
// the hierarchy tree, to get the proper Offset accounting for scrolling.
val parents = directManipulationParents ?: mutableListOf()
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedTransitionScope.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedTransitionScope.kt
index d3d8248..779033d 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedTransitionScope.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedTransitionScope.kt
@@ -65,30 +65,17 @@
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.addOutline
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
-import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.LookaheadScope
-import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.layout.MeasureScope
-import androidx.compose.ui.layout.ScaleFactor
import androidx.compose.ui.layout.layout
-import androidx.compose.ui.node.LayoutModifierNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.constrain
-import androidx.compose.ui.unit.toSize
import androidx.compose.ui.util.fastForEach
-import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -1150,91 +1137,6 @@
}
}
-private val DefaultEnabled: () -> Boolean = { true }
-
-@OptIn(ExperimentalSharedTransitionApi::class)
-private fun Modifier.createContentScaleModifier(
- scaleToBounds: ScaleToBoundsImpl,
- isEnabled: () -> Boolean
-): Modifier =
- this.then(
- if (scaleToBounds.contentScale == ContentScale.Crop) {
- Modifier.graphicsLayer { clip = isEnabled() }
- } else Modifier
- ) then SkipToLookaheadElement(scaleToBounds, isEnabled)
-
-@OptIn(ExperimentalSharedTransitionApi::class)
-private data class SkipToLookaheadElement(
- val scaleToBounds: ScaleToBoundsImpl? = null,
- val isEnabled: () -> Boolean = DefaultEnabled,
-) : ModifierNodeElement<SkipToLookaheadNode>() {
- override fun create(): SkipToLookaheadNode {
- return SkipToLookaheadNode(scaleToBounds, isEnabled)
- }
-
- override fun update(node: SkipToLookaheadNode) {
- node.scaleToBounds = scaleToBounds
- node.isEnabled = isEnabled
- }
-
- override fun InspectorInfo.inspectableProperties() {
- name = "skipToLookahead"
- properties["scaleToBounds"] = scaleToBounds
- properties["isEnabled"] = isEnabled
- }
-}
-
-@OptIn(ExperimentalSharedTransitionApi::class)
-private class SkipToLookaheadNode(scaleToBounds: ScaleToBoundsImpl?, isEnabled: () -> Boolean) :
- LayoutModifierNode, Modifier.Node() {
- var lookaheadConstraints: Constraints? = null
- var scaleToBounds: ScaleToBoundsImpl? by mutableStateOf(scaleToBounds)
- var isEnabled: () -> Boolean by mutableStateOf(isEnabled)
-
- override fun MeasureScope.measure(
- measurable: Measurable,
- constraints: Constraints
- ): MeasureResult {
- if (isLookingAhead) {
- lookaheadConstraints = constraints
- }
- val p = measurable.measure(lookaheadConstraints!!)
- val contentSize = IntSize(p.width, p.height)
- val constrainedSize = constraints.constrain(contentSize)
- return layout(constrainedSize.width, constrainedSize.height) {
- val scaleToBounds = scaleToBounds
- if (!isEnabled() || scaleToBounds == null) {
- p.place(0, 0)
- } else {
- val contentScale = scaleToBounds.contentScale
- val resolvedScale =
- if (contentSize.width == 0 || contentSize.height == 0) {
- ScaleFactor(1f, 1f)
- } else
- contentScale.computeScaleFactor(
- contentSize.toSize(),
- constrainedSize.toSize()
- )
-
- val (x, y) =
- scaleToBounds.alignment.align(
- IntSize(
- (contentSize.width * resolvedScale.scaleX).roundToInt(),
- (contentSize.height * resolvedScale.scaleY).roundToInt()
- ),
- constrainedSize,
- layoutDirection
- )
- p.placeWithLayer(x, y) {
- scaleX = resolvedScale.scaleX
- scaleY = resolvedScale.scaleY
- transformOrigin = TransformOrigin(0f, 0f)
- }
- }
- }
- }
-}
-
internal interface LayerRenderer {
val parentState: SharedElementInternalState?
@@ -1312,7 +1214,7 @@
@Immutable
@ExperimentalSharedTransitionApi
-private class ScaleToBoundsImpl(val contentScale: ContentScale, val alignment: Alignment) :
+internal class ScaleToBoundsImpl(val contentScale: ContentScale, val alignment: Alignment) :
ResizeMode
@ExperimentalSharedTransitionApi private object RemeasureImpl : ResizeMode
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SkipToLookaheadNode.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SkipToLookaheadNode.kt
new file mode 100644
index 0000000..f8d6aa4
--- /dev/null
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SkipToLookaheadNode.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.compose.animation
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.IntrinsicMeasurable
+import androidx.compose.ui.layout.IntrinsicMeasureScope
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.ScaleFactor
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.constrain
+import androidx.compose.ui.unit.toSize
+import kotlin.math.roundToInt
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+internal class SkipToLookaheadNode(scaleToBounds: ScaleToBoundsImpl?, isEnabled: () -> Boolean) :
+ LayoutModifierNode, Modifier.Node() {
+ var scaleToBounds: ScaleToBoundsImpl? by mutableStateOf(scaleToBounds)
+ var isEnabled: () -> Boolean by mutableStateOf(isEnabled)
+
+ private var lookaheadConstraints: Constraints? = null
+ private var lookaheadSize: IntSize = InvalidSize
+
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ if (isLookingAhead) {
+ lookaheadConstraints = constraints
+ }
+ val p = measurable.measure(lookaheadConstraints!!)
+ lookaheadSize = IntSize(p.width, p.height)
+ val constrainedSize = constraints.constrain(lookaheadSize)
+ return layout(constrainedSize.width, constrainedSize.height) {
+ val scaleToBounds = scaleToBounds
+ if (!isEnabled() || scaleToBounds == null) {
+ p.place(0, 0)
+ } else {
+ val contentScale = scaleToBounds.contentScale
+ val resolvedScale =
+ if (lookaheadSize.width == 0 || lookaheadSize.height == 0) {
+ ScaleFactor(1f, 1f)
+ } else
+ contentScale.computeScaleFactor(
+ lookaheadSize.toSize(),
+ constrainedSize.toSize()
+ )
+
+ val (x, y) =
+ scaleToBounds.alignment.align(
+ IntSize(
+ (lookaheadSize.width * resolvedScale.scaleX).roundToInt(),
+ (lookaheadSize.height * resolvedScale.scaleY).roundToInt()
+ ),
+ constrainedSize,
+ layoutDirection
+ )
+ p.placeWithLayer(x, y) {
+ scaleX = resolvedScale.scaleX
+ scaleY = resolvedScale.scaleY
+ transformOrigin = TransformOrigin(0f, 0f)
+ }
+ }
+ }
+ }
+
+ override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+ measurable: IntrinsicMeasurable,
+ height: Int
+ ): Int {
+ // If lookahead has already occurred, return the lookahead width/height to skip propagating
+ // the call further, and ensure convergence with lookahead.
+ return if (!isLookingAhead && lookaheadSize.isValid) {
+ lookaheadSize.width
+ } else {
+ measurable.maxIntrinsicWidth(height)
+ }
+ }
+
+ override fun IntrinsicMeasureScope.minIntrinsicWidth(
+ measurable: IntrinsicMeasurable,
+ height: Int
+ ): Int {
+ // If lookahead has already occurred, return the lookahead width/height to skip propagating
+ // the call further, and ensure convergence with lookahead.
+ return if (!isLookingAhead && lookaheadSize.isValid) {
+ lookaheadSize.width
+ } else {
+ measurable.minIntrinsicWidth(height)
+ }
+ }
+
+ override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+ measurable: IntrinsicMeasurable,
+ width: Int
+ ): Int {
+ // If lookahead has already occurred, return the lookahead width/height to skip propagating
+ // the call further, and ensure convergence with lookahead.
+ return if (!isLookingAhead && lookaheadSize.isValid) {
+ lookaheadSize.height
+ } else {
+ measurable.maxIntrinsicHeight(width)
+ }
+ }
+
+ override fun IntrinsicMeasureScope.minIntrinsicHeight(
+ measurable: IntrinsicMeasurable,
+ width: Int
+ ): Int {
+ // If lookahead has already occurred, return the lookahead width/height to skip propagating
+ // the call further, and ensure convergence with lookahead.
+ return if (!isLookingAhead && lookaheadSize.isValid) {
+ lookaheadSize.height
+ } else {
+ measurable.minIntrinsicHeight(width)
+ }
+ }
+}
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+internal data class SkipToLookaheadElement(
+ val scaleToBounds: ScaleToBoundsImpl? = null,
+ val isEnabled: () -> Boolean = DefaultEnabled,
+) : ModifierNodeElement<SkipToLookaheadNode>() {
+ override fun create(): SkipToLookaheadNode {
+ return SkipToLookaheadNode(scaleToBounds, isEnabled)
+ }
+
+ override fun update(node: SkipToLookaheadNode) {
+ node.scaleToBounds = scaleToBounds
+ node.isEnabled = isEnabled
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "skipToLookahead"
+ properties["scaleToBounds"] = scaleToBounds
+ properties["isEnabled"] = isEnabled
+ }
+}
+
+private val DefaultEnabled: () -> Boolean = { true }
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+internal fun Modifier.createContentScaleModifier(
+ scaleToBounds: ScaleToBoundsImpl,
+ isEnabled: () -> Boolean
+): Modifier =
+ this.then(
+ if (scaleToBounds.contentScale == ContentScale.Crop) {
+ Modifier.graphicsLayer { clip = isEnabled() }
+ } else Modifier
+ ) then SkipToLookaheadElement(scaleToBounds, isEnabled)
diff --git a/compose/benchmark-utils/benchmark/build.gradle b/compose/benchmark-utils/benchmark/build.gradle
index 94968c5..0ab9324 100644
--- a/compose/benchmark-utils/benchmark/build.gradle
+++ b/compose/benchmark-utils/benchmark/build.gradle
@@ -23,10 +23,10 @@
}
dependencies {
- androidTestImplementation project(":benchmark:benchmark-junit4")
- androidTestImplementation project(":compose:benchmark-utils")
- androidTestImplementation project(":compose:runtime:runtime")
- androidTestImplementation project(":compose:foundation:foundation-layout")
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":compose:benchmark-utils"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:foundation:foundation-layout"))
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.kotlinTestCommon)
diff --git a/compose/foundation/foundation-layout/benchmark/build.gradle b/compose/foundation/foundation-layout/benchmark/build.gradle
index 3146ce7..2b41dc0 100644
--- a/compose/foundation/foundation-layout/benchmark/build.gradle
+++ b/compose/foundation/foundation-layout/benchmark/build.gradle
@@ -24,12 +24,12 @@
dependencies {
- androidTestImplementation project(":benchmark:benchmark-junit4")
- androidTestImplementation project(":compose:foundation:foundation-layout")
- androidTestImplementation project(":compose:material:material")
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":compose:foundation:foundation-layout"))
+ androidTestImplementation(project(":compose:material:material"))
androidTestImplementation("androidx.compose.material:material-icons-core:1.6.7")
- androidTestImplementation project(":compose:runtime:runtime")
- androidTestImplementation project(":compose:benchmark-utils")
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:benchmark-utils"))
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
diff --git a/compose/foundation/foundation/benchmark/build.gradle b/compose/foundation/foundation/benchmark/build.gradle
index 254ee75..7fdab92 100644
--- a/compose/foundation/foundation/benchmark/build.gradle
+++ b/compose/foundation/foundation/benchmark/build.gradle
@@ -33,13 +33,13 @@
}
dependencies {
- androidTestImplementation project(":benchmark:benchmark-junit4")
- androidTestImplementation project(":compose:runtime:runtime")
- androidTestImplementation project(":compose:ui:ui-text:ui-text-benchmark")
- androidTestImplementation project(":compose:foundation:foundation-layout")
- androidTestImplementation project(":compose:foundation:foundation")
- androidTestImplementation project(":compose:material:material")
- androidTestImplementation project(":compose:benchmark-utils")
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:ui:ui-text:ui-text-benchmark"))
+ androidTestImplementation(project(":compose:foundation:foundation-layout"))
+ androidTestImplementation(project(":compose:foundation:foundation"))
+ androidTestImplementation(project(":compose:material:material"))
+ androidTestImplementation(project(":compose:benchmark-utils"))
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index d35b2d5..0d84d31 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -138,11 +138,8 @@
lintPublish(project(":compose:foundation:foundation-lint"))
}
-// Screenshot tests related setup
android {
compileSdk = 35
- sourceSets.androidTest.assets.srcDirs +=
- project.rootDir.absolutePath + "/../../golden/compose/foundation/foundation"
namespace = "androidx.compose.foundation"
buildTypes.configureEach {
consumerProguardFiles("proguard-rules.pro")
@@ -160,4 +157,5 @@
metalavaK2UastEnabled = false
samples(project(":compose:foundation:foundation:foundation-samples"))
kotlinTarget = KotlinTarget.KOTLIN_1_9
+ addGoldenImageAssets()
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeLineHeight.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeLineHeight.kt
index 5e49dd7e..0306cd0 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeLineHeight.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeLineHeight.kt
@@ -29,7 +29,6 @@
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
-import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Checkbox
import androidx.compose.material.RadioButton
@@ -51,6 +50,7 @@
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.text.style.LineHeightStyle.Trim
import androidx.compose.ui.text.style.TextOverflow
@@ -443,10 +443,11 @@
Spacer(Modifier.padding(16.dp))
Column(Modifier.width(width)) {
- val state = remember(text) { TextFieldState(text.text) }
+ var textFieldValue by remember(text) { mutableStateOf(TextFieldValue(text)) }
TextFieldWithMetrics(
- state = state,
+ value = textFieldValue,
+ onValueChange = { textFieldValue = it },
style = style,
maxLines = maxLines,
softWrap = !singleLine
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemoMetrics.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemoMetrics.kt
index 18734fd..a6a0d3c 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemoMetrics.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemoMetrics.kt
@@ -21,8 +21,6 @@
import androidx.compose.foundation.demos.text.TextMetricHelper.Alignment.Left
import androidx.compose.foundation.demos.text.TextMetricHelper.Alignment.Right
import androidx.compose.foundation.text.BasicTextField
-import androidx.compose.foundation.text.input.TextFieldLineLimits
-import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -43,6 +41,7 @@
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -71,7 +70,8 @@
@Composable
internal fun TextFieldWithMetrics(
- state: TextFieldState,
+ value: TextFieldValue,
+ onValueChange: (TextFieldValue) -> Unit,
style: TextStyle,
maxLines: Int,
softWrap: Boolean = true,
@@ -80,16 +80,13 @@
var textLayout by remember { mutableStateOf<TextLayoutResult?>(null) }
BasicTextField(
- state = state,
+ value = value,
+ onValueChange = onValueChange,
modifier = Modifier.drawTextMetrics(textLayout, colors).background(Color.White),
textStyle = style,
- lineLimits =
- if (!softWrap) {
- TextFieldLineLimits.SingleLine
- } else {
- TextFieldLineLimits.MultiLine(maxHeightInLines = maxLines)
- },
- onTextLayout = { textLayout = it.invoke() }
+ singleLine = !softWrap,
+ maxLines = maxLines,
+ onTextLayout = { textLayout = it }
)
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/ReceiveContentDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/ReceiveContentDemos.kt
index 946920f..0ba79a5 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/ReceiveContentDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/ReceiveContentDemos.kt
@@ -334,7 +334,7 @@
}
}
-@Suppress("ClassVerificationFailure", "DEPRECATION")
+@Suppress("DEPRECATION")
private fun Uri.readImageBitmap(context: Context): ImageBitmap? {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollableFocusableInteractionTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollableFocusableInteractionTest.kt
index 47dadf2..d93aaed 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollableFocusableInteractionTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollableFocusableInteractionTest.kt
@@ -38,7 +38,6 @@
import androidx.compose.ui.focus.FocusDirection.Companion.Next
import androidx.compose.ui.focus.FocusDirection.Companion.Previous
import androidx.compose.ui.focus.FocusManager
-import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
@@ -112,15 +111,14 @@
rule.setContent {
ScrollableRowOrColumn(size = viewportSize) {
- // Put a focusable in the bottom of the viewport.
- Spacer(Modifier.size(90.toDp()))
- TestFocusable(size = 10.toDp())
+ // Put a focusable at the end of the viewport.
+ WithSpacerBefore(size = 90.toDp()) { TestFocusable(size = 10.toDp()) }
}
}
requestFocusAndScrollToTop()
rule
.onNodeWithTag(focusableTag)
- .assertScrollAxisPositionInRootIsEqualTo(90.toDp())
+ .assertScrollAxisPositionInRootIsEqualTo(if (reverseScrolling) 0.toDp() else 90.toDp())
.assertIsDisplayed()
.assertIsFocused()
@@ -129,7 +127,7 @@
rule
.onNodeWithTag(focusableTag)
- .assertScrollAxisPositionInRootIsEqualTo(40.toDp())
+ .assertScrollAxisPositionInRootIsEqualTo(if (reverseScrolling) 0.toDp() else 40.toDp())
.assertIsDisplayed()
}
@@ -139,15 +137,14 @@
rule.setContent {
ScrollableRowOrColumn(size = viewportSize) {
- // Put a focusable in the bottom of the viewport.
- Spacer(Modifier.size(90.toDp()))
- TestFocusable(size = 10.toDp())
+ // Put a focusable at the end of the viewport.
+ WithSpacerBefore(size = 90.toDp()) { TestFocusable(size = 10.toDp()) }
}
}
requestFocusAndScrollToTop()
rule
.onNodeWithTag(focusableTag)
- .assertScrollAxisPositionInRootIsEqualTo(90.toDp())
+ .assertScrollAxisPositionInRootIsEqualTo(if (reverseScrolling) 0.toDp() else 90.toDp())
.assertIsDisplayed()
.assertIsFocused()
@@ -156,7 +153,7 @@
rule
.onNodeWithTag(focusableTag)
- .assertScrollAxisPositionInRootIsEqualTo(85.toDp())
+ .assertScrollAxisPositionInRootIsEqualTo(if (reverseScrolling) 0.toDp() else 85.toDp())
.assertIsDisplayed()
}
@@ -166,7 +163,7 @@
rule.setContent {
ScrollableRowOrColumn(size = viewportSize) {
- // Put a focusable in the bottom of the viewport.
+ // Put a focusable at the end of the viewport.
WithSpacerBefore(size = 90.toDp()) { TestFocusable(size = 10.toDp()) }
}
}
@@ -228,9 +225,8 @@
rule.setContent {
ScrollableRowOrColumn(size = viewportSize) {
- // Put a focusable in the bottom of the viewport.
- Spacer(Modifier.size(90.toDp()))
- TestFocusable(size = 10.toDp())
+ // Put a focusable at the end of the viewport.
+ WithSpacerBefore(size = 90.toDp()) { TestFocusable(size = 10.toDp()) }
}
if (animate) {
@@ -247,7 +243,7 @@
requestFocusAndScrollToTop()
rule
.onNodeWithTag(focusableTag)
- .assertScrollAxisPositionInRootIsEqualTo(90.toDp())
+ .assertScrollAxisPositionInRootIsEqualTo(if (reverseScrolling) 0.toDp() else 90.toDp())
.assertIsDisplayed()
.assertIsFocused()
@@ -255,7 +251,7 @@
rule
.onNodeWithTag(focusableTag)
- .assertScrollAxisPositionInRootIsEqualTo(30.toDp())
+ .assertScrollAxisPositionInRootIsEqualTo(if (reverseScrolling) 0.toDp() else 30.toDp())
.assertIsDisplayed()
}
@@ -265,15 +261,14 @@
rule.setContent {
ScrollableRowOrColumn(size = viewportSize) {
- // Put a focusable in the bottom of the viewport.
- Spacer(Modifier.size(90.toDp()))
- TestFocusable(size = 10.toDp())
+ // Put a focusable at the end of the viewport.
+ WithSpacerBefore(size = 90.toDp()) { TestFocusable(size = 10.toDp()) }
}
}
requestFocusAndScrollToTop()
rule
.onNodeWithTag(focusableTag)
- .assertScrollAxisPositionInRootIsEqualTo(90.toDp())
+ .assertScrollAxisPositionInRootIsEqualTo(if (reverseScrolling) 0.toDp() else 90.toDp())
.assertIsDisplayed()
.assertIsFocused()
@@ -284,10 +279,9 @@
rule.mainClock.advanceTimeByFrame()
rule.waitForIdle()
- // Interrupt the scroll by manually dragging.
+ // Interrupt the scroll.
rule.onNodeWithTag(scrollableAreaTag).performTouchInput {
down(center)
- moveBy(Offset(viewConfiguration.touchSlop + 1, viewConfiguration.touchSlop + 1))
up()
}
@@ -311,15 +305,14 @@
rule.setContent {
ScrollableRowOrColumn(size = viewportSize) {
- // Put a focusable in the bottom of the viewport.
- Spacer(Modifier.size(90.toDp()))
- TestFocusable(size = 10.toDp())
+ // Put a focusable at the end of the viewport.
+ WithSpacerBefore(size = 90.toDp()) { TestFocusable(size = 10.toDp()) }
}
}
requestFocusAndScrollToTop()
rule
.onNodeWithTag(focusableTag)
- .assertScrollAxisPositionInRootIsEqualTo(90.toDp())
+ .assertScrollAxisPositionInRootIsEqualTo(if (reverseScrolling) 0.toDp() else 90.toDp())
.assertIsDisplayed()
.assertIsFocused()
@@ -330,10 +323,9 @@
rule.mainClock.advanceTimeByFrame()
rule.waitForIdle()
- // Interrupt the scroll by manually dragging.
+ // Interrupt the scroll.
rule.onNodeWithTag(scrollableAreaTag).performTouchInput {
down(center)
- moveBy(Offset(viewConfiguration.touchSlop + 1, viewConfiguration.touchSlop + 1))
up()
}
@@ -396,7 +388,7 @@
rule.setContent {
ScrollableRowOrColumn(size = viewportSize) {
- // Put a focusable in the bottom of the viewport.
+ // Put a focusable at the end of the viewport.
WithSpacerBefore(size = 90.toDp()) { TestFocusable(size = 10.toDp()) }
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/draganddrop/DragAndDropSourceTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/draganddrop/DragAndDropSourceTest.kt
new file mode 100644
index 0000000..1b8cac3
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/draganddrop/DragAndDropSourceTest.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.compose.foundation.draganddrop
+
+import android.os.Build
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Constraints
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@MediumTest
+class DragAndDropSourceTest {
+ @get:Rule val rule = createComposeRule()
+
+ /** Regression test for b/379682458 */
+ @Test
+ fun dragAndDropSource_doesNotPreventChildInvalidations() {
+ var moveBox by mutableStateOf(false)
+ val tag = "source"
+ rule.setContent {
+ with(LocalDensity.current) {
+ Box(
+ Modifier.size(200.toDp())
+ .testTag(tag)
+ .dragAndDropSource { _ -> null }
+ .clip(RectangleShape)
+ .layout { measurable, constraints ->
+ val placeable =
+ measurable.measure(
+ constraints.copy(maxWidth = Constraints.Infinity)
+ )
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ placeable.place(x = if (moveBox) -200 else 0, y = 0)
+ }
+ }
+ .width(400.toDp())
+ .drawBehind {
+ val halfWidth = 200f
+ val halfWidthSize = Size(halfWidth, size.height)
+ drawRect(Color.Blue, size = halfWidthSize)
+ drawRect(
+ Color.Red,
+ topLeft = Offset(halfWidth, 0f),
+ size = halfWidthSize
+ )
+ }
+ )
+ }
+ }
+
+ rule.onNodeWithTag(tag).captureToImage().assertPixels { Color.Blue }
+
+ // Make the layout move the child so that the red box is now visible
+ rule.runOnIdle { moveBox = true }
+
+ rule.onNodeWithTag(tag).captureToImage().assertPixels { Color.Red }
+ }
+}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/draganddrop/AndroidDragAndDropSource.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/draganddrop/AndroidDragAndDropSource.android.kt
index 4981f7c..1182e61 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/draganddrop/AndroidDragAndDropSource.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/draganddrop/AndroidDragAndDropSource.android.kt
@@ -19,15 +19,13 @@
package androidx.compose.foundation.draganddrop
-import android.graphics.Picture
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.runtime.Immutable
import androidx.compose.ui.draw.CacheDrawScope
import androidx.compose.ui.draw.DrawResult
import androidx.compose.ui.graphics.drawscope.DrawScope
-import androidx.compose.ui.graphics.drawscope.draw
-import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
-import androidx.compose.ui.graphics.nativeCanvas
+import androidx.compose.ui.graphics.layer.GraphicsLayer
+import androidx.compose.ui.graphics.layer.drawLayer
@Immutable
internal actual object DragAndDropSourceDefaults {
@@ -37,39 +35,24 @@
}
internal actual class CacheDrawScopeDragShadowCallback {
- private var cachedPicture: Picture? = null
+ private var graphicsLayer: GraphicsLayer? = null
actual fun drawDragShadow(drawScope: DrawScope) =
with(drawScope) {
- when (val picture = cachedPicture) {
+ when (val layer = graphicsLayer) {
null ->
throw IllegalArgumentException(
"No cached drag shadow. Check if Modifier.cacheDragShadow(painter) was called."
)
- else -> drawIntoCanvas { canvas -> canvas.nativeCanvas.drawPicture(picture) }
+ else -> {
+ if (!layer.isReleased) drawLayer(layer)
+ }
}
}
actual fun cachePicture(scope: CacheDrawScope): DrawResult =
with(scope) {
- val picture = Picture()
- cachedPicture = picture
- val width = this.size.width.toInt()
- val height = this.size.height.toInt()
- onDrawWithContent {
- val pictureCanvas =
- androidx.compose.ui.graphics.Canvas(picture.beginRecording(width, height))
- draw(
- density = this,
- layoutDirection = this.layoutDirection,
- canvas = pictureCanvas,
- size = this.size
- ) {
- [email protected]()
- }
- picture.endRecording()
-
- drawIntoCanvas { canvas -> canvas.nativeCanvas.drawPicture(picture) }
- }
+ graphicsLayer = scope.obtainGraphicsLayer().apply { record { drawContent() } }
+ onDrawWithContent { drawLayer(graphicsLayer!!) }
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt
index 6c93970..e887c7e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt
@@ -94,17 +94,21 @@
private var focusedChild: LayoutCoordinates? = null
/**
- * The previous bounds of the [focusedChild] used by [onRemeasured] to calculate when the
- * focused child is first clipped when scrolling is reversed.
- */
- private var focusedChildBoundsFromPreviousRemeasure: Rect? = null
-
- /**
* Set to true when this class is actively animating the scroll to keep the focused child in
* view.
*/
private var trackingFocusedChild = false
+ /**
+ * When the viewport shrinks, we need to wait for the focused bounds to be updated, which
+ * happens when globally positioned callbacks happen - this is _after_ this node has been
+ * remeasured. As a result we cannot bring into view until we know what the new bounds of the
+ * child are, as this can be affected by the measurement logic of this scrollable container /
+ * its parent(s). The old bounds of the child might still appear to be 'visible', even though it
+ * will now be placed in a different place, and become invisible.
+ */
+ private var childWasMaxVisibleBeforeViewportShrunk = false
+
/** The size of the scrollable container. */
internal var viewportSize = IntSize.Zero
private set
@@ -141,41 +145,49 @@
fun onFocusBoundsChanged(newBounds: LayoutCoordinates?) {
focusedChild = newBounds
+
+ if (childWasMaxVisibleBeforeViewportShrunk) {
+ getFocusedChildBounds()?.let { focusedChild ->
+ if (DEBUG) println("[$TAG] focused child bounds: $focusedChild")
+ if (!focusedChild.isMaxVisible(viewportSize)) {
+ if (DEBUG)
+ println(
+ "[$TAG] focused child was clipped by viewport shrink: $focusedChild"
+ )
+ trackingFocusedChild = true
+ launchAnimation()
+ }
+ }
+ }
+ childWasMaxVisibleBeforeViewportShrunk = false
}
override fun onRemeasured(size: IntSize) {
- val oldSize = viewportSize
+ val previousViewportSize = viewportSize
viewportSize = size
// Don't care if the viewport grew.
- if (size >= oldSize) return
+ if (size >= previousViewportSize) return
- if (DEBUG) println("[$TAG] viewport shrunk: $oldSize -> $size")
+ if (DEBUG) println("[$TAG] viewport shrunk: $previousViewportSize -> $size")
- getFocusedChildBounds()?.let { focusedChild ->
- if (DEBUG) println("[$TAG] focused child bounds: $focusedChild")
- val previousFocusedChildBounds = focusedChildBoundsFromPreviousRemeasure ?: focusedChild
- if (
- !isAnimationRunning &&
- !trackingFocusedChild &&
- // Resize caused it to go from being fully visible to at least partially
- // clipped. Need to use the lastFocusedChildBounds to compare with the old size
- // only to handle the case where scrolling direction is reversed: in that case,
- // when
- // the child first goes out-of-bounds, it will be out of bounds regardless of
- // which
- // size we pass in, so the only way to detect the change is to use the previous
- // bounds.
- previousFocusedChildBounds.isMaxVisible(oldSize) &&
- !focusedChild.isMaxVisible(size)
- ) {
- if (DEBUG)
- println("[$TAG] focused child was clipped by viewport shrink: $focusedChild")
- trackingFocusedChild = true
- launchAnimation()
- }
+ // Ignore if we are already tracking an existing animation
+ if (isAnimationRunning || trackingFocusedChild) {
+ if (DEBUG) println("[$TAG] ignoring size change because animation is in progress")
+ return
+ }
- this.focusedChildBoundsFromPreviousRemeasure = focusedChild
+ // onRemeasured is called before the onGloballyPositioned callbacks are dispatched, so
+ // the bounds we get here will essentially be the previous bounds of the focused child,
+ // before this remeasurement occurred.
+ val boundsBeforeRemeasurement = getFocusedChildBounds() ?: return
+
+ // If the focused child was previously fully visible (its 'previous' bounds fit inside the
+ // previous viewport), we need to see if it is still visible after this remeasurement
+ // finishes and the child is placed in its new location. To do that we need to wait for
+ // its onGloballyPositioned callback, which will then end up calling onFocusBoundsChanged
+ if (boundsBeforeRemeasurement.isMaxVisible(previousViewportSize)) {
+ childWasMaxVisibleBeforeViewportShrunk = true
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
index 194bcf9..fbc0c73 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
@@ -784,7 +784,7 @@
* `null` if all pointers are raised or the position change was consumed by another gesture
* detector.
*/
-private suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
+internal suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
pointerId: PointerId,
pointerType: PointerType,
orientation: Orientation?,
@@ -836,7 +836,7 @@
* new [PointerInputChange] one should add it to this detector using [addPointerInputChange]. If the
* position change causes the touch slop to be crossed, [addPointerInputChange] will return true.
*/
-private class TouchSlopDetector(
+internal class TouchSlopDetector(
val orientation: Orientation? = null,
val initialPositionChange: Offset
) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
index 73e957a..bbe9326 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
@@ -275,8 +275,19 @@
// prefetch is best effort.
val prefetchHandles = mutableListOf<LazyLayoutPrefetchState.PrefetchHandle>()
Snapshot.withoutReadObservation {
- layoutInfoState.value.prefetchInfoRetriever(lineIndex).fastForEach {
- prefetchHandles.add(prefetchState.schedulePrefetch(it.first, it.second))
+ val layoutInfo =
+ if (hasLookaheadOccurred) {
+ approachLayoutInfo
+ } else {
+ layoutInfoState.value
+ }
+
+ layoutInfo?.let {
+ it.prefetchInfoRetriever(lineIndex).fastForEach { lineInfo ->
+ prefetchHandles.add(
+ prefetchState.schedulePrefetch(lineInfo.first, lineInfo.second)
+ )
+ }
}
}
return prefetchHandles
diff --git a/compose/integration-tests/material-catalog/build.gradle b/compose/integration-tests/material-catalog/build.gradle
index e2ab46c..ca73ac9 100644
--- a/compose/integration-tests/material-catalog/build.gradle
+++ b/compose/integration-tests/material-catalog/build.gradle
@@ -50,16 +50,16 @@
dependencies {
implementation(libs.kotlinStdlib)
- implementation project(":compose:runtime:runtime")
- implementation project(":compose:foundation:foundation-layout")
- implementation project(":compose:ui:ui")
- implementation project(":compose:material:material")
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:foundation:foundation-layout"))
+ implementation(project(":compose:ui:ui"))
+ implementation(project(":compose:material:material"))
implementation("androidx.compose.material:material-icons-core:1.6.7")
- implementation project(":compose:material3:material3")
- implementation project(":compose:material:material:integration-tests:material-catalog")
- implementation project(":compose:material3:material3:integration-tests:material3-catalog")
+ implementation(project(":compose:material3:material3"))
+ implementation(project(":compose:material:material:integration-tests:material-catalog"))
+ implementation(project(":compose:material3:material3:integration-tests:material3-catalog"))
implementation "androidx.activity:activity-compose:1.3.1"
- implementation project(":navigation:navigation-compose")
+ implementation(project(":navigation:navigation-compose"))
// old version of common-java8 conflicts with newer version, because both have
// DefaultLifecycleEventObserver.
// Outside of androidx this is resolved via constraint added to lifecycle-common,
diff --git a/compose/material/material-navigation/api/current.txt b/compose/material/material-navigation/api/current.txt
index dd70def..f339d6c 100644
--- a/compose/material/material-navigation/api/current.txt
+++ b/compose/material/material-navigation/api/current.txt
@@ -2,7 +2,8 @@
package androidx.compose.material.navigation {
public final class BottomSheetKt {
- method @androidx.compose.runtime.Composable public static void ModalBottomSheetLayout(androidx.compose.material.navigation.BottomSheetNavigator bottomSheetNavigator, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void ModalBottomSheetLayout(androidx.compose.material.navigation.BottomSheetNavigator bottomSheetNavigator, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ModalBottomSheetLayout(androidx.compose.material.navigation.BottomSheetNavigator bottomSheetNavigator, optional androidx.compose.ui.Modifier modifier, optional boolean sheetGesturesEnabled, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
@androidx.navigation.Navigator.Name("bottomSheet") public final class BottomSheetNavigator extends androidx.navigation.Navigator<androidx.compose.material.navigation.BottomSheetNavigator.Destination> {
diff --git a/compose/material/material-navigation/api/restricted_current.txt b/compose/material/material-navigation/api/restricted_current.txt
index 1dbe896..a024227 100644
--- a/compose/material/material-navigation/api/restricted_current.txt
+++ b/compose/material/material-navigation/api/restricted_current.txt
@@ -2,7 +2,8 @@
package androidx.compose.material.navigation {
public final class BottomSheetKt {
- method @androidx.compose.runtime.Composable public static void ModalBottomSheetLayout(androidx.compose.material.navigation.BottomSheetNavigator bottomSheetNavigator, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void ModalBottomSheetLayout(androidx.compose.material.navigation.BottomSheetNavigator bottomSheetNavigator, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ModalBottomSheetLayout(androidx.compose.material.navigation.BottomSheetNavigator bottomSheetNavigator, optional androidx.compose.ui.Modifier modifier, optional boolean sheetGesturesEnabled, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
@androidx.navigation.Navigator.Name("bottomSheet") public final class BottomSheetNavigator extends androidx.navigation.Navigator<androidx.compose.material.navigation.BottomSheetNavigator.Destination> {
diff --git a/compose/material/material-navigation/build.gradle b/compose/material/material-navigation/build.gradle
index 6d3b9f4..a80dfb7 100644
--- a/compose/material/material-navigation/build.gradle
+++ b/compose/material/material-navigation/build.gradle
@@ -31,7 +31,7 @@
implementation(libs.kotlinStdlib)
implementation(libs.kotlinSerializationCore)
- androidTestImplementation project(":compose:test-utils")
+ androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation("androidx.navigation:navigation-testing:2.7.7")
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
androidTestImplementation(project(":compose:ui:ui-test-manifest"))
diff --git a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheet.kt b/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheet.kt
index c4e544f..873012c 100644
--- a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheet.kt
+++ b/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheet.kt
@@ -45,6 +45,7 @@
* @sample androidx.compose.material.navigation.samples.BottomSheetNavDemo
* @see [ModalBottomSheetLayout]
*/
+@Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
@Suppress("MissingJvmstatic")
@Composable
// Keep defaults in sync with androidx.compose.material.ModalBottomSheetLayout
@@ -70,3 +71,51 @@
content = content
)
}
+
+/**
+ * Create a [ModalBottomSheetLayout] displaying content from a [BottomSheetNavigator].
+ *
+ * @param bottomSheetNavigator The navigator that manages the bottom sheet content.
+ * @param modifier Optional [Modifier] for the entire component.
+ * @param sheetGesturesEnabled Whether the bottom sheet can be interacted with by gestures.
+ * @param sheetShape The shape of the bottom sheet.
+ * @param sheetElevation The elevation of the bottom sheet.
+ * @param sheetBackgroundColor The background color of the bottom sheet.
+ * @param sheetContentColor The preferred content color provided by the bottom sheet to its
+ * children. Defaults to the matching content color for [sheetBackgroundColor], or if that is not
+ * a color from the theme, this will keep the same content color set above the bottom sheet.
+ * @param scrimColor The color of the scrim that is applied to the rest of the screen when the
+ * bottom sheet is visible. If the color passed is [Color.Unspecified], then a scrim will no
+ * longer be applied and the bottom sheet will not block interaction with the rest of the screen
+ * when visible.
+ * @param content The content of rest of the screen.
+ * @sample androidx.compose.material.navigation.samples.BottomSheetNavDemo
+ * @see [ModalBottomSheetLayout]
+ */
+@Suppress("MissingJvmstatic")
+@Composable
+// Keep defaults in sync with androidx.compose.material.ModalBottomSheetLayout
+public fun ModalBottomSheetLayout(
+ bottomSheetNavigator: BottomSheetNavigator,
+ modifier: Modifier = Modifier,
+ sheetGesturesEnabled: Boolean = true,
+ sheetShape: Shape = MaterialTheme.shapes.large,
+ sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
+ sheetBackgroundColor: Color = MaterialTheme.colors.surface,
+ sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
+ scrimColor: Color = ModalBottomSheetDefaults.scrimColor,
+ content: @Composable () -> Unit
+) {
+ ModalBottomSheetLayout(
+ sheetState = bottomSheetNavigator.sheetState,
+ sheetContent = bottomSheetNavigator.sheetContent,
+ modifier = modifier,
+ sheetGesturesEnabled = sheetGesturesEnabled,
+ sheetShape = sheetShape,
+ sheetElevation = sheetElevation,
+ sheetBackgroundColor = sheetBackgroundColor,
+ sheetContentColor = sheetContentColor,
+ scrimColor = scrimColor,
+ content = content
+ )
+}
diff --git a/compose/material/material-ripple/benchmark/build.gradle b/compose/material/material-ripple/benchmark/build.gradle
index 1da1934..4d59f30 100644
--- a/compose/material/material-ripple/benchmark/build.gradle
+++ b/compose/material/material-ripple/benchmark/build.gradle
@@ -32,11 +32,11 @@
}
dependencies {
- androidTestImplementation project(":benchmark:benchmark-junit4")
- androidTestImplementation project(":compose:benchmark-utils")
- androidTestImplementation project(":compose:foundation:foundation")
- androidTestImplementation project(":compose:material:material-ripple")
- androidTestImplementation project(":compose:runtime:runtime")
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":compose:benchmark-utils"))
+ androidTestImplementation(project(":compose:foundation:foundation"))
+ androidTestImplementation(project(":compose:material:material-ripple"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
diff --git a/compose/material/material/benchmark/build.gradle b/compose/material/material/benchmark/build.gradle
index 8a8f1b9..64803b4 100644
--- a/compose/material/material/benchmark/build.gradle
+++ b/compose/material/material/benchmark/build.gradle
@@ -33,13 +33,13 @@
dependencies {
- androidTestImplementation project(":benchmark:benchmark-junit4")
- androidTestImplementation project(":compose:runtime:runtime")
- androidTestImplementation project(":compose:ui:ui-text:ui-text-benchmark")
- androidTestImplementation project(":compose:foundation:foundation")
- androidTestImplementation project(":compose:material:material")
- androidTestImplementation project(":compose:runtime:runtime")
- androidTestImplementation project(":compose:benchmark-utils")
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:ui:ui-text:ui-text-benchmark"))
+ androidTestImplementation(project(":compose:foundation:foundation"))
+ androidTestImplementation(project(":compose:material:material"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:benchmark-utils"))
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index 3b67763b..ad67f6d 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -127,8 +127,8 @@
}
dependencies {
- lintChecks project(":compose:material:material-lint")
- lintPublish project(":compose:material:material-lint")
+ lintChecks(project(":compose:material:material-lint"))
+ lintPublish(project(":compose:material:material-lint"))
}
androidx {
@@ -141,12 +141,10 @@
metalavaK2UastEnabled = false
samples(project(":compose:material:material:material-samples"))
kotlinTarget = KotlinTarget.KOTLIN_1_9
+ addGoldenImageAssets()
}
-// Screenshot tests related setup
android {
compileSdk = 35
- sourceSets.androidTest.assets.srcDirs +=
- project.rootDir.absolutePath + "/../../golden/compose/material/material"
namespace = "androidx.compose.material"
}
diff --git a/compose/material/material/integration-tests/material-catalog/build.gradle b/compose/material/material/integration-tests/material-catalog/build.gradle
index c9aa047..29c6f75 100644
--- a/compose/material/material/integration-tests/material-catalog/build.gradle
+++ b/compose/material/material/integration-tests/material-catalog/build.gradle
@@ -32,13 +32,13 @@
dependencies {
implementation(libs.kotlinStdlib)
- implementation project(":core:core")
- implementation project(":compose:runtime:runtime")
- implementation project(":compose:foundation:foundation-layout")
- implementation project(":compose:ui:ui")
- implementation project(":compose:material:material")
- implementation project(":compose:material:material:material-samples")
- implementation project(":navigation:navigation-compose")
+ implementation(project(":core:core"))
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:foundation:foundation-layout"))
+ implementation(project(":compose:ui:ui"))
+ implementation(project(":compose:material:material"))
+ implementation(project(":compose:material:material:material-samples"))
+ implementation(project(":navigation:navigation-compose"))
}
androidx {
diff --git a/compose/material/material/integration-tests/material-demos/build.gradle b/compose/material/material/integration-tests/material-demos/build.gradle
index 5417ae4..5d92707 100644
--- a/compose/material/material/integration-tests/material-demos/build.gradle
+++ b/compose/material/material/integration-tests/material-demos/build.gradle
@@ -22,7 +22,7 @@
implementation(project(":compose:integration-tests:demos:common"))
implementation(project(":compose:material:material"))
implementation(project(":compose:material:material:material-samples"))
- implementation project(":compose:material:material-navigation-samples")
+ implementation(project(":compose:material:material-navigation-samples"))
implementation(project(":compose:runtime:runtime"))
implementation(project(":compose:ui:ui"))
implementation(project(":compose:ui:ui-text"))
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
index 2a16cb3..e0ff4c1 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
@@ -109,9 +109,9 @@
* <a href="https://material.io/design/layout/understanding-layout.html" class="external"
* target="_blank">Material Design layout</a>.
*
- * Scaffold implements the basic material design visual layout structure.
+ * Scaffold implements the basic Material Design visual layout structure.
*
- * This component provides API to put together several material components to construct your screen,
+ * This component provides API to put together several Material components to construct your screen,
* by ensuring proper layout strategy for them and collecting necessary data so these components
* will work together correctly.
*
@@ -127,12 +127,12 @@
* @sample androidx.compose.material.samples.SimpleScaffoldWithTopBar
*
* More fancy usage with [BottomAppBar] with cutout and docked [FloatingActionButton], which
- * animates it's shape when clicked:
+ * animates its shape when clicked:
*
* @sample androidx.compose.material.samples.ScaffoldWithBottomBarAndCutout
*
* To show a [Snackbar], use [SnackbarHostState.showSnackbar]. Scaffold state already have
- * [ScaffoldState.snackbarHostState] when created
+ * [ScaffoldState.snackbarHostState] when created.
*
* @sample androidx.compose.material.samples.ScaffoldWithSimpleSnackbar
* @param contentWindowInsets window insets to be passed to [content] slot via [PaddingValues]
@@ -170,9 +170,9 @@
* content color for [backgroundColor], or, if it is not a color from the theme, this will keep
* the same value set above this Surface.
* @param content content of your screen. The lambda receives an [PaddingValues] that should be
- * applied to the content root via Modifier.padding to properly offset top and bottom bars. If
- * you're using VerticalScroller, apply this modifier to the child of the scroller, and not on the
- * scroller itself.
+ * applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to
+ * properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to
+ * the child of the scroll, and not on the scroll itself.
*/
@Composable
fun Scaffold(
@@ -244,9 +244,9 @@
* <a href="https://material.io/design/layout/understanding-layout.html" class="external"
* target="_blank">Material Design layout</a>.
*
- * Scaffold implements the basic material design visual layout structure.
+ * Scaffold implements the basic Material Design visual layout structure.
*
- * This component provides API to put together several material components to construct your screen,
+ * This component provides API to put together several Material components to construct your screen,
* by ensuring proper layout strategy for them and collecting necessary data so these components
* will work together correctly.
*
@@ -259,7 +259,7 @@
* @sample androidx.compose.material.samples.SimpleScaffoldWithTopBar
*
* More fancy usage with [BottomAppBar] with cutout and docked [FloatingActionButton], which
- * animates it's shape when clicked:
+ * animates its shape when clicked:
*
* @sample androidx.compose.material.samples.ScaffoldWithBottomBarAndCutout
*
@@ -297,9 +297,9 @@
* content color for [backgroundColor], or, if it is not a color from the theme, this will keep
* the same value set above this Surface.
* @param content content of your screen. The lambda receives an [PaddingValues] that should be
- * applied to the content root via Modifier.padding to properly offset top and bottom bars. If
- * you're using VerticalScroller, apply this modifier to the child of the scroller, and not on the
- * scroller itself.
+ * applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to
+ * properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to
+ * the child of the scroll, and not on the scroll itself.
*/
@Composable
fun Scaffold(
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
index aa58990..45efedc 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
@@ -138,9 +138,10 @@
* @param enabled whether or not component is enabled and can be interacted with or not
* @param valueRange range of values that Slider value can take. Passed [value] will be coerced to
* this range
- * @param steps if positive, specifies the amount of discrete allowable values (in addition to the
- * endpoints of the value range). Step values are evenly distributed across the range. If 0, the
- * slider will behave continuously and allow any value from the range. Must not be negative.
+ * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
+ * of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
+ * distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
+ * continuously and allow any value from the range. Must not be negative.
* @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
* shouldn't be used to update the slider value (use [onValueChange] for that), but rather to know
* when the user has completed selecting a new value by ending a drag or a click.
@@ -382,9 +383,10 @@
* @param enabled whether or not component is enabled and can we interacted with or not
* @param valueRange range of values that Range Slider values can take. Passed [value] will be
* coerced to this range
- * @param steps if positive, specifies the amount of discrete allowable values (in addition to the
- * endpoints of the value range). Step values are evenly distributed across the range. If 0, the
- * range slider will behave continuously and allow any value from the range. Must not be negative.
+ * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
+ * of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
+ * distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
+ * continuously and allow any value from the range. Must not be negative.
* @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
* shouldn't be used to update the range slider values (use [onValueChange] for that), but rather
* to know when the user has completed selecting a new value by ending a drag or a click.
diff --git a/compose/material3/adaptive/adaptive-layout/build.gradle b/compose/material3/adaptive/adaptive-layout/build.gradle
index 56d63c4..4c3c2d5 100644
--- a/compose/material3/adaptive/adaptive-layout/build.gradle
+++ b/compose/material3/adaptive/adaptive-layout/build.gradle
@@ -23,7 +23,6 @@
*/
import androidx.build.LibraryType
import androidx.build.PlatformIdentifier
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -110,12 +109,10 @@
legacyDisableKotlinStrictApiMode = true
metalavaK2UastEnabled = false
samples(project(":compose:material3:adaptive:adaptive-samples"))
+ addGoldenImageAssets()
}
-// Screenshot tests related setup
android {
compileSdk = 35
- sourceSets.androidTest.assets.srcDirs +=
- project.rootDir.absolutePath + "/../../golden/compose/material3/adaptive"
namespace = "androidx.compose.material3.adaptive.layout"
}
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/GoldenCommon.kt b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/GoldenCommon.kt
index f16ff5e..5bf1297 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/GoldenCommon.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/GoldenCommon.kt
@@ -16,4 +16,4 @@
package androidx.compose.material3.adaptive.layout
-internal const val GOLDEN_MATERIAL3_ADAPTIVE = "compose/material3/adaptive"
+internal const val GOLDEN_MATERIAL3_ADAPTIVE = "compose/material3/adaptive/adaptive-layout"
diff --git a/compose/material3/adaptive/adaptive-navigation/build.gradle b/compose/material3/adaptive/adaptive-navigation/build.gradle
index 759ce50..eb1274b 100644
--- a/compose/material3/adaptive/adaptive-navigation/build.gradle
+++ b/compose/material3/adaptive/adaptive-navigation/build.gradle
@@ -23,7 +23,6 @@
*/
import androidx.build.LibraryType
import androidx.build.PlatformIdentifier
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -96,10 +95,6 @@
}
}
-android {
- namespace = "androidx.compose.material3.adaptive.navigation"
-}
-
androidx {
name = "Material Adaptive"
type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
@@ -109,10 +104,7 @@
metalavaK2UastEnabled = false
}
-// Screenshot tests related setup
android {
compileSdk = 35
- sourceSets.androidTest.assets.srcDirs +=
- project.rootDir.absolutePath + "/../../golden/compose/material3/adaptive"
namespace = "androidx.compose.material3.adaptive.navigation"
}
diff --git a/compose/material3/adaptive/adaptive/build.gradle b/compose/material3/adaptive/adaptive/build.gradle
index 6dc0ad0..1636d92 100644
--- a/compose/material3/adaptive/adaptive/build.gradle
+++ b/compose/material3/adaptive/adaptive/build.gradle
@@ -23,7 +23,6 @@
*/
import androidx.build.LibraryType
import androidx.build.PlatformIdentifier
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -96,10 +95,6 @@
}
}
-android {
- namespace = "androidx.compose.material3.adaptive"
-}
-
androidx {
name = "Material Adaptive"
type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
@@ -109,10 +104,7 @@
metalavaK2UastEnabled = false
}
-// Screenshot tests related setup
android {
compileSdk = 35
- sourceSets.androidTest.assets.srcDirs +=
- project.rootDir.absolutePath + "/../../golden/compose/material3/adaptive"
namespace = "androidx.compose.material3.adaptive"
}
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index c930af7..153d2b4 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -124,8 +124,8 @@
}
dependencies {
- lintChecks project(":compose:material3:material3-lint")
- lintPublish project(":compose:material3:material3-lint")
+ lintChecks(project(":compose:material3:material3-lint"))
+ lintPublish(project(":compose:material3:material3-lint"))
}
androidx {
@@ -137,13 +137,11 @@
metalavaK2UastEnabled = false
samples(project(":compose:material3:material3:material3-samples"))
kotlinTarget = KotlinTarget.KOTLIN_1_9
+ addGoldenImageAssets()
}
-// Screenshot tests related setup
android {
compileSdk = 35
- sourceSets.androidTest.assets.srcDirs +=
- project.rootDir.absolutePath + "/../../golden/compose/material3/material3"
namespace = "androidx.compose.material3"
// TODO(b/345531033)
experimentalProperties["android.lint.useK2Uast"] = false
diff --git a/compose/material3/material3/integration-tests/material3-catalog/build.gradle b/compose/material3/material3/integration-tests/material3-catalog/build.gradle
index fc3f343..2dfcfc5 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/build.gradle
+++ b/compose/material3/material3/integration-tests/material3-catalog/build.gradle
@@ -34,7 +34,7 @@
dependencies {
implementation(libs.kotlinStdlib)
implementation("androidx.core:core:1.12.0")
- implementation project(":compose:runtime:runtime")
+ implementation(project(":compose:runtime:runtime"))
implementation("androidx.compose.foundation:foundation-layout:1.6.0")
implementation("androidx.compose.ui:ui:1.6.0")
implementation("androidx.compose.material:material:1.6.0")
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DatePickerSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DatePickerSamples.kt
index 51bd2ed..439e915 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DatePickerSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DatePickerSamples.kt
@@ -142,7 +142,6 @@
}
}
-@Suppress("ClassVerificationFailure")
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Sampled
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SplitButtonSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SplitButtonSamples.kt
index 0a7af3f..115b07c 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SplitButtonSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SplitButtonSamples.kt
@@ -68,7 +68,7 @@
onCheckedChange = { checked = it },
modifier =
Modifier.semantics {
- stateDescription = if (checked) "Checked" else "Unchecked"
+ stateDescription = if (checked) "Expanded" else "Collapsed"
contentDescription = "Toggle Button"
},
) {
@@ -148,11 +148,11 @@
SplitButtonDefaults.TonalTrailingButton(
checked = checked,
onCheckedChange = { checked = it },
- // modifier =
- // Modifier.semantics {
- // stateDescription = if (checked) "Checked" else "Unchecked"
- // contentDescription = "Toggle Button"
- // },
+ modifier =
+ Modifier.semantics {
+ stateDescription = if (checked) "Expanded" else "Collapsed"
+ contentDescription = "Toggle Button"
+ },
) {
val rotation: Float by
animateFloatAsState(
@@ -199,7 +199,7 @@
onCheckedChange = { checked = it },
modifier =
Modifier.semantics {
- stateDescription = if (checked) "Checked" else "Unchecked"
+ stateDescription = if (checked) "Expanded" else "Collapsed"
contentDescription = "Toggle Button"
},
) {
@@ -248,7 +248,7 @@
onCheckedChange = { checked = it },
modifier =
Modifier.semantics {
- stateDescription = if (checked) "Checked" else "Unchecked"
+ stateDescription = if (checked) "Expanded" else "Collapsed"
contentDescription = "Toggle Button"
},
) {
@@ -288,7 +288,12 @@
trailingButton = {
SplitButtonDefaults.TrailingButton(
checked = checked,
- onCheckedChange = { checked = it }
+ onCheckedChange = { checked = it },
+ modifier =
+ Modifier.semantics {
+ stateDescription = if (checked) "Expanded" else "Collapsed"
+ contentDescription = "Toggle Button"
+ },
) {
val rotation: Float by
animateFloatAsState(
@@ -330,7 +335,12 @@
trailingButton = {
SplitButtonDefaults.TrailingButton(
checked = checked,
- onCheckedChange = { checked = it }
+ onCheckedChange = { checked = it },
+ modifier =
+ Modifier.semantics {
+ stateDescription = if (checked) "Expanded" else "Collapsed"
+ contentDescription = "Toggle Button"
+ },
) {
val rotation: Float by
animateFloatAsState(
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ChipTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ChipTest.kt
index f583f3d..df233ac 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ChipTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ChipTest.kt
@@ -19,9 +19,12 @@
import android.os.Build
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.requiredWidth
@@ -71,6 +74,7 @@
import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.click
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
@@ -80,6 +84,7 @@
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.compose.ui.unit.width
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
@@ -514,6 +519,60 @@
}
@Test
+ fun intrinsicSize_filterChip() {
+ val iconSize = 24.dp
+ val horizontalPadding = 8.dp
+ val minTouchTarget = 48.dp
+
+ rule.setMaterialContent(lightColorScheme()) {
+ Column {
+ Box(Modifier.height(IntrinsicSize.Max).testTag("chipMax")) {
+ FilterChip(
+ selected = false,
+ onClick = {},
+ label = { Text("Text", modifier = Modifier.testTag("labelMax")) },
+ leadingIcon = { Icon(Icons.Filled.Settings, contentDescription = null) },
+ trailingIcon = { Icon(Icons.Filled.Settings, contentDescription = null) },
+ )
+ }
+ Box(Modifier.height(IntrinsicSize.Min).testTag("chipMin")) {
+ FilterChip(
+ selected = false,
+ onClick = {},
+ label = { Text("Text", modifier = Modifier.testTag("labelMin")) },
+ leadingIcon = { Icon(Icons.Filled.Settings, contentDescription = null) },
+ trailingIcon = { Icon(Icons.Filled.Settings, contentDescription = null) },
+ )
+ }
+ }
+ }
+
+ val labelMaxWidth =
+ rule.onNodeWithTag("labelMax", useUnmergedTree = true).getUnclippedBoundsInRoot().width
+ rule
+ .onNodeWithTag("chipMax")
+ .assertHeightIsEqualTo(minTouchTarget)
+ .assertWidthIsEqualTo(
+ iconSize +
+ labelMaxWidth +
+ iconSize +
+ horizontalPadding * 4 // chip start, chip end, label start, label end
+ )
+
+ val labelMinWidth =
+ rule.onNodeWithTag("labelMin", useUnmergedTree = true).getUnclippedBoundsInRoot().width
+ rule
+ .onNodeWithTag("chipMin")
+ .assertHeightIsEqualTo(minTouchTarget)
+ .assertWidthIsEqualTo(
+ iconSize +
+ labelMinWidth +
+ iconSize +
+ horizontalPadding * 4 // chip start, chip end, label start, label end
+ )
+ }
+
+ @Test
fun longLabelDoesNotHideTrailingIcon_filterChip() {
rule.setMaterialContent(lightColorScheme()) {
FilterChip(
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingToolbarTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingToolbarTest.kt
index 1607eb4..c2bc520 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingToolbarTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingToolbarTest.kt
@@ -635,7 +635,7 @@
@Test
fun horizontalFloatingToolbar_withFab_customContentPadding() {
- val padding = 64.dp
+ val padding = 48.dp
rule.setMaterialContent(lightColorScheme()) {
HorizontalFloatingToolbar(
modifier = Modifier.testTag(FloatingToolbarTestTag),
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
index 6df4b47..65cc199 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
@@ -683,21 +683,18 @@
ModalBottomSheet(
onDismissRequest = {},
modifier = Modifier.testTag(topTag),
+ dragHandle = null,
sheetState = sheetState,
) {
if (showShortContent) {
- Box(Modifier.fillMaxWidth().height(100.dp))
+ Box(Modifier.fillMaxWidth().height(1.dp))
} else {
Box(Modifier.fillMaxSize().testTag(sheetTag))
}
}
}
- rule.onNodeWithTag(topTag).performTouchInput {
- swipeDown()
- swipeDown()
- }
-
+ scope.launch { sheetState.hide() }
rule.runOnIdle { assertThat(sheetState.currentValue).isEqualTo(SheetValue.Hidden) }
showShortContent = true
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonTest.kt
index 1029be4..3393ea3 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonTest.kt
@@ -135,7 +135,7 @@
rule
.onNode(isToggleable())
- .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Checkbox))
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button))
.assertIsEnabled()
.assertIsOff()
.performClick()
@@ -171,7 +171,7 @@
assertIsEnabled()
}
rule.onNodeWithTag("trailing button").apply {
- assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Checkbox))
+ assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button))
assertIsEnabled()
}
}
@@ -207,7 +207,7 @@
assertIsNotEnabled()
}
rule.onNodeWithTag("trailing button").apply {
- assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Checkbox))
+ assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button))
assertIsNotEnabled()
}
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
index 07bed9e..f86190b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
@@ -42,6 +42,7 @@
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.material3.internal.FloatProducer
import androidx.compose.material3.internal.ProvideContentColorTextStyle
import androidx.compose.material3.internal.systemBarsForVisualComponents
import androidx.compose.material3.tokens.BottomAppBarTokens
@@ -2597,7 +2598,7 @@
* the actions are optional.
*
* @param modifier a [Modifier]
- * @param scrolledOffset a [ScrolledOffset] that provides the app bar offset in pixels (note that
+ * @param scrolledOffset a [FloatProducer] that provides the app bar offset in pixels (note that
* when the app bar is scrolled, the lambda will output negative values)
* @param navigationIconContentColor the content color that will be applied via a
* [LocalContentColor] when composing the navigation icon
@@ -2623,7 +2624,7 @@
@Composable
private fun TopAppBarLayout(
modifier: Modifier,
- scrolledOffset: ScrolledOffset,
+ scrolledOffset: FloatProducer,
navigationIconContentColor: Color,
titleContentColor: Color,
actionIconContentColor: Color,
@@ -2720,7 +2721,7 @@
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private class TopAppBarMeasurePolicy(
- val scrolledOffset: ScrolledOffset,
+ val scrolledOffset: FloatProducer,
val titleVerticalArrangement: Arrangement.Vertical,
val titleHorizontalAlignment: TopAppBarTitleAlignment,
val titleBottomPadding: Int,
@@ -2761,7 +2762,7 @@
// Subtract the scrolledOffset from the maxHeight. The scrolledOffset is expected to be
// equal or smaller than zero.
- val scrolledOffsetValue = scrolledOffset.offset()
+ val scrolledOffsetValue = scrolledOffset()
val heightOffset = if (scrolledOffsetValue.isNaN()) 0 else scrolledOffsetValue.roundToInt()
val maxLayoutHeight = max(expandedHeight.roundToPx(), titlePlaceable.height)
@@ -2897,11 +2898,6 @@
}
}
-/** A functional interface for providing an app-bar scroll offset. */
-private fun interface ScrolledOffset {
- fun offset(): Float
-}
-
/**
* Returns a [TopAppBarScrollBehavior] that only adjusts its content offset, without adjusting any
* properties that affect the height of a top app bar.
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
index fddd3f7..0c52f23 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
@@ -58,18 +58,27 @@
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.takeOrElse
+import androidx.compose.ui.layout.IntrinsicMeasurable
+import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.offset
import androidx.compose.ui.util.fastFirst
import androidx.compose.ui.util.fastFirstOrNull
+import androidx.compose.ui.util.fastMaxOfOrNull
+import androidx.compose.ui.util.fastSumBy
/**
* <a href="https://m3.material.io/components/chips/overview" class="external"
@@ -2081,45 +2090,71 @@
}
)
}
- }
- ) { measurables, constraints ->
- val leadingIconPlaceable: Placeable? =
- measurables
- .fastFirstOrNull { it.layoutId == LeadingIconLayoutId }
- ?.measure(constraints.copy(minWidth = 0, minHeight = 0))
- val leadingIconWidth = leadingIconPlaceable.widthOrZero
- val leadingIconHeight = leadingIconPlaceable.heightOrZero
+ },
+ measurePolicy = remember { ChipLayoutMeasurePolicy() },
+ )
+ }
+}
- val trailingIconPlaceable: Placeable? =
- measurables
- .fastFirstOrNull { it.layoutId == TrailingIconLayoutId }
- ?.measure(constraints.copy(minWidth = 0, minHeight = 0))
- val trailingIconWidth = trailingIconPlaceable.widthOrZero
- val trailingIconHeight = trailingIconPlaceable.heightOrZero
+private class ChipLayoutMeasurePolicy : MeasurePolicy {
+ override fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: Constraints
+ ): MeasureResult {
+ val leadingIconPlaceable: Placeable? =
+ measurables
+ .fastFirstOrNull { it.layoutId == LeadingIconLayoutId }
+ ?.measure(constraints.copy(minWidth = 0, minHeight = 0))
+ val leadingIconWidth = leadingIconPlaceable.widthOrZero
+ val leadingIconHeight = leadingIconPlaceable.heightOrZero
- val labelPlaceable =
- measurables
- .fastFirst { it.layoutId == LabelLayoutId }
- .measure(
- constraints.offset(horizontal = -(leadingIconWidth + trailingIconWidth))
- )
+ val trailingIconPlaceable: Placeable? =
+ measurables
+ .fastFirstOrNull { it.layoutId == TrailingIconLayoutId }
+ ?.measure(constraints.copy(minWidth = 0, minHeight = 0))
+ val trailingIconWidth = trailingIconPlaceable.widthOrZero
+ val trailingIconHeight = trailingIconPlaceable.heightOrZero
- val width = leadingIconWidth + labelPlaceable.width + trailingIconWidth
- val height = maxOf(leadingIconHeight, labelPlaceable.height, trailingIconHeight)
+ val labelPlaceable =
+ measurables
+ .fastFirst { it.layoutId == LabelLayoutId }
+ .measure(constraints.offset(horizontal = -(leadingIconWidth + trailingIconWidth)))
- layout(width, height) {
- leadingIconPlaceable?.placeRelative(
- 0,
- Alignment.CenterVertically.align(leadingIconHeight, height)
- )
- labelPlaceable.placeRelative(leadingIconWidth, 0)
- trailingIconPlaceable?.placeRelative(
- leadingIconWidth + labelPlaceable.width,
- Alignment.CenterVertically.align(trailingIconHeight, height)
- )
- }
+ val width = leadingIconWidth + labelPlaceable.width + trailingIconWidth
+ val height = maxOf(leadingIconHeight, labelPlaceable.height, trailingIconHeight)
+
+ return layout(width, height) {
+ leadingIconPlaceable?.placeRelative(
+ 0,
+ Alignment.CenterVertically.align(leadingIconHeight, height)
+ )
+ labelPlaceable.placeRelative(leadingIconWidth, 0)
+ trailingIconPlaceable?.placeRelative(
+ leadingIconWidth + labelPlaceable.width,
+ Alignment.CenterVertically.align(trailingIconHeight, height)
+ )
}
}
+
+ override fun IntrinsicMeasureScope.minIntrinsicHeight(
+ measurables: List<IntrinsicMeasurable>,
+ width: Int
+ ): Int = measurables.fastMaxOfOrNull { it.minIntrinsicHeight(width) } ?: 0
+
+ override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+ measurables: List<IntrinsicMeasurable>,
+ width: Int
+ ): Int = measurables.fastMaxOfOrNull { it.maxIntrinsicHeight(width) } ?: 0
+
+ override fun IntrinsicMeasureScope.minIntrinsicWidth(
+ measurables: List<IntrinsicMeasurable>,
+ height: Int
+ ): Int = measurables.fastSumBy { it.minIntrinsicWidth(height) }
+
+ override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+ measurables: List<IntrinsicMeasurable>,
+ height: Int
+ ): Int = measurables.fastSumBy { it.maxIntrinsicWidth(height) }
}
/**
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
index 4ebaa83..09ba3d4 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
@@ -46,6 +46,7 @@
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.internal.AnchoredDraggableState
import androidx.compose.material3.internal.DraggableAnchors
+import androidx.compose.material3.internal.FloatProducer
import androidx.compose.material3.internal.Strings
import androidx.compose.material3.internal.anchoredDraggable
import androidx.compose.material3.internal.getString
@@ -797,7 +798,7 @@
drawerContainerColor: Color = DrawerDefaults.standardContainerColor,
drawerContentColor: Color = contentColorFor(drawerContainerColor),
drawerTonalElevation: Dp = DrawerDefaults.PermanentDrawerElevation,
- drawerOffset: () -> Float = { 0F },
+ drawerOffset: FloatProducer = FloatProducer { 0F },
content: @Composable ColumnScope.() -> Unit
) {
val density = LocalDensity.current
@@ -862,7 +863,7 @@
* @see horizontalScaleDown
*/
private fun Modifier.horizontalScaleUp(
- drawerOffset: () -> Float,
+ drawerOffset: FloatProducer,
drawerWidth: Float,
isRtl: Boolean
) = graphicsLayer {
@@ -880,7 +881,7 @@
* @see horizontalScaleUp
*/
private fun Modifier.horizontalScaleDown(
- drawerOffset: () -> Float,
+ drawerOffset: FloatProducer,
drawerWidth: Float,
isRtl: Boolean
) = graphicsLayer {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
index ca6c6f2..bcbd38b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
@@ -48,13 +48,13 @@
* <a href="https://m3.material.io/foundations/layout/understanding-layout/" class="external"
* target="_blank">Material Design layout</a>.
*
- * Scaffold implements the basic material design visual layout structure.
+ * Scaffold implements the basic Material Design visual layout structure.
*
- * This component provides API to put together several material components to construct your screen,
+ * This component provides API to put together several Material components to construct your screen,
* by ensuring proper layout strategy for them and collecting necessary data so these components
* will work together correctly.
*
- * Simple example of a Scaffold with [SmallTopAppBar], [FloatingActionButton]:
+ * Simple example of a Scaffold with [TopAppBar] and [FloatingActionButton]:
*
* @sample androidx.compose.material3.samples.SimpleScaffoldWithTopBar
*
@@ -62,7 +62,7 @@
*
* @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
* @param modifier the [Modifier] to be applied to this scaffold
- * @param topBar top app bar of the screen, typically a [SmallTopAppBar]
+ * @param topBar top app bar of the screen, typically a [TopAppBar]
* @param bottomBar bottom bar of the screen, typically a [NavigationBar]
* @param snackbarHost component to host [Snackbar]s that are pushed to be shown via
* [SnackbarHostState.showSnackbar], typically a [SnackbarHost]
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SearchBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SearchBar.kt
index 2ef7f6a..d7c7dce 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SearchBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SearchBar.kt
@@ -16,14 +16,17 @@
package androidx.compose.material3
+import androidx.annotation.FloatRange
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
@@ -38,16 +41,21 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
+import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged
+import androidx.compose.foundation.layout.only
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.GenericShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
@@ -60,11 +68,15 @@
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material3.SearchBarDefaults.InputFieldHeight
import androidx.compose.material3.internal.BackEventCompat
+import androidx.compose.material3.internal.BackEventProgress
import androidx.compose.material3.internal.BackHandler
+import androidx.compose.material3.internal.BasicEdgeToEdgeDialog
import androidx.compose.material3.internal.MutableWindowInsets
import androidx.compose.material3.internal.PredictiveBack
import androidx.compose.material3.internal.PredictiveBackHandler
+import androidx.compose.material3.internal.PredictiveBackState
import androidx.compose.material3.internal.Strings
+import androidx.compose.material3.internal.SwipeEdge
import androidx.compose.material3.internal.getString
import androidx.compose.material3.internal.textFieldBackground
import androidx.compose.material3.tokens.ElevationTokens
@@ -78,12 +90,20 @@
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableFloatState
import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import androidx.compose.runtime.structuralEqualityPolicy
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@@ -92,14 +112,22 @@
import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.addOutline
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionOnScreen
+import androidx.compose.ui.platform.InterceptPlatformTextInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
@@ -109,11 +137,13 @@
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
import androidx.compose.ui.util.fastFirst
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.lerp
@@ -124,7 +154,71 @@
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sign
+import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+/**
+ * <a href="https://m3.material.io/components/search/overview" class="external"
+ * target="_blank">Material Design search</a>.
+ *
+ * A search bar represents a field that allows users to enter a keyword or phrase and get relevant
+ * information. It can be used as a way to navigate through an app via search queries.
+ *
+ * 
+ *
+ * A search bar expands when focused to display dynamic suggestions or search results. An expanded
+ * [SearchBar] displays its results in a full-screen dialog. If this expansion behavior is
+ * undesirable, for example on large tablet screens, consider using [DockedSearchBar] instead.
+ *
+ * @param state the state of the search bar. This state should also be passed to the [inputField].
+ * @param inputField the input field of this search bar that allows entering a query, typically a
+ * [SearchBarDefaults.InputField].
+ * @param modifier the [Modifier] to be applied to this search bar when collapsed.
+ * @param shape the shape of this search bar when it is collapsed. When expanded, the shape will
+ * always be [SearchBarDefaults.fullScreenShape].
+ * @param colors [SearchBarColors] that will be used to resolve the colors used for this search bar
+ * in different states. See [SearchBarDefaults.colors].
+ * @param tonalElevation when [SearchBarColors.containerColor] is [ColorScheme.surface], a
+ * translucent primary color overlay is applied on top of the container. A higher tonal elevation
+ * value will result in a darker color in light theme and lighter color in dark theme. See also:
+ * [Surface].
+ * @param shadowElevation the elevation for the shadow below this search bar.
+ * @param content the content of this search bar to display search results below the [inputField].
+ */
+@ExperimentalMaterial3Api
+@Composable
+internal fun SearchBar(
+ state: SearchBarState,
+ inputField: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ shape: Shape = SearchBarDefaults.inputFieldShape,
+ colors: SearchBarColors = SearchBarDefaults.colors(),
+ tonalElevation: Dp = SearchBarDefaults.TonalElevation,
+ shadowElevation: Dp = SearchBarDefaults.ShadowElevation,
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ CollapsedSearchBar(
+ // Modifier gets passed to Collapsed because Expanded opens a separate dialog.
+ modifier = modifier,
+ state = state,
+ inputField = inputField,
+ shape = shape,
+ colors = colors,
+ tonalElevation = tonalElevation,
+ shadowElevation = shadowElevation
+ )
+ ExpandedFullScreenSearchBar(
+ state = state,
+ inputField = inputField,
+ collapsedShape = shape,
+ colors = colors,
+ tonalElevation = tonalElevation,
+ shadowElevation = shadowElevation,
+ content = content
+ )
+}
/**
* <a href="https://m3.material.io/components/search/overview" class="external"
@@ -328,6 +422,240 @@
BackHandler(enabled = expanded) { onExpandedChange(false) }
}
+/**
+ * A building block component representing a search bar in the collapsed state.
+ *
+ * Unless specific customization is needed, consider using a higher level component such as
+ * [SearchBar] or [DockedSearchBar] instead.
+ *
+ * @param state the state of the search bar. This state should also be passed to the [inputField].
+ * @param inputField the input field of this search bar that allows entering a query, typically a
+ * [SearchBarDefaults.InputField].
+ * @param modifier the [Modifier] to be applied to this collapsed search bar.
+ * @param shape the shape of this search bar.
+ * @param colors [SearchBarColors] that will be used to resolve the colors used for this search bar
+ * in different states. See [SearchBarDefaults.colors].
+ * @param tonalElevation when [SearchBarColors.containerColor] is [ColorScheme.surface], a
+ * translucent primary color overlay is applied on top of the container. A higher tonal elevation
+ * value will result in a darker color in light theme and lighter color in dark theme. See also:
+ * [Surface].
+ * @param shadowElevation the elevation for the shadow below this search bar.
+ */
+@Suppress("ComposableLambdaParameterNaming", "ComposableLambdaParameterPosition")
+@ExperimentalMaterial3Api
+@Composable
+internal fun CollapsedSearchBar(
+ state: SearchBarState,
+ inputField: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ shape: Shape = SearchBarDefaults.inputFieldShape,
+ colors: SearchBarColors = SearchBarDefaults.colors(),
+ tonalElevation: Dp = SearchBarDefaults.TonalElevation,
+ shadowElevation: Dp = SearchBarDefaults.ShadowElevation,
+) {
+ // Disable when collapsed to avoid keyboard flicker when the expanded search bar opens.
+ DisableSoftKeyboard {
+ Surface(
+ modifier = modifier.onGloballyPositioned { state.collapsedCoords = it },
+ shape = shape,
+ color = colors.containerColor,
+ contentColor = contentColorFor(colors.containerColor),
+ tonalElevation = tonalElevation,
+ shadowElevation = shadowElevation,
+ content = inputField,
+ )
+ }
+}
+
+/**
+ * A building block component representing a search bar that is currently expanding or in the
+ * expanded state. This component is displayed in a new full-screen dialog.
+ *
+ * Unless specific customization is needed, consider using a higher level component such as
+ * [SearchBar] or [DockedSearchBar] instead.
+ *
+ * @param state the state of the search bar. This state should also be passed to the [inputField].
+ * @param inputField the input field of this search bar that allows entering a query, typically a
+ * [SearchBarDefaults.InputField].
+ * @param modifier the [Modifier] to be applied to this expanded search bar.
+ * @param collapsedShape the shape of the search bar when it is collapsed. When fully expanded, the
+ * shape will always be [SearchBarDefaults.fullScreenShape].
+ * @param colors [SearchBarColors] that will be used to resolve the colors used for this search bar
+ * in different states. See [SearchBarDefaults.colors].
+ * @param tonalElevation when [SearchBarColors.containerColor] is [ColorScheme.surface], a
+ * translucent primary color overlay is applied on top of the container. A higher tonal elevation
+ * value will result in a darker color in light theme and lighter color in dark theme. See also:
+ * [Surface].
+ * @param shadowElevation the elevation for the shadow below this search bar.
+ * @param windowInsets the window insets that this search bar will respect.
+ * @param content the content of this search bar to display search results below the [inputField].
+ */
+@ExperimentalMaterial3Api
+@Composable
+internal fun ExpandedFullScreenSearchBar(
+ state: SearchBarState,
+ inputField: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ collapsedShape: Shape = SearchBarDefaults.inputFieldShape,
+ colors: SearchBarColors = SearchBarDefaults.colors(),
+ tonalElevation: Dp = SearchBarDefaults.TonalElevation,
+ shadowElevation: Dp = SearchBarDefaults.ShadowElevation,
+ windowInsets: @Composable () -> WindowInsets = { SearchBarDefaults.windowInsets },
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ if (!state.isExpanded) return
+
+ val coroutineScope = rememberCoroutineScope()
+
+ BasicEdgeToEdgeDialog(
+ onDismissRequest = { coroutineScope.launch { state.animateToCollapsed() } }
+ ) { predictiveBackState ->
+ val softwareKeyboardController = LocalSoftwareKeyboardController.current
+ SideEffect { state.softwareKeyboardController = softwareKeyboardController }
+ FullScreenSearchBarLayout(
+ state = state,
+ predictiveBackState = predictiveBackState,
+ inputField = inputField,
+ modifier = modifier,
+ collapsedShape = collapsedShape,
+ colors = colors,
+ tonalElevation = tonalElevation,
+ shadowElevation = shadowElevation,
+ windowInsets = windowInsets(),
+ content = content,
+ )
+ }
+}
+
+/**
+ * The state of a search bar.
+ *
+ * @property focusRequester The [FocusRequester] to request focus on the search bar.
+ * [SearchBarDefaults.InputField] applies this automatically. Custom input fields must attach this
+ * focus requester using [Modifier.focusRequester].
+ */
+@ExperimentalMaterial3Api
+@Stable
+internal class SearchBarState
+private constructor(
+ private val animatable: Animatable<Float, AnimationVector1D>,
+ private val animationSpecForExpand: AnimationSpec<Float>,
+ private val animationSpecForCollapse: AnimationSpec<Float>,
+ val focusRequester: FocusRequester,
+) {
+ /**
+ * Construct a [SearchBarState].
+ *
+ * @param initialExpanded the initial value of whether the search bar is expanded.
+ * @param animationSpecForExpand the animation spec used when the search bar expands.
+ * @param animationSpecForCollapse the animation spec used when the search bar collapses.
+ * @param focusRequester the focus requester to be applied to the search bar's input field.
+ */
+ constructor(
+ initialExpanded: Boolean,
+ animationSpecForExpand: AnimationSpec<Float>,
+ animationSpecForCollapse: AnimationSpec<Float>,
+ focusRequester: FocusRequester = FocusRequester(),
+ ) : this(
+ animatable = Animatable(if (initialExpanded) 1f else 0f),
+ animationSpecForExpand = animationSpecForExpand,
+ animationSpecForCollapse = animationSpecForCollapse,
+ focusRequester = focusRequester,
+ )
+
+ /** The layout coordinates, if available, of the search bar when it is collapsed. */
+ var collapsedCoords: LayoutCoordinates? by mutableStateOf(null)
+
+ /**
+ * The animation progress of the search bar, where 0 represents the collapsed state and 1
+ * represents the expanded state.
+ */
+ @get:FloatRange(from = 0.0, to = 1.0)
+ val progress: Float
+ get() = animatable.value.coerceIn(0f, 1f)
+
+ /**
+ * Whether this search bar is expanded (showing search results), or in the process of expanding.
+ */
+ val isExpanded: Boolean by derivedStateOf { progress > 0f }
+
+ internal var softwareKeyboardController: SoftwareKeyboardController? = null
+
+ /** Animate the search bar to its expanded state. */
+ suspend fun animateToExpanded() {
+ animatable.animateTo(targetValue = 1f, animationSpec = animationSpecForExpand)
+ focusRequester.requestFocus()
+ }
+
+ /** Animate the search bar to its collapsed state. */
+ suspend fun animateToCollapsed() {
+ softwareKeyboardController?.hide()
+ animatable.animateTo(targetValue = 0f, animationSpec = animationSpecForCollapse)
+ }
+
+ /** Snap the search bar progress to the given [fraction]. */
+ suspend fun snapTo(fraction: Float) {
+ animatable.snapTo(fraction)
+ }
+
+ companion object {
+ fun Saver(
+ animationSpecForExpand: AnimationSpec<Float>,
+ animationSpecForCollapse: AnimationSpec<Float>,
+ focusRequester: FocusRequester,
+ ): Saver<SearchBarState, *> =
+ listSaver(
+ save = { listOf(it.progress) },
+ restore = {
+ SearchBarState(
+ animatable = Animatable(it[0], Float.VectorConverter),
+ animationSpecForExpand = animationSpecForExpand,
+ animationSpecForCollapse = animationSpecForCollapse,
+ focusRequester = focusRequester,
+ )
+ },
+ )
+ }
+}
+
+/**
+ * Create and remember a [SearchBarState].
+ *
+ * @param initialExpanded the initial value of whether the search bar is expanded.
+ * @param animationSpecForExpand the animation spec used when the search bar expands.
+ * @param animationSpecForCollapse the animation spec used when the search bar collapses.
+ * @param focusRequester the focus requester to be applied to the search bar's input field.
+ */
+@ExperimentalMaterial3Api
+@Composable
+internal fun rememberSearchBarState(
+ initialExpanded: Boolean = false,
+ animationSpecForExpand: AnimationSpec<Float> = MotionSchemeKeyTokens.SlowSpatial.value(),
+ animationSpecForCollapse: AnimationSpec<Float> = MotionSchemeKeyTokens.DefaultSpatial.value(),
+ focusRequester: FocusRequester? = null,
+): SearchBarState {
+ @Suppress("NAME_SHADOWING") val focusRequester = focusRequester ?: remember { FocusRequester() }
+ return rememberSaveable(
+ initialExpanded,
+ animationSpecForExpand,
+ animationSpecForCollapse,
+ focusRequester,
+ saver =
+ SearchBarState.Saver(
+ animationSpecForExpand = animationSpecForExpand,
+ animationSpecForCollapse = animationSpecForCollapse,
+ focusRequester = focusRequester,
+ )
+ ) {
+ SearchBarState(
+ initialExpanded = initialExpanded,
+ animationSpecForExpand = animationSpecForExpand,
+ animationSpecForCollapse = animationSpecForCollapse,
+ focusRequester = focusRequester,
+ )
+ }
+}
+
/** Defaults used in [SearchBar] and [DockedSearchBar]. */
@ExperimentalMaterial3Api
object SearchBarDefaults {
@@ -361,7 +689,7 @@
/** Default window insets for a [SearchBar]. */
val windowInsets: WindowInsets
- @Composable get() = WindowInsets.statusBars
+ @Composable get() = WindowInsets.safeDrawing
/**
* Creates a [SearchBarColors] that represents the different colors used in parts of the search
@@ -497,8 +825,170 @@
* A text field to input a query in a search bar.
*
* This overload of [InputField] uses [TextFieldState] to keep track of the text content and
- * position of the cursor or selection. It is the recommended overload to use with [SearchBar]
- * and [DockedSearchBar].
+ * position of the cursor or selection, and [SearchBarState] to keep track of the state of the
+ * search bar.
+ *
+ * @param textFieldState [TextFieldState] that holds the internal editing state of the input
+ * field.
+ * @param searchBarState the state of the search bar as a whole.
+ * @param onSearch the callback to be invoked when the input service triggers the
+ * [ImeAction.Search] action. The current query in the [textFieldState] comes as a parameter
+ * of the callback.
+ * @param modifier the [Modifier] to be applied to this input field.
+ * @param enabled the enabled state of this input field. When `false`, this component will not
+ * respond to user input, and it will appear visually disabled and disabled to accessibility
+ * services.
+ * @param readOnly controls the editable state of the input field. When `true`, the field cannot
+ * be modified. However, a user can focus it and copy text from it.
+ * @param textStyle the style to be applied to the input text. Defaults to [LocalTextStyle].
+ * @param placeholder the placeholder to be displayed when the input text is empty.
+ * @param leadingIcon the leading icon to be displayed at the start of the input field.
+ * @param trailingIcon the trailing icon to be displayed at the end of the input field.
+ * @param prefix the optional prefix to be displayed before the input text.
+ * @param suffix the optional suffix to be displayed after the input text.
+ * @param inputTransformation optional [InputTransformation] that will be used to transform
+ * changes to the [TextFieldState] made by the user. The transformation will be applied to
+ * changes made by hardware and software keyboard events, pasting or dropping text,
+ * accessibility services, and tests. The transformation will _not_ be applied when changing
+ * the [textFieldState] programmatically, or when the transformation is changed. If the
+ * transformation is changed on an existing text field, it will be applied to the next user
+ * edit. The transformation will not immediately affect the current [textFieldState].
+ * @param outputTransformation optional [OutputTransformation] that transforms how the contents
+ * of the text field are presented.
+ * @param scrollState scroll state that manages the horizontal scroll of the input field.
+ * @param shape the shape of the input field.
+ * @param colors [TextFieldColors] that will be used to resolve the colors used for this input
+ * field in different states. See [SearchBarDefaults.inputFieldColors].
+ * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
+ * emitting [Interaction]s for this input field. You can use this to change the search bar's
+ * appearance or preview the search bar in different states. Note that if `null` is provided,
+ * interactions will still happen internally.
+ */
+ @ExperimentalMaterial3Api
+ @Composable
+ internal fun InputField(
+ textFieldState: TextFieldState,
+ searchBarState: SearchBarState,
+ onSearch: (String) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ readOnly: Boolean = false,
+ textStyle: TextStyle = LocalTextStyle.current,
+ placeholder: @Composable (() -> Unit)? = null,
+ leadingIcon: @Composable (() -> Unit)? = null,
+ trailingIcon: @Composable (() -> Unit)? = null,
+ prefix: @Composable (() -> Unit)? = null,
+ suffix: @Composable (() -> Unit)? = null,
+ inputTransformation: InputTransformation? = null,
+ outputTransformation: OutputTransformation? = null,
+ scrollState: ScrollState = rememberScrollState(),
+ shape: Shape = inputFieldShape,
+ colors: TextFieldColors = inputFieldColors(),
+ interactionSource: MutableInteractionSource? = null,
+ ) {
+ @Suppress("NAME_SHADOWING")
+ val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
+
+ val focused = interactionSource.collectIsFocusedAsState().value
+ val focusManager = LocalFocusManager.current
+
+ val searchSemantics = getString(Strings.SearchBarSearch)
+ val suggestionsAvailableSemantics = getString(Strings.SuggestionsAvailable)
+
+ val textColor =
+ textStyle.color.takeOrElse {
+ colors.textColor(enabled, isError = false, focused = focused)
+ }
+ val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
+
+ val coroutineScope = rememberCoroutineScope()
+
+ BasicTextField(
+ state = textFieldState,
+ modifier =
+ modifier
+ .sizeIn(
+ minWidth = SearchBarMinWidth,
+ maxWidth = SearchBarMaxWidth,
+ minHeight = InputFieldHeight,
+ )
+ .focusRequester(searchBarState.focusRequester)
+ .onFocusChanged {
+ if (it.isFocused) {
+ coroutineScope.launch { searchBarState.animateToExpanded() }
+ }
+ }
+ .semantics {
+ contentDescription = searchSemantics
+ if (searchBarState.isExpanded) {
+ stateDescription = suggestionsAvailableSemantics
+ }
+ onClick {
+ searchBarState.focusRequester.requestFocus()
+ true
+ }
+ },
+ enabled = enabled,
+ readOnly = readOnly,
+ lineLimits = TextFieldLineLimits.SingleLine,
+ textStyle = mergedTextStyle,
+ cursorBrush = SolidColor(colors.cursorColor(isError = false)),
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
+ onKeyboardAction = { onSearch(textFieldState.text.toString()) },
+ interactionSource = interactionSource,
+ inputTransformation = inputTransformation,
+ outputTransformation = outputTransformation,
+ scrollState = scrollState,
+ decorator =
+ TextFieldDefaults.decorator(
+ state = textFieldState,
+ enabled = enabled,
+ lineLimits = TextFieldLineLimits.SingleLine,
+ outputTransformation = outputTransformation,
+ interactionSource = interactionSource,
+ placeholder = placeholder,
+ leadingIcon =
+ leadingIcon?.let { leading ->
+ { Box(Modifier.offset(x = SearchBarIconOffsetX)) { leading() } }
+ },
+ trailingIcon =
+ trailingIcon?.let { trailing ->
+ { Box(Modifier.offset(x = -SearchBarIconOffsetX)) { trailing() } }
+ },
+ prefix = prefix,
+ suffix = suffix,
+ colors = colors,
+ contentPadding = TextFieldDefaults.contentPaddingWithoutLabel(),
+ container = {
+ val containerColor =
+ animateColorAsState(
+ targetValue =
+ colors.containerColor(
+ enabled = enabled,
+ isError = false,
+ focused = focused
+ ),
+ animationSpec = MotionSchemeKeyTokens.FastEffects.value(),
+ )
+ Box(Modifier.textFieldBackground(containerColor::value, shape))
+ },
+ )
+ )
+
+ val shouldClearFocus = !searchBarState.isExpanded && focused
+ LaunchedEffect(searchBarState.isExpanded) {
+ if (shouldClearFocus) {
+ focusManager.clearFocus()
+ }
+ }
+ }
+
+ /**
+ * A text field to input a query in a search bar.
+ *
+ * This overload of [InputField] uses [TextFieldState] to keep track of the text content and
+ * position of the cursor or selection, and [expanded] and [onExpandedChange] to keep track of
+ * the state of the search bar.
*
* @param state [TextFieldState] that holds the internal editing state of the input field.
* @param onSearch the callback to be invoked when the input service triggers the
@@ -1355,6 +1845,228 @@
}
}
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun FullScreenSearchBarLayout(
+ state: SearchBarState,
+ predictiveBackState: PredictiveBackState,
+ inputField: @Composable () -> Unit,
+ modifier: Modifier,
+ collapsedShape: Shape,
+ colors: SearchBarColors,
+ tonalElevation: Dp,
+ shadowElevation: Dp,
+ windowInsets: WindowInsets,
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ val backEvent by remember { derivedStateOf { predictiveBackState.value } }
+ val firstInProgressValue =
+ remember { mutableStateOf<BackEventProgress.InProgress?>(null) }
+ .apply {
+ when (val event = backEvent) {
+ is BackEventProgress.InProgress -> if (value == null) value = event
+ BackEventProgress.NotRunning -> value = null
+ BackEventProgress.Completed -> Unit
+ }
+ }
+ val lastInProgressValue =
+ remember { mutableStateOf<BackEventProgress.InProgress?>(null) }
+ .apply {
+ when (val event = backEvent) {
+ is BackEventProgress.InProgress -> value = event
+ BackEventProgress.NotRunning -> value = null
+ BackEventProgress.Completed -> Unit
+ }
+ }
+
+ val density = LocalDensity.current
+ val fullScreenShape = SearchBarDefaults.fullScreenShape
+ val animatedShape =
+ remember(density, fullScreenShape) {
+ GenericShape { size, layoutDirection ->
+ if (collapsedShape === CircleShape && fullScreenShape === RectangleShape) {
+ // The shape can only be animated if it's the default spec value
+ val radius =
+ with(density) {
+ val fraction =
+ max(1 - state.progress, lastInProgressValue.value.transform())
+ (SearchBarCornerRadius * fraction).toPx()
+ }
+ if (radius < 1e-3) {
+ addRect(size.toRect())
+ } else {
+ addRoundRect(RoundRect(size.toRect(), CornerRadius(radius)))
+ }
+ } else {
+ val shape = if (state.progress < 0.5f) collapsedShape else fullScreenShape
+ addOutline(shape.createOutline(size, layoutDirection, density))
+ }
+ }
+ }
+
+ // Top window insets need to be animated, but `Modifier.windowInsetsPadding` does not support
+ // animation. The top insets are separated out so the animation calculations can be done
+ // manually in the Layout's MeasureScope.
+ val unconsumedInsets = remember { MutableWindowInsets() }
+ val nonTopInsets =
+ unconsumedInsets.insets.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom)
+ Layout(
+ modifier =
+ modifier
+ .onConsumedWindowInsetsChanged { consumedInsets ->
+ unconsumedInsets.insets = windowInsets.exclude(consumedInsets)
+ }
+ .consumeWindowInsets(windowInsets),
+ content = {
+ Box(
+ modifier =
+ Modifier.layoutId(LayoutIdInputField)
+ .padding(nonTopInsets.only(WindowInsetsSides.Horizontal).asPaddingValues()),
+ propagateMinConstraints = true,
+ ) {
+ inputField()
+ }
+
+ Surface(
+ modifier = Modifier.layoutId(LayoutIdSurface),
+ shape = animatedShape,
+ color = colors.containerColor,
+ contentColor = contentColorFor(colors.containerColor),
+ tonalElevation = tonalElevation,
+ shadowElevation = shadowElevation,
+ content = {},
+ )
+
+ Column(
+ Modifier.layoutId(LayoutIdSearchContent).padding(nonTopInsets.asPaddingValues())
+ ) {
+ HorizontalDivider(color = colors.dividerColor)
+ content()
+ }
+ },
+ ) { measurables, constraints ->
+ val predictiveBackProgress = lastInProgressValue.value.transform()
+
+ val predictiveBackEndWidth =
+ (constraints.maxWidth * SearchBarPredictiveBackMinScale)
+ .roundToInt()
+ .coerceAtLeast(state.collapsedBounds.width)
+ val predictiveBackEndHeight =
+ (constraints.maxHeight * SearchBarPredictiveBackMinScale)
+ .roundToInt()
+ .coerceAtLeast(state.collapsedBounds.height)
+ val endWidth = lerp(constraints.maxWidth, predictiveBackEndWidth, predictiveBackProgress)
+ val endHeight = lerp(constraints.maxHeight, predictiveBackEndHeight, predictiveBackProgress)
+ val width =
+ constraints.constrainWidth(lerp(state.collapsedBounds.width, endWidth, state.progress))
+ val height =
+ constraints.constrainHeight(
+ lerp(state.collapsedBounds.height, endHeight, state.progress)
+ )
+
+ val surfaceMeasurable = measurables.fastFirst { it.layoutId == LayoutIdSurface }
+ val surfacePlaceable = surfaceMeasurable.measure(Constraints.fixed(width, height))
+
+ val inputFieldMeasurable = measurables.fastFirst { it.layoutId == LayoutIdInputField }
+ val inputFieldPlaceable =
+ inputFieldMeasurable.measure(Constraints.fixed(width, state.collapsedBounds.height))
+
+ val contentMeasurable = measurables.fastFirst { it.layoutId == LayoutIdSearchContent }
+ val contentPlaceable =
+ contentMeasurable.measure(
+ Constraints(
+ minWidth = width,
+ maxWidth = width,
+ minHeight = 0,
+ maxHeight = height,
+ )
+ )
+
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ val topPadding =
+ unconsumedInsets.getTop(this@Layout) + SearchBarVerticalPadding.roundToPx()
+ val bottomPadding = SearchBarVerticalPadding.roundToPx()
+ val animatedTopPadding =
+ lerp(0, topPadding, min(state.progress, 1 - predictiveBackProgress))
+ val animatedBottomPadding = lerp(0, bottomPadding, state.progress)
+
+ fun BackEventProgress.InProgress.endOffsetX(): Int =
+ (if (swipeEdge == SwipeEdge.Left) {
+ constraints.maxWidth -
+ SearchBarPredictiveBackMinMargin.roundToPx() -
+ predictiveBackEndWidth
+ } else {
+ SearchBarPredictiveBackMinMargin.roundToPx()
+ })
+ .coerceAtLeast(state.collapsedBounds.right - predictiveBackEndWidth)
+ .coerceAtMost(state.collapsedBounds.left)
+
+ fun BackEventProgress.InProgress.endOffsetY(): Int {
+ val absoluteDeltaY = this.touchY - (firstInProgressValue.value?.touchY ?: return 0)
+ val relativeDeltaY = abs(absoluteDeltaY) / constraints.maxHeight
+
+ val availableVerticalSpace =
+ ((constraints.maxHeight - predictiveBackEndHeight) / 2 -
+ SearchBarPredictiveBackMinMargin.roundToPx())
+ .coerceAtLeast(0)
+ val totalOffsetY =
+ min(
+ availableVerticalSpace,
+ SearchBarPredictiveBackMaxOffsetY.roundToPx(),
+ )
+ val interpolatedOffsetY = lerp(0, totalOffsetY, relativeDeltaY)
+ return (interpolatedOffsetY * sign(absoluteDeltaY).toInt() + topPadding)
+ .coerceAtMost(state.collapsedBounds.top)
+ }
+
+ val endOffsetX =
+ lerp(
+ 0,
+ lastInProgressValue.value?.endOffsetX() ?: 0,
+ predictiveBackProgress,
+ )
+ val endOffsetY =
+ lerp(
+ 0,
+ lastInProgressValue.value?.endOffsetY() ?: 0,
+ predictiveBackProgress,
+ )
+ val offsetX = lerp(state.collapsedBounds.left, endOffsetX, state.progress)
+ val offsetY = lerp(state.collapsedBounds.top, endOffsetY, state.progress)
+
+ surfacePlaceable.place(x = offsetX, y = offsetY)
+ inputFieldPlaceable.place(x = offsetX, y = offsetY + animatedTopPadding)
+ contentPlaceable.placeWithLayer(
+ x = offsetX,
+ y =
+ offsetY +
+ animatedTopPadding +
+ inputFieldPlaceable.height +
+ animatedBottomPadding,
+ layerBlock = { alpha = state.progress },
+ )
+ }
+ }
+}
+
+private fun BackEventProgress.InProgress?.transform(): Float =
+ if (this == null) 0f else PredictiveBack.transform(this.progress)
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+private fun DisableSoftKeyboard(content: @Composable () -> Unit) {
+ InterceptPlatformTextInput(
+ interceptor = { _, _ -> awaitCancellation() },
+ content = content,
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+private val SearchBarState.collapsedBounds: IntRect
+ get() =
+ collapsedCoords?.let { IntRect(offset = it.positionOnScreen().round(), size = it.size) }
+ ?: IntRect.Zero
+
private fun calculatePredictiveBackMultiplier(
currentBackEvent: BackEventCompat?,
progress: Float,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
index 1b29dd1..2e0704a 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
@@ -148,9 +148,10 @@
* services.
* @param valueRange range of values that this slider can take. The passed [value] will be coerced
* to this range.
- * @param steps if positive, specifies the amount of discrete allowable values (in addition to the
- * endpoints of the value range). Step values are evenly distributed across the range. If 0, the
- * slider will behave continuously and allow any value from the range. Must not be negative.
+ * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
+ * of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
+ * distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
+ * continuously and allow any value from the range. Must not be negative.
* @param onValueChangeFinished called when value change has ended. This should not be used to
* update the slider value (use [onValueChange] instead), but rather to know when the user has
* completed selecting a new value by ending a drag or a click.
@@ -240,9 +241,10 @@
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this slider. You can create and pass in your own `remember`ed instance to observe
* [Interaction]s and customize the appearance / behavior of this slider in different states.
- * @param steps if positive, specifies the amount of discrete allowable values (in addition to the
- * endpoints of the value range). Step values are evenly distributed across the range. If 0, the
- * slider will behave continuously and allow any value from the range. Must not be negative.
+ * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
+ * of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
+ * distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
+ * continuously and allow any value from the range. Must not be negative.
* @param thumb the thumb to be displayed on the slider, it is placed on top of the track. The
* lambda receives a [SliderState] which is used to obtain the current active track.
* @param track the track to be displayed on the slider, it is placed underneath the thumb. The
@@ -389,9 +391,10 @@
* @param enabled whether or not component is enabled and can we interacted with or not
* @param valueRange range of values that Range Slider values can take. Passed [value] will be
* coerced to this range
- * @param steps if positive, specifies the amount of discrete allowable values (in addition to the
- * endpoints of the value range). Step values are evenly distributed across the range. If 0, the
- * range slider will behave continuously and allow any value from the range. Must not be negative.
+ * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
+ * of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
+ * distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
+ * continuously and allow any value from the range. Must not be negative.
* @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
* shouldn't be used to update the range slider values (use [onValueChange] for that), but rather
* to know when the user has completed selecting a new value by ending a drag or a click.
@@ -489,9 +492,10 @@
* @param endInteractionSource the [MutableInteractionSource] representing the stream of
* [Interaction]s for the end thumb. You can create and pass in your own `remember`ed instance to
* observe.
- * @param steps if positive, specifies the amount of discrete allowable values (in addition to the
- * endpoints of the value range). Step values are evenly distributed across the range. If 0, the
- * range slider will behave continuously and allow any value from the range. Must not be negative.
+ * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
+ * of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
+ * distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
+ * continuously and allow any value from the range. Must not be negative.
* @param startThumb the start thumb to be displayed on the Range Slider. The lambda receives a
* [RangeSliderState] which is used to obtain the current active track.
* @param endThumb the end thumb to be displayed on the Range Slider. The lambda receives a
@@ -2020,9 +2024,10 @@
*
* @param value [Float] that indicates the initial position of the thumb. If outside of [valueRange]
* provided, value will be coerced to this range.
- * @param steps if positive, specifies the amount of discrete allowable values (in addition to the
- * endpoints of the value range). Step values are evenly distributed across the range. If 0, the
- * slider will behave continuously and allow any value from the range. Must not be negative.
+ * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
+ * of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
+ * distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
+ * continuously and allow any value from the range. Must not be negative.
* @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
* shouldn't be used to update the range slider values (use [onValueChange] for that), but rather
* to know when the user has completed selecting a new value by ending a drag or a click.
@@ -2141,9 +2146,10 @@
* slider. If outside of [valueRange] provided, value will be coerced to this range.
* @param activeRangeEnd [Float] that indicates the initial end of the active range of the slider.
* If outside of [valueRange] provided, value will be coerced to this range.
- * @param steps if positive, specifies the amount of discrete allowable values (in addition to the
- * endpoints of the value range). Step values are evenly distributed across the range. If 0, the
- * range slider will behave continuously and allow any value from the range. Must not be negative.
+ * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
+ * of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
+ * distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
+ * continuously and allow any value from the range. Must not be negative.
* @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
* shouldn't be used to update the range slider values (use [onValueChange] for that), but rather
* to know when the user has completed selecting a new value by ending a drag or a click.
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
index d4c416f..b1a95b3 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
@@ -532,7 +532,7 @@
)
}
}
- .semantics { role = Role.Checkbox },
+ .semantics { role = Role.Button },
enabled = enabled,
shape = shape,
color = colors.containerColor,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
index 9006ec5..95ce48f 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
@@ -16,9 +16,16 @@
package androidx.compose.material3
-import androidx.compose.foundation.BorderStroke
+import androidx.compose.animation.VectorConverter
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.AnimationVector4D
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.snap
import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.interaction.FocusInteraction
import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Box
@@ -41,6 +48,7 @@
import androidx.compose.foundation.text.input.TextFieldLineLimits.SingleLine
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
+import androidx.compose.material3.TextFieldDefaults.defaultTextFieldColors
import androidx.compose.material3.internal.AboveLabelBottomPadding
import androidx.compose.material3.internal.AboveLabelHorizontalPadding
import androidx.compose.material3.internal.ContainerId
@@ -67,15 +75,17 @@
import androidx.compose.material3.internal.subtractConstraintSafely
import androidx.compose.material3.internal.textFieldHorizontalIconPadding
import androidx.compose.material3.internal.widthOrZero
+import androidx.compose.material3.tokens.FilledTextFieldTokens
+import androidx.compose.material3.tokens.MotionSchemeKeyTokens
import androidx.compose.material3.tokens.MotionTokens.EasingEmphasizedAccelerateCubicBezier
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.draw.CacheDrawModifierNode
import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
@@ -90,6 +100,11 @@
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.currentValueOf
+import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
@@ -113,6 +128,8 @@
import androidx.compose.ui.util.lerp
import kotlin.math.max
import kotlin.math.roundToInt
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
/**
* <a href="https://m3.material.io/components/text-fields/overview" class="external"
@@ -1400,44 +1417,242 @@
}
}
-/** A draw modifier that draws a bottom indicator line in [TextField] */
-internal fun Modifier.drawIndicatorLine(
- indicatorBorder: State<BorderStroke>,
- textFieldShape: Shape,
-): Modifier {
- return drawWithCache {
- val strokeWidth = indicatorBorder.value.width.toPx()
- val textFieldShapePath =
- Path().apply {
- addOutline(
- textFieldShape.createOutline(
- size,
- layoutDirection,
- density = this@drawWithCache
- )
- )
- }
- val linePath =
- Path().apply {
- addRect(
- Rect(
- left = 0f,
- top = size.height - strokeWidth,
- right = size.width,
- bottom = size.height,
- )
- )
- }
- val clippedLine = linePath and textFieldShapePath
+internal data class IndicatorLineElement(
+ val enabled: Boolean,
+ val isError: Boolean,
+ val interactionSource: InteractionSource,
+ val colors: TextFieldColors?,
+ val textFieldShape: Shape?,
+ val focusedIndicatorLineThickness: Dp,
+ val unfocusedIndicatorLineThickness: Dp,
+) : ModifierNodeElement<IndicatorLineNode>() {
+ override fun create(): IndicatorLineNode {
+ return IndicatorLineNode(
+ enabled = enabled,
+ isError = isError,
+ interactionSource = interactionSource,
+ colors = colors,
+ textFieldShape = textFieldShape,
+ focusedIndicatorWidth = focusedIndicatorLineThickness,
+ unfocusedIndicatorWidth = unfocusedIndicatorLineThickness,
+ )
+ }
- onDrawWithContent {
- drawContent()
- drawPath(
- path = clippedLine,
- brush = indicatorBorder.value.brush,
+ override fun update(node: IndicatorLineNode) {
+ node.update(
+ enabled = enabled,
+ isError = isError,
+ interactionSource = interactionSource,
+ colors = colors,
+ textFieldShape = textFieldShape,
+ focusedIndicatorWidth = focusedIndicatorLineThickness,
+ unfocusedIndicatorWidth = unfocusedIndicatorLineThickness,
+ )
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "indicatorLine"
+ properties["enabled"] = enabled
+ properties["isError"] = isError
+ properties["interactionSource"] = interactionSource
+ properties["colors"] = colors
+ properties["textFieldShape"] = textFieldShape
+ properties["focusedIndicatorLineThickness"] = focusedIndicatorLineThickness
+ properties["unfocusedIndicatorLineThickness"] = unfocusedIndicatorLineThickness
+ }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+internal class IndicatorLineNode(
+ private var enabled: Boolean,
+ private var isError: Boolean,
+ private var interactionSource: InteractionSource,
+ colors: TextFieldColors?,
+ textFieldShape: Shape?,
+ private var focusedIndicatorWidth: Dp,
+ private var unfocusedIndicatorWidth: Dp,
+) : DelegatingNode(), CompositionLocalConsumerModifierNode {
+ private var focused = false
+ private var trackFocusStateJob: Job? = null
+
+ private var _colors: TextFieldColors? = colors
+ private val colors: TextFieldColors
+ get() =
+ _colors
+ ?: currentValueOf(LocalColorScheme)
+ .defaultTextFieldColors(currentValueOf(LocalTextSelectionColors))
+
+ // Must be initialized in `onAttach` so `colors` can read from the `MaterialTheme`
+ private var colorAnimatable: Animatable<Color, AnimationVector4D>? = null
+
+ private var _shape: Shape? = textFieldShape
+ private set(value) {
+ if (field != value) {
+ field = value
+ drawWithCacheModifierNode.invalidateDrawCache()
+ }
+ }
+
+ private val shape: Shape
+ get() =
+ _shape ?: currentValueOf(LocalShapes).fromToken(FilledTextFieldTokens.ContainerShape)
+
+ private val widthAnimatable: Animatable<Dp, AnimationVector1D> =
+ Animatable(
+ initialValue =
+ if (focused && this.enabled) this.focusedIndicatorWidth
+ else this.unfocusedIndicatorWidth,
+ typeConverter = Dp.VectorConverter,
+ )
+
+ fun update(
+ enabled: Boolean,
+ isError: Boolean,
+ interactionSource: InteractionSource,
+ colors: TextFieldColors?,
+ textFieldShape: Shape?,
+ focusedIndicatorWidth: Dp,
+ unfocusedIndicatorWidth: Dp,
+ ) {
+ var shouldInvalidate = false
+
+ if (this.enabled != enabled) {
+ this.enabled = enabled
+ shouldInvalidate = true
+ }
+
+ if (this.isError != isError) {
+ this.isError = isError
+ shouldInvalidate = true
+ }
+
+ if (this.interactionSource !== interactionSource) {
+ this.interactionSource = interactionSource
+ trackFocusStateJob?.cancel()
+ trackFocusStateJob = coroutineScope.launch { trackFocusState() }
+ }
+
+ if (this._colors != colors) {
+ this._colors = colors
+ shouldInvalidate = true
+ }
+
+ if (this._shape != textFieldShape) {
+ this._shape = textFieldShape
+ shouldInvalidate = true
+ }
+
+ if (this.focusedIndicatorWidth != focusedIndicatorWidth) {
+ this.focusedIndicatorWidth = focusedIndicatorWidth
+ shouldInvalidate = true
+ }
+
+ if (this.unfocusedIndicatorWidth != unfocusedIndicatorWidth) {
+ this.unfocusedIndicatorWidth = unfocusedIndicatorWidth
+ shouldInvalidate = true
+ }
+
+ if (shouldInvalidate) {
+ invalidateIndicator()
+ }
+ }
+
+ override val shouldAutoInvalidate: Boolean
+ get() = false
+
+ override fun onAttach() {
+ trackFocusStateJob = coroutineScope.launch { trackFocusState() }
+ if (colorAnimatable == null) {
+ val initialColor = colors.indicatorColor(enabled, isError, focused)
+ colorAnimatable =
+ Animatable(
+ initialValue = initialColor,
+ typeConverter = Color.VectorConverter(initialColor.colorSpace),
+ )
+ }
+ }
+
+ /** Copied from [InteractionSource.collectIsFocusedAsState] */
+ private suspend fun trackFocusState() {
+ focused = false
+ val focusInteractions = mutableListOf<FocusInteraction.Focus>()
+ interactionSource.interactions.collect { interaction ->
+ when (interaction) {
+ is FocusInteraction.Focus -> focusInteractions.add(interaction)
+ is FocusInteraction.Unfocus -> focusInteractions.remove(interaction.focus)
+ }
+ val isFocused = focusInteractions.isNotEmpty()
+ if (isFocused != focused) {
+ focused = isFocused
+ invalidateIndicator()
+ }
+ }
+ }
+
+ private fun invalidateIndicator() {
+ coroutineScope.launch {
+ colorAnimatable?.animateTo(
+ targetValue = colors.indicatorColor(enabled, isError, focused),
+ animationSpec =
+ if (enabled) {
+ currentValueOf(LocalMotionScheme)
+ .fromToken(MotionSchemeKeyTokens.FastEffects)
+ } else {
+ snap()
+ },
+ )
+ }
+ coroutineScope.launch {
+ widthAnimatable.animateTo(
+ targetValue =
+ if (focused && enabled) focusedIndicatorWidth else unfocusedIndicatorWidth,
+ animationSpec =
+ if (enabled) {
+ currentValueOf(LocalMotionScheme)
+ .fromToken(MotionSchemeKeyTokens.FastSpatial)
+ } else {
+ snap()
+ },
)
}
}
+
+ private val drawWithCacheModifierNode =
+ delegate(
+ CacheDrawModifierNode {
+ val strokeWidth = widthAnimatable.value.toPx()
+ val textFieldShapePath =
+ Path().apply {
+ addOutline(
+ [email protected](
+ size,
+ layoutDirection,
+ density = this@CacheDrawModifierNode
+ )
+ )
+ }
+ val linePath =
+ Path().apply {
+ addRect(
+ Rect(
+ left = 0f,
+ top = size.height - strokeWidth,
+ right = size.width,
+ bottom = size.height,
+ )
+ )
+ }
+ val clippedLine = linePath and textFieldShapePath
+
+ onDrawWithContent {
+ drawContent()
+ drawPath(
+ path = clippedLine,
+ brush = SolidColor(colorAnimatable!!.value),
+ )
+ }
+ }
+ )
}
/** Padding from text field top to label top, and from input field bottom to text field bottom */
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
index 6155142..17f68b4 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
@@ -50,11 +50,9 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.takeOrElse
-import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.Dp
@@ -295,32 +293,16 @@
focusedIndicatorLineThickness: Dp = FocusedIndicatorThickness,
unfocusedIndicatorLineThickness: Dp = UnfocusedIndicatorThickness
) =
- composed(
- inspectorInfo =
- debugInspectorInfo {
- name = "indicatorLine"
- properties["enabled"] = enabled
- properties["isError"] = isError
- properties["interactionSource"] = interactionSource
- properties["colors"] = colors
- properties["focusedIndicatorLineThickness"] = focusedIndicatorLineThickness
- properties["unfocusedIndicatorLineThickness"] = unfocusedIndicatorLineThickness
- }
- ) {
- val resolvedColors = colors ?: colors()
- val shape = textFieldShape ?: shape
- val focused = interactionSource.collectIsFocusedAsState().value
- val stroke =
- animateBorderStrokeAsState(
- enabled,
- isError,
- focused,
- resolvedColors,
- focusedIndicatorLineThickness,
- unfocusedIndicatorLineThickness
- )
- Modifier.drawIndicatorLine(stroke, shape)
- }
+ this then
+ IndicatorLineElement(
+ enabled = enabled,
+ isError = isError,
+ interactionSource = interactionSource,
+ colors = colors,
+ textFieldShape = textFieldShape,
+ focusedIndicatorLineThickness = focusedIndicatorLineThickness,
+ unfocusedIndicatorLineThickness = unfocusedIndicatorLineThickness,
+ )
/**
* A decoration box used to create custom text fields based on <a
@@ -492,7 +474,9 @@
* Creates a [TextFieldColors] that represents the default input text, container, and content
* colors (including label, placeholder, icons, etc.) used in a [TextField].
*/
- @Composable fun colors() = MaterialTheme.colorScheme.defaultTextFieldColors
+ @Composable
+ fun colors() =
+ MaterialTheme.colorScheme.defaultTextFieldColors(LocalTextSelectionColors.current)
/**
* Creates a [TextFieldColors] that represents the default input text, container, and content
@@ -594,145 +578,140 @@
disabledSuffixColor: Color = Color.Unspecified,
errorSuffixColor: Color = Color.Unspecified,
): TextFieldColors =
- MaterialTheme.colorScheme.defaultTextFieldColors.copy(
- focusedTextColor = focusedTextColor,
- unfocusedTextColor = unfocusedTextColor,
- disabledTextColor = disabledTextColor,
- errorTextColor = errorTextColor,
- focusedContainerColor = focusedContainerColor,
- unfocusedContainerColor = unfocusedContainerColor,
- disabledContainerColor = disabledContainerColor,
- errorContainerColor = errorContainerColor,
- cursorColor = cursorColor,
- errorCursorColor = errorCursorColor,
- textSelectionColors = selectionColors,
- focusedIndicatorColor = focusedIndicatorColor,
- unfocusedIndicatorColor = unfocusedIndicatorColor,
- disabledIndicatorColor = disabledIndicatorColor,
- errorIndicatorColor = errorIndicatorColor,
- focusedLeadingIconColor = focusedLeadingIconColor,
- unfocusedLeadingIconColor = unfocusedLeadingIconColor,
- disabledLeadingIconColor = disabledLeadingIconColor,
- errorLeadingIconColor = errorLeadingIconColor,
- focusedTrailingIconColor = focusedTrailingIconColor,
- unfocusedTrailingIconColor = unfocusedTrailingIconColor,
- disabledTrailingIconColor = disabledTrailingIconColor,
- errorTrailingIconColor = errorTrailingIconColor,
- focusedLabelColor = focusedLabelColor,
- unfocusedLabelColor = unfocusedLabelColor,
- disabledLabelColor = disabledLabelColor,
- errorLabelColor = errorLabelColor,
- focusedPlaceholderColor = focusedPlaceholderColor,
- unfocusedPlaceholderColor = unfocusedPlaceholderColor,
- disabledPlaceholderColor = disabledPlaceholderColor,
- errorPlaceholderColor = errorPlaceholderColor,
- focusedSupportingTextColor = focusedSupportingTextColor,
- unfocusedSupportingTextColor = unfocusedSupportingTextColor,
- disabledSupportingTextColor = disabledSupportingTextColor,
- errorSupportingTextColor = errorSupportingTextColor,
- focusedPrefixColor = focusedPrefixColor,
- unfocusedPrefixColor = unfocusedPrefixColor,
- disabledPrefixColor = disabledPrefixColor,
- errorPrefixColor = errorPrefixColor,
- focusedSuffixColor = focusedSuffixColor,
- unfocusedSuffixColor = unfocusedSuffixColor,
- disabledSuffixColor = disabledSuffixColor,
- errorSuffixColor = errorSuffixColor,
- )
+ MaterialTheme.colorScheme
+ .defaultTextFieldColors(LocalTextSelectionColors.current)
+ .copy(
+ focusedTextColor = focusedTextColor,
+ unfocusedTextColor = unfocusedTextColor,
+ disabledTextColor = disabledTextColor,
+ errorTextColor = errorTextColor,
+ focusedContainerColor = focusedContainerColor,
+ unfocusedContainerColor = unfocusedContainerColor,
+ disabledContainerColor = disabledContainerColor,
+ errorContainerColor = errorContainerColor,
+ cursorColor = cursorColor,
+ errorCursorColor = errorCursorColor,
+ textSelectionColors = selectionColors,
+ focusedIndicatorColor = focusedIndicatorColor,
+ unfocusedIndicatorColor = unfocusedIndicatorColor,
+ disabledIndicatorColor = disabledIndicatorColor,
+ errorIndicatorColor = errorIndicatorColor,
+ focusedLeadingIconColor = focusedLeadingIconColor,
+ unfocusedLeadingIconColor = unfocusedLeadingIconColor,
+ disabledLeadingIconColor = disabledLeadingIconColor,
+ errorLeadingIconColor = errorLeadingIconColor,
+ focusedTrailingIconColor = focusedTrailingIconColor,
+ unfocusedTrailingIconColor = unfocusedTrailingIconColor,
+ disabledTrailingIconColor = disabledTrailingIconColor,
+ errorTrailingIconColor = errorTrailingIconColor,
+ focusedLabelColor = focusedLabelColor,
+ unfocusedLabelColor = unfocusedLabelColor,
+ disabledLabelColor = disabledLabelColor,
+ errorLabelColor = errorLabelColor,
+ focusedPlaceholderColor = focusedPlaceholderColor,
+ unfocusedPlaceholderColor = unfocusedPlaceholderColor,
+ disabledPlaceholderColor = disabledPlaceholderColor,
+ errorPlaceholderColor = errorPlaceholderColor,
+ focusedSupportingTextColor = focusedSupportingTextColor,
+ unfocusedSupportingTextColor = unfocusedSupportingTextColor,
+ disabledSupportingTextColor = disabledSupportingTextColor,
+ errorSupportingTextColor = errorSupportingTextColor,
+ focusedPrefixColor = focusedPrefixColor,
+ unfocusedPrefixColor = unfocusedPrefixColor,
+ disabledPrefixColor = disabledPrefixColor,
+ errorPrefixColor = errorPrefixColor,
+ focusedSuffixColor = focusedSuffixColor,
+ unfocusedSuffixColor = unfocusedSuffixColor,
+ disabledSuffixColor = disabledSuffixColor,
+ errorSuffixColor = errorSuffixColor,
+ )
- internal val ColorScheme.defaultTextFieldColors: TextFieldColors
- @Composable
- get() {
- return defaultTextFieldColorsCached?.let { cachedColors ->
- val localTextSelectionColors = LocalTextSelectionColors.current
- if (cachedColors.textSelectionColors == localTextSelectionColors) {
- cachedColors
- } else {
- cachedColors.copy(textSelectionColors = localTextSelectionColors).also {
- defaultTextFieldColorsCached = it
- }
+ internal fun ColorScheme.defaultTextFieldColors(
+ localTextSelectionColors: TextSelectionColors
+ ): TextFieldColors {
+ return defaultTextFieldColorsCached?.let { cachedColors ->
+ if (cachedColors.textSelectionColors == localTextSelectionColors) {
+ cachedColors
+ } else {
+ cachedColors.copy(textSelectionColors = localTextSelectionColors).also {
+ defaultTextFieldColorsCached = it
}
}
- ?: TextFieldColors(
- focusedTextColor = fromToken(FilledTextFieldTokens.FocusInputColor),
- unfocusedTextColor = fromToken(FilledTextFieldTokens.InputColor),
- disabledTextColor =
- fromToken(FilledTextFieldTokens.DisabledInputColor)
- .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity),
- errorTextColor = fromToken(FilledTextFieldTokens.ErrorInputColor),
- focusedContainerColor = fromToken(FilledTextFieldTokens.ContainerColor),
- unfocusedContainerColor = fromToken(FilledTextFieldTokens.ContainerColor),
- disabledContainerColor = fromToken(FilledTextFieldTokens.ContainerColor),
- errorContainerColor = fromToken(FilledTextFieldTokens.ContainerColor),
- cursorColor = fromToken(FilledTextFieldTokens.CaretColor),
- errorCursorColor = fromToken(FilledTextFieldTokens.ErrorFocusCaretColor),
- textSelectionColors = LocalTextSelectionColors.current,
- focusedIndicatorColor =
- fromToken(FilledTextFieldTokens.FocusActiveIndicatorColor),
- unfocusedIndicatorColor =
- fromToken(FilledTextFieldTokens.ActiveIndicatorColor),
- disabledIndicatorColor =
- fromToken(FilledTextFieldTokens.DisabledActiveIndicatorColor)
- .copy(alpha = FilledTextFieldTokens.DisabledActiveIndicatorOpacity),
- errorIndicatorColor =
- fromToken(FilledTextFieldTokens.ErrorActiveIndicatorColor),
- focusedLeadingIconColor =
- fromToken(FilledTextFieldTokens.FocusLeadingIconColor),
- unfocusedLeadingIconColor =
- fromToken(FilledTextFieldTokens.LeadingIconColor),
- disabledLeadingIconColor =
- fromToken(FilledTextFieldTokens.DisabledLeadingIconColor)
- .copy(alpha = FilledTextFieldTokens.DisabledLeadingIconOpacity),
- errorLeadingIconColor =
- fromToken(FilledTextFieldTokens.ErrorLeadingIconColor),
- focusedTrailingIconColor =
- fromToken(FilledTextFieldTokens.FocusTrailingIconColor),
- unfocusedTrailingIconColor =
- fromToken(FilledTextFieldTokens.TrailingIconColor),
- disabledTrailingIconColor =
- fromToken(FilledTextFieldTokens.DisabledTrailingIconColor)
- .copy(alpha = FilledTextFieldTokens.DisabledTrailingIconOpacity),
- errorTrailingIconColor =
- fromToken(FilledTextFieldTokens.ErrorTrailingIconColor),
- focusedLabelColor = fromToken(FilledTextFieldTokens.FocusLabelColor),
- unfocusedLabelColor = fromToken(FilledTextFieldTokens.LabelColor),
- disabledLabelColor =
- fromToken(FilledTextFieldTokens.DisabledLabelColor)
- .copy(alpha = FilledTextFieldTokens.DisabledLabelOpacity),
- errorLabelColor = fromToken(FilledTextFieldTokens.ErrorLabelColor),
- focusedPlaceholderColor =
- fromToken(FilledTextFieldTokens.InputPlaceholderColor),
- unfocusedPlaceholderColor =
- fromToken(FilledTextFieldTokens.InputPlaceholderColor),
- disabledPlaceholderColor =
- fromToken(FilledTextFieldTokens.DisabledInputColor)
- .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity),
- errorPlaceholderColor =
- fromToken(FilledTextFieldTokens.InputPlaceholderColor),
- focusedSupportingTextColor =
- fromToken(FilledTextFieldTokens.FocusSupportingColor),
- unfocusedSupportingTextColor =
- fromToken(FilledTextFieldTokens.SupportingColor),
- disabledSupportingTextColor =
- fromToken(FilledTextFieldTokens.DisabledSupportingColor)
- .copy(alpha = FilledTextFieldTokens.DisabledSupportingOpacity),
- errorSupportingTextColor =
- fromToken(FilledTextFieldTokens.ErrorSupportingColor),
- focusedPrefixColor = fromToken(FilledTextFieldTokens.InputPrefixColor),
- unfocusedPrefixColor = fromToken(FilledTextFieldTokens.InputPrefixColor),
- disabledPrefixColor =
- fromToken(FilledTextFieldTokens.InputPrefixColor)
- .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity),
- errorPrefixColor = fromToken(FilledTextFieldTokens.InputPrefixColor),
- focusedSuffixColor = fromToken(FilledTextFieldTokens.InputSuffixColor),
- unfocusedSuffixColor = fromToken(FilledTextFieldTokens.InputSuffixColor),
- disabledSuffixColor =
- fromToken(FilledTextFieldTokens.InputSuffixColor)
- .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity),
- errorSuffixColor = fromToken(FilledTextFieldTokens.InputSuffixColor),
- )
- .also { defaultTextFieldColorsCached = it }
}
+ ?: TextFieldColors(
+ focusedTextColor = fromToken(FilledTextFieldTokens.FocusInputColor),
+ unfocusedTextColor = fromToken(FilledTextFieldTokens.InputColor),
+ disabledTextColor =
+ fromToken(FilledTextFieldTokens.DisabledInputColor)
+ .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity),
+ errorTextColor = fromToken(FilledTextFieldTokens.ErrorInputColor),
+ focusedContainerColor = fromToken(FilledTextFieldTokens.ContainerColor),
+ unfocusedContainerColor = fromToken(FilledTextFieldTokens.ContainerColor),
+ disabledContainerColor = fromToken(FilledTextFieldTokens.ContainerColor),
+ errorContainerColor = fromToken(FilledTextFieldTokens.ContainerColor),
+ cursorColor = fromToken(FilledTextFieldTokens.CaretColor),
+ errorCursorColor = fromToken(FilledTextFieldTokens.ErrorFocusCaretColor),
+ textSelectionColors = localTextSelectionColors,
+ focusedIndicatorColor =
+ fromToken(FilledTextFieldTokens.FocusActiveIndicatorColor),
+ unfocusedIndicatorColor = fromToken(FilledTextFieldTokens.ActiveIndicatorColor),
+ disabledIndicatorColor =
+ fromToken(FilledTextFieldTokens.DisabledActiveIndicatorColor)
+ .copy(alpha = FilledTextFieldTokens.DisabledActiveIndicatorOpacity),
+ errorIndicatorColor =
+ fromToken(FilledTextFieldTokens.ErrorActiveIndicatorColor),
+ focusedLeadingIconColor =
+ fromToken(FilledTextFieldTokens.FocusLeadingIconColor),
+ unfocusedLeadingIconColor = fromToken(FilledTextFieldTokens.LeadingIconColor),
+ disabledLeadingIconColor =
+ fromToken(FilledTextFieldTokens.DisabledLeadingIconColor)
+ .copy(alpha = FilledTextFieldTokens.DisabledLeadingIconOpacity),
+ errorLeadingIconColor = fromToken(FilledTextFieldTokens.ErrorLeadingIconColor),
+ focusedTrailingIconColor =
+ fromToken(FilledTextFieldTokens.FocusTrailingIconColor),
+ unfocusedTrailingIconColor = fromToken(FilledTextFieldTokens.TrailingIconColor),
+ disabledTrailingIconColor =
+ fromToken(FilledTextFieldTokens.DisabledTrailingIconColor)
+ .copy(alpha = FilledTextFieldTokens.DisabledTrailingIconOpacity),
+ errorTrailingIconColor =
+ fromToken(FilledTextFieldTokens.ErrorTrailingIconColor),
+ focusedLabelColor = fromToken(FilledTextFieldTokens.FocusLabelColor),
+ unfocusedLabelColor = fromToken(FilledTextFieldTokens.LabelColor),
+ disabledLabelColor =
+ fromToken(FilledTextFieldTokens.DisabledLabelColor)
+ .copy(alpha = FilledTextFieldTokens.DisabledLabelOpacity),
+ errorLabelColor = fromToken(FilledTextFieldTokens.ErrorLabelColor),
+ focusedPlaceholderColor =
+ fromToken(FilledTextFieldTokens.InputPlaceholderColor),
+ unfocusedPlaceholderColor =
+ fromToken(FilledTextFieldTokens.InputPlaceholderColor),
+ disabledPlaceholderColor =
+ fromToken(FilledTextFieldTokens.DisabledInputColor)
+ .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity),
+ errorPlaceholderColor = fromToken(FilledTextFieldTokens.InputPlaceholderColor),
+ focusedSupportingTextColor =
+ fromToken(FilledTextFieldTokens.FocusSupportingColor),
+ unfocusedSupportingTextColor = fromToken(FilledTextFieldTokens.SupportingColor),
+ disabledSupportingTextColor =
+ fromToken(FilledTextFieldTokens.DisabledSupportingColor)
+ .copy(alpha = FilledTextFieldTokens.DisabledSupportingOpacity),
+ errorSupportingTextColor =
+ fromToken(FilledTextFieldTokens.ErrorSupportingColor),
+ focusedPrefixColor = fromToken(FilledTextFieldTokens.InputPrefixColor),
+ unfocusedPrefixColor = fromToken(FilledTextFieldTokens.InputPrefixColor),
+ disabledPrefixColor =
+ fromToken(FilledTextFieldTokens.InputPrefixColor)
+ .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity),
+ errorPrefixColor = fromToken(FilledTextFieldTokens.InputPrefixColor),
+ focusedSuffixColor = fromToken(FilledTextFieldTokens.InputSuffixColor),
+ unfocusedSuffixColor = fromToken(FilledTextFieldTokens.InputSuffixColor),
+ disabledSuffixColor =
+ fromToken(FilledTextFieldTokens.InputSuffixColor)
+ .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity),
+ errorSuffixColor = fromToken(FilledTextFieldTokens.InputSuffixColor),
+ )
+ .also { defaultTextFieldColorsCached = it }
+ }
@Deprecated(
level = DeprecationLevel.HIDDEN,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/FloatProducer.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/FloatProducer.kt
new file mode 100644
index 0000000..483bf8a
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/FloatProducer.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.compose.material3.internal
+
+/**
+ * Alternative to `() -> Float` but avoids boxing.
+ *
+ * !!! Do not use in public APIs !!!
+ */
+internal fun interface FloatProducer {
+ /** Returns the Float. */
+ operator fun invoke(): Float
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
index 7bfd347..3c48ab1 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
@@ -36,6 +36,7 @@
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.LoadingIndicatorDefaults
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.internal.FloatProducer
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator
import androidx.compose.material3.tokens.ElevationTokens
import androidx.compose.material3.tokens.MotionSchemeKeyTokens
@@ -705,7 +706,7 @@
/** The default pull indicator for [PullToRefreshBox] */
@Composable
private fun CircularArrowProgressIndicator(
- progress: () -> Float,
+ progress: FloatProducer,
color: Color,
) {
val path = remember { Path().apply { fillType = PathFillType.EvenOdd } }
diff --git a/compose/runtime/runtime-saveable/samples/build.gradle b/compose/runtime/runtime-saveable/samples/build.gradle
index 3bcc90b..cda4890 100644
--- a/compose/runtime/runtime-saveable/samples/build.gradle
+++ b/compose/runtime/runtime-saveable/samples/build.gradle
@@ -36,12 +36,12 @@
implementation(libs.kotlinStdlib)
- compileOnly project(":annotation:annotation-sampled")
+ compileOnly(project(":annotation:annotation-sampled"))
implementation "androidx.compose.foundation:foundation:1.2.1"
implementation "androidx.compose.material:material:1.2.1"
- implementation project(":compose:runtime:runtime")
- implementation project(":compose:runtime:runtime-saveable")
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:runtime:runtime-saveable"))
}
androidx {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt
index 3d420c5..9088371f 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt
@@ -258,7 +258,6 @@
}
internal class RecordingApplier<N>(root: N) : Applier<N> {
- private val stack = mutableObjectListOf<N>()
private val operations = mutableIntListOf()
private val instances = mutableObjectListOf<Any?>()
@@ -267,13 +266,10 @@
override fun down(node: N) {
operations.add(DOWN)
instances.add(node)
- stack.add(current)
- current = node
}
override fun up() {
operations.add(UP)
- current = stack.removeAt(stack.size - 1)
}
override fun remove(index: Int, count: Int) {
diff --git a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/reflect/ComposableMethod.jvm.kt b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/reflect/ComposableMethod.jvm.kt
index b34353b..96a6404 100644
--- a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/reflect/ComposableMethod.jvm.kt
+++ b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/reflect/ComposableMethod.jvm.kt
@@ -100,8 +100,7 @@
/** Returns method parameters excluding the utility Compose-specific parameters. */
val parameters: Array<Parameter>
- @Suppress("ClassVerificationFailure", "NewApi")
- get() = method.parameters.copyOfRange(0, composableInfo.realParamsCount)
+ @Suppress("NewApi") get() = method.parameters.copyOfRange(0, composableInfo.realParamsCount)
/** Returns method parameters types excluding the utility Compose-specific parameters. */
val parameterTypes: Array<Class<*>>
diff --git a/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/reflect/ComposableMethodTest.kt b/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/reflect/ComposableMethodTest.kt
index 2eba387..af980a6 100644
--- a/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/reflect/ComposableMethodTest.kt
+++ b/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/reflect/ComposableMethodTest.kt
@@ -362,7 +362,7 @@
assertEquals(0, composableMethod.asComposableMethod()!!.parameterCount)
}
- @Suppress("ClassVerificationFailure", "NewApi")
+ @Suppress("NewApi")
@Throws(NoSuchMethodException::class)
@Test
fun test_realParameters_returns_correct_parameters() {
diff --git a/compose/ui/ui-graphics/benchmark/build.gradle b/compose/ui/ui-graphics/benchmark/build.gradle
index 8ad2df7..6f9f2ec 100644
--- a/compose/ui/ui-graphics/benchmark/build.gradle
+++ b/compose/ui/ui-graphics/benchmark/build.gradle
@@ -23,13 +23,13 @@
}
dependencies {
- implementation project(":compose:foundation:foundation")
- implementation project(":compose:runtime:runtime")
- implementation project(":compose:benchmark-utils")
- implementation project(":compose:ui:ui")
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:benchmark-utils"))
+ implementation(project(":compose:ui:ui"))
implementation(libs.kotlinStdlib)
- androidTestImplementation project(":benchmark:benchmark-junit4")
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
androidTestImplementation(libs.testRules)
}
diff --git a/compose/ui/ui-graphics/benchmark/test/build.gradle b/compose/ui/ui-graphics/benchmark/test/build.gradle
index 517f004..bbaf11f 100644
--- a/compose/ui/ui-graphics/benchmark/test/build.gradle
+++ b/compose/ui/ui-graphics/benchmark/test/build.gradle
@@ -23,12 +23,12 @@
dependencies {
- androidTestImplementation project(":compose:foundation:foundation")
- androidTestImplementation project(":compose:runtime:runtime")
- androidTestImplementation project(":compose:test-utils")
- androidTestImplementation project(":compose:ui:ui")
- androidTestImplementation project(":compose:ui:ui-graphics")
- androidTestImplementation project(":compose:ui:ui-graphics:ui-graphics-benchmark")
+ androidTestImplementation(project(":compose:foundation:foundation"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:test-utils"))
+ androidTestImplementation(project(":compose:ui:ui"))
+ androidTestImplementation(project(":compose:ui:ui-graphics"))
+ androidTestImplementation(project(":compose:ui:ui-graphics:ui-graphics-benchmark"))
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt
index 52a10ab..789e0b2 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt
@@ -166,6 +166,12 @@
assertThat(DEBUG).isFalse()
}
+ @Test // regression test for b/383639244
+ fun noViews() {
+ val builder = LayoutInspectorTree()
+ builder.convert(emptyList())
+ }
+
@Test
fun buildTree() {
val slotTableRecord = CompositionDataRecord.create()
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
index 4b7e817..ed49a53 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
@@ -18,6 +18,7 @@
import android.view.View
import androidx.collection.LongObjectMap
+import androidx.collection.emptyLongObjectMap
import androidx.collection.mutableIntObjectMapOf
import androidx.collection.mutableLongObjectMapOf
import androidx.compose.runtime.tooling.CompositionData
@@ -54,6 +55,9 @@
/** Converts the [CompositionData] held by [views] into a list of root nodes per view id. */
fun convert(views: List<View>): LongObjectMap<MutableList<InspectorNode>> {
clear()
+ if (views.isEmpty()) {
+ return emptyLongObjectMap()
+ }
val defaultView = views.first()
builderData.setDensity(defaultView)
val defaultViewId = defaultView.uniqueDrawingId
diff --git a/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/DeviceConfigurationOverrideSamples.kt b/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/DeviceConfigurationOverrideSamples.kt
index d345fcb..c260944 100644
--- a/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/DeviceConfigurationOverrideSamples.kt
+++ b/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/DeviceConfigurationOverrideSamples.kt
@@ -105,7 +105,6 @@
}
}
-@Suppress("ClassVerificationFailure") // Only used in sample
@Sampled
@Composable
fun DeviceConfigurationOverrideRoundScreenSample() {
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt
index 5d97f9c..fac5641 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt
@@ -29,8 +29,6 @@
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
@@ -267,42 +265,6 @@
rule.runOnIdle { assertEquals(calculateExpectedIntOffset(alignment), targetOffset.value) }
}
- @Test
- fun onPlacedCalledOnReuseInsideLazyColumn() {
- lateinit var density: Density
- val items = 200
- val visibleItems = 2
- val itemSize = 50.dp
- val invocations = arrayOf(0, 0)
-
- // It's important to share lambda across all iterations
- val placedCallback0: (LayoutCoordinates) -> Unit = { invocations[0] = invocations[0] + 1 }
- val placedCallback1: (LayoutCoordinates) -> Unit = { invocations[1] = invocations[1] + 1 }
- val scrollState = LazyListState()
- rule.setContent {
- density = LocalDensity.current
- LazyColumn(Modifier.size(itemSize, itemSize * visibleItems), scrollState) {
- items(items) {
- Box(Modifier.size(itemSize).onPlaced(placedCallback0)) {
- Box(Modifier.size(itemSize).onPlaced(placedCallback1))
- }
- }
- }
- }
-
- var expectedInvocations = visibleItems
- val delta = with(density) { (itemSize * visibleItems).toPx() }
- repeat(items / visibleItems) {
- rule.runOnIdle {
- assertThat(invocations[0]).isAtLeast(expectedInvocations)
- assertThat(invocations[1]).isAtLeast(expectedInvocations)
-
- scrollState.dispatchRawDelta(delta)
- expectedInvocations += visibleItems
- }
- }
- }
-
private fun Modifier.animatePlacement(
targetOffset: MutableState<Offset>,
alignment: () -> Alignment
diff --git a/compose/ui/ui-text/benchmark/build.gradle b/compose/ui/ui-text/benchmark/build.gradle
index ae3f95f..19f23c8 100644
--- a/compose/ui/ui-text/benchmark/build.gradle
+++ b/compose/ui/ui-text/benchmark/build.gradle
@@ -23,17 +23,17 @@
}
dependencies {
- implementation project(":benchmark:benchmark-junit4")
- implementation project(":compose:runtime:runtime")
- implementation project(":compose:ui:ui-test-junit4")
+ implementation(project(":benchmark:benchmark-junit4"))
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:ui:ui-test-junit4"))
implementation(libs.kotlinStdlib)
implementation(libs.kotlinReflect)
implementation(libs.testRules)
implementation(libs.junit)
- androidTestImplementation project(":compose:runtime:runtime")
- androidTestImplementation project(":compose:benchmark-utils")
- androidTestImplementation project(":compose:ui:ui")
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:benchmark-utils"))
+ androidTestImplementation(project(":compose:ui:ui"))
androidTestImplementation(libs.kotlinTestCommon)
androidTestImplementation(libs.truth)
}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/android/TextLayoutTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/android/TextLayoutTest.kt
index 9d860cb..e061576 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/android/TextLayoutTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/android/TextLayoutTest.kt
@@ -35,6 +35,7 @@
import org.junit.Test
import org.junit.runner.RunWith
+@OptIn(InternalPlatformTextApi::class)
@RunWith(AndroidJUnit4::class)
@SmallTest
class TextLayoutTest {
@@ -372,112 +373,23 @@
text = "aA",
fontSize = fontSize,
lineHeight = lineHeight,
- preserveMinimumHeight = false,
- trimFirstLineTop = false,
- trimLastLineBottom = false
+ preserveMinimumHeight = false
)
val defaultFontMetrics = createTextPaint(fontSize).fontMetricsInt
+ val expectedPadding = ((fontSize - lineHeight) / 2).toInt()
- assertThat(layout.topPadding).isEqualTo(0)
- assertThat(layout.bottomPadding).isEqualTo(0)
+ assertThat(layout.topPadding).isEqualTo(expectedPadding)
+ assertThat(layout.bottomPadding).isEqualTo(expectedPadding)
assertThat(layout.height).isEqualTo(fontSize.toInt())
assertThat(layout.getLineTop(0)).isEqualTo(0)
- assertThat(layout.getLineBottom(0)).isEqualTo(fontSize.toInt())
+ assertThat(layout.getLineBottom(0)).isEqualTo(layout.height)
assertThat(layout.getLineBaseline(0)).isEqualTo(-defaultFontMetrics.ascent.toFloat())
assertThat(layout.getLineForVertical(0)).isEqualTo(0)
assertThat(layout.getLineForVertical(layout.height)).isEqualTo(0)
}
@Test
- fun small_lineheight_allows_clip_when_trimFirstLine_single_line() {
- val fontSize = 120f
- val lineHeight = 60f
-
- val layout =
- TextLayoutWithSmallLineHeight(
- text = "aA",
- fontSize = fontSize,
- lineHeight = lineHeight,
- preserveMinimumHeight = false,
- trimFirstLineTop = true,
- trimLastLineBottom = false
- )
-
- val defaultFontMetrics = createTextPaint(fontSize).fontMetricsInt
- // We divide by 2 because the default topRatio is 0.5f in TextLayoutWithSmallLineHeight
- val diff = (fontSize - lineHeight) / 2
-
- assertThat(layout.topPadding).isEqualTo(0)
- assertThat(layout.bottomPadding).isEqualTo(0)
- assertThat(layout.height).isEqualTo((fontSize - diff).toInt())
- assertThat(layout.getLineTop(0)).isEqualTo(0)
- assertThat(layout.getLineBottom(0)).isEqualTo((fontSize - diff).toInt())
- assertThat(layout.getLineBaseline(0))
- .isEqualTo(-(defaultFontMetrics.descent - diff - lineHeight))
- assertThat(layout.getLineForVertical(0)).isEqualTo(0)
- assertThat(layout.getLineForVertical(layout.height)).isEqualTo(0)
- }
-
- @Test
- fun small_lineheight_allows_clip_when_trimLastLine_single_line() {
- val fontSize = 120f
- val lineHeight = 60f
-
- val layout =
- TextLayoutWithSmallLineHeight(
- text = "aA",
- fontSize = fontSize,
- lineHeight = lineHeight,
- preserveMinimumHeight = false,
- trimFirstLineTop = false,
- trimLastLineBottom = true
- )
-
- val defaultFontMetrics = createTextPaint(fontSize).fontMetricsInt
- // We divide by 2 because the default topRatio is 0.5f in TextLayoutWithSmallLineHeight
- val diff = (fontSize - lineHeight) / 2
-
- assertThat(layout.topPadding).isEqualTo(0)
- assertThat(layout.bottomPadding).isEqualTo(0)
- assertThat(layout.height).isEqualTo((fontSize - diff).toInt())
- assertThat(layout.getLineTop(0)).isEqualTo(0)
- assertThat(layout.getLineBottom(0)).isEqualTo((fontSize - diff).toInt())
- assertThat(layout.getLineBaseline(0)).isEqualTo(-defaultFontMetrics.ascent)
- assertThat(layout.getLineForVertical(0)).isEqualTo(0)
- assertThat(layout.getLineForVertical(layout.height)).isEqualTo(0)
- }
-
- @Test
- fun small_lineheight_allows_clip_when_trimFirstLine_trimLastLine_single_line() {
- val fontSize = 120f
- val lineHeight = 60f
-
- val layout =
- TextLayoutWithSmallLineHeight(
- text = "aA",
- fontSize = fontSize,
- lineHeight = lineHeight,
- preserveMinimumHeight = false,
- trimFirstLineTop = true,
- trimLastLineBottom = true
- )
-
- val defaultFontMetrics = createTextPaint(fontSize).fontMetricsInt
- // We divide by 2 because the default topRatio is 0.5f in TextLayoutWithSmallLineHeight
- val diff = (fontSize - lineHeight) / 2
-
- assertThat(layout.topPadding).isEqualTo(0)
- assertThat(layout.bottomPadding).isEqualTo(0)
- assertThat(layout.height).isEqualTo(lineHeight.toInt())
- assertThat(layout.getLineTop(0)).isEqualTo(0)
- assertThat(layout.getLineBottom(0)).isEqualTo(lineHeight.toInt())
- assertThat(layout.getLineBaseline(0)).isEqualTo(-(defaultFontMetrics.ascent + diff))
- assertThat(layout.getLineForVertical(0)).isEqualTo(0)
- assertThat(layout.getLineForVertical(layout.height)).isEqualTo(0)
- }
-
- @Test
fun small_lineheight_prevents_clip_multi_line() {
val fontSize = 120f
val lineHeight = 60f
@@ -486,16 +398,14 @@
text = "aA\naA\naA",
fontSize = fontSize,
lineHeight = lineHeight,
- preserveMinimumHeight = false,
- trimFirstLineTop = false,
- trimLastLineBottom = false
+ preserveMinimumHeight = false
)
val defaultFontMetrics = createTextPaint(fontSize).fontMetricsInt
val expectedPadding = ((fontSize - lineHeight) / 2).toInt()
- assertThat(layout.topPadding).isEqualTo(0)
- assertThat(layout.bottomPadding).isEqualTo(0)
+ assertThat(layout.topPadding).isEqualTo(expectedPadding)
+ assertThat(layout.bottomPadding).isEqualTo(expectedPadding)
assertThat(layout.height).isEqualTo((3 * lineHeight + 2 * expectedPadding).toInt())
assertThat(layout.getLineTop(0)).isEqualTo(0)
assertThat(layout.getLineBaseline(0)).isEqualTo(-defaultFontMetrics.ascent)
@@ -505,90 +415,6 @@
}
@Test
- fun small_lineheight_allows_clip_when_trimFirstLine_multi_line() {
- val fontSize = 120f
- val lineHeight = 60f
- val layout =
- TextLayoutWithSmallLineHeight(
- text = "aA\naA\naA",
- fontSize = fontSize,
- lineHeight = lineHeight,
- preserveMinimumHeight = false,
- trimFirstLineTop = true,
- trimLastLineBottom = false
- )
-
- val defaultFontMetrics = createTextPaint(fontSize).fontMetricsInt
- val expectedPadding = ((fontSize - lineHeight) / 2).toInt()
-
- assertThat(layout.topPadding).isEqualTo(0)
- assertThat(layout.bottomPadding).isEqualTo(0)
- assertThat(layout.height).isEqualTo((2 * lineHeight + fontSize - expectedPadding).toInt())
- assertThat(layout.getLineTop(0)).isEqualTo(0)
- assertThat(layout.getLineBaseline(0))
- .isEqualTo(-(defaultFontMetrics.ascent + expectedPadding))
- assertThat(layout.getLineForVertical(0)).isEqualTo(0)
- assertThat(layout.getLineBottom(2)).isEqualTo(layout.height)
- assertThat(layout.getLineBaseline(2)).isEqualTo(layout.height - defaultFontMetrics.descent)
- assertThat(layout.getLineForVertical(layout.height)).isEqualTo(2)
- }
-
- @Test
- fun small_lineheight_allows_clip_when_trimLastLine_multi_line() {
- val fontSize = 120f
- val lineHeight = 60f
- val layout =
- TextLayoutWithSmallLineHeight(
- text = "aA\naA\naA",
- fontSize = fontSize,
- lineHeight = lineHeight,
- preserveMinimumHeight = false,
- trimFirstLineTop = false,
- trimLastLineBottom = true
- )
-
- val defaultFontMetrics = createTextPaint(fontSize).fontMetricsInt
- val expectedPadding = ((fontSize - lineHeight) / 2).toInt()
-
- assertThat(layout.topPadding).isEqualTo(0)
- assertThat(layout.bottomPadding).isEqualTo(0)
- assertThat(layout.height).isEqualTo((2 * lineHeight + fontSize - expectedPadding).toInt())
- assertThat(layout.getLineTop(0)).isEqualTo(0)
- assertThat(layout.getLineBaseline(0)).isEqualTo(-defaultFontMetrics.ascent)
- assertThat(layout.getLineForVertical(0)).isEqualTo(0)
- assertThat(layout.getLineBottom(2)).isEqualTo(layout.height)
- assertThat(layout.getLineForVertical(layout.height)).isEqualTo(2)
- }
-
- @Test
- fun small_lineheight_allows_clip_when_trimFirstLine_trimLastLine_multi_line() {
- val fontSize = 120f
- val lineHeight = 60f
- val layout =
- TextLayoutWithSmallLineHeight(
- text = "aA\naA\naA",
- fontSize = fontSize,
- lineHeight = lineHeight,
- preserveMinimumHeight = false,
- trimFirstLineTop = true,
- trimLastLineBottom = true
- )
-
- val defaultFontMetrics = createTextPaint(fontSize).fontMetricsInt
- val expectedPadding = ((fontSize - lineHeight) / 2).toInt()
-
- assertThat(layout.topPadding).isEqualTo(0)
- assertThat(layout.bottomPadding).isEqualTo(0)
- assertThat(layout.height).isEqualTo((3 * lineHeight).toInt())
- assertThat(layout.getLineTop(0)).isEqualTo(0)
- assertThat(layout.getLineBaseline(0))
- .isEqualTo(-(defaultFontMetrics.ascent + expectedPadding))
- assertThat(layout.getLineForVertical(0)).isEqualTo(0)
- assertThat(layout.getLineBottom(2)).isEqualTo(layout.height)
- assertThat(layout.getLineForVertical(layout.height)).isEqualTo(2)
- }
-
- @Test
fun small_lineheight_prevents_clip_multi_line_preserve_linespacing() {
val fontSize = 120f
val specifyLineHeight = 60f
@@ -598,9 +424,7 @@
text = "aA\naA\naA",
fontSize = fontSize,
lineHeight = specifyLineHeight,
- preserveMinimumHeight = true,
- trimFirstLineTop = false,
- trimLastLineBottom = false
+ preserveMinimumHeight = true
)
val defaultFontMetrics = createTextPaint(fontSize).fontMetricsInt
val expectedPadding = ((fontSize - systemPreferredLineHeight) / 2).toInt()
@@ -620,9 +444,7 @@
text: CharSequence,
fontSize: Float,
lineHeight: Float,
- preserveMinimumHeight: Boolean,
- trimFirstLineTop: Boolean,
- trimLastLineBottom: Boolean,
+ preserveMinimumHeight: Boolean
): TextLayout {
val textPaint = createTextPaint(fontSize)
val spannable = SpannableString(text)
@@ -631,8 +453,8 @@
lineHeight = lineHeight,
startIndex = 0,
endIndex = text.length,
- trimFirstLineTop = trimFirstLineTop,
- trimLastLineBottom = trimLastLineBottom,
+ trimFirstLineTop = false,
+ trimLastLineBottom = false,
topRatio = 0.5f,
preserveMinimumHeight = preserveMinimumHeight,
),
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/android/style/LineHeightStyleSpanTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/android/style/LineHeightStyleSpanTest.kt
index 84a3a57..5a330ef 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/android/style/LineHeightStyleSpanTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/android/style/LineHeightStyleSpanTest.kt
@@ -31,6 +31,7 @@
private const val MultiLineStartIndex = 0
private const val MultiLineEndIndex = 3
+@OptIn(InternalPlatformTextApi::class)
@RunWith(AndroidJUnit4::class)
@SmallTest
class LineHeightStyleSpanTest {
@@ -83,12 +84,12 @@
@Test
fun singleLine_topRatio_0_trimFirstLineTop_false_trimLastLineBottom_false_preserve_no() {
- singleLine_topRatio_0_trimFirstLineTop_false_trimLastLineBottom_false(false)
+ negative_line_height_does_not_chage_the_values(false)
}
@Test
fun singleLine_topRatio_0_trimFirstLineTop_false_trimLastLineBottom_false_preserve_yes() {
- singleLine_topRatio_0_trimFirstLineTop_false_trimLastLineBottom_false(true)
+ negative_line_height_does_not_chage_the_values(true)
}
private fun singleLine_topRatio_0_trimFirstLineTop_false_trimLastLineBottom_true(
@@ -1141,6 +1142,154 @@
multiLine_topRatio_proportional_trimFirstLineTop_true_trimLastLineBottom_true(true)
}
+ /* first ascent & last descent diff */
+
+ private fun singleLine_with_firstLineTop_and_lastLineBottom_topRatio_50_larger_line_height(
+ preserve: Boolean
+ ) {
+ val fontMetrics = createFontMetrics()
+
+ val span =
+ createSingleLineSpan(
+ topRatio = 0.5f,
+ trimFirstLineTop = false,
+ trimLastLineBottom = false,
+ newLineHeight = fontMetrics.doubleLineHeight(),
+ preserveMinimumHeight = preserve,
+ )
+
+ span.runFirstLine(fontMetrics)
+
+ val halfLeading = fontMetrics.lineHeight() / 2
+ assertThat(span.firstAscentDiff).isEqualTo(halfLeading)
+ assertThat(span.lastDescentDiff).isEqualTo(halfLeading)
+ }
+
+ @Test
+ fun singleLine_with_firstLineTop_and_lastLineBottom_topRatio_50_larger_line_height_preserve_no() {
+ singleLine_with_firstLineTop_and_lastLineBottom_topRatio_50_larger_line_height(false)
+ }
+
+ @Test
+ fun singleLine_with_firstLineTop_and_lastLineBottom_topRatio_50_larger_line_height_preserve_yes() {
+ singleLine_with_firstLineTop_and_lastLineBottom_topRatio_50_larger_line_height(true)
+ }
+
+ private fun multiLine_with_firstLineTop_and_lastLineBottom_topRatio_50_larger_line_height(
+ preserve: Boolean
+ ) {
+ val fontMetrics = createFontMetrics()
+
+ val span =
+ createMultiLineSpan(
+ topRatio = 0.5f,
+ trimFirstLineTop = false,
+ trimLastLineBottom = false,
+ fontMetrics = fontMetrics,
+ preserveMinimumHeight = preserve,
+ )
+
+ span.runFirstLine(fontMetrics)
+ span.runSecondLine(fontMetrics)
+ span.runLastLine(fontMetrics)
+
+ val halfLeading = fontMetrics.lineHeight() / 2
+ assertThat(span.firstAscentDiff).isEqualTo(halfLeading)
+ assertThat(span.lastDescentDiff).isEqualTo(halfLeading)
+ }
+
+ @Test
+ fun multiLine_with_firstLineTop_and_lastLineBottom_topRatio_50_larger_line_height_preserve_no() {
+ multiLine_with_firstLineTop_and_lastLineBottom_topRatio_50_larger_line_height(false)
+ }
+
+ @Test
+ fun multiLine_with_firstLineTop_and_lastLineBottom_topRatio_50_larger_line_height_preserve_yes() {
+ multiLine_with_firstLineTop_and_lastLineBottom_topRatio_50_larger_line_height(true)
+ }
+
+ @Test
+ fun singleLine_with_firstLineTop_and_lastLineBottom_topRatio_50_smaller_line_height_preserve_no() {
+ val fontMetrics = createFontMetrics()
+
+ val span =
+ createSingleLineSpan(
+ topRatio = 0.5f,
+ trimFirstLineTop = false,
+ trimLastLineBottom = false,
+ newLineHeight = fontMetrics.lineHeight() / 2,
+ preserveMinimumHeight = false,
+ )
+
+ span.runFirstLine(fontMetrics)
+
+ val halfLeading = fontMetrics.lineHeight() / -4
+ assertThat(span.firstAscentDiff).isEqualTo(halfLeading)
+ assertThat(span.lastDescentDiff).isEqualTo(halfLeading)
+ }
+
+ @Test
+ fun singleLine_with_firstLineTop_and_lastLineBottom_topRatio_50_smaller_line_height_preserve_yes() {
+ val fontMetrics = createFontMetrics()
+
+ val span =
+ createSingleLineSpan(
+ topRatio = 0.5f,
+ trimFirstLineTop = false,
+ trimLastLineBottom = false,
+ newLineHeight = fontMetrics.lineHeight() / 2,
+ preserveMinimumHeight = true,
+ )
+
+ span.runFirstLine(fontMetrics)
+
+ assertThat(span.firstAscentDiff).isEqualTo(0)
+ assertThat(span.lastDescentDiff).isEqualTo(0)
+ }
+
+ @Test
+ fun multiLine_with_firstLineTop_and_lastLineBottom_topRatio_50_smaller_line_height_preserve_no() {
+ val fontMetrics = createFontMetrics()
+
+ val span =
+ createMultiLineSpan(
+ topRatio = 0.5f,
+ trimFirstLineTop = false,
+ trimLastLineBottom = false,
+ newLineHeight = fontMetrics.lineHeight() / 2,
+ preserveMinimumHeight = false,
+ )
+
+ span.runFirstLine(fontMetrics)
+ span.runSecondLine(fontMetrics)
+ span.runLastLine(fontMetrics)
+
+ val halfLeading = fontMetrics.lineHeight() / -4
+ assertThat(span.firstAscentDiff).isEqualTo(halfLeading)
+ assertThat(span.lastDescentDiff).isEqualTo(halfLeading)
+ }
+
+ @Test
+ fun multiLine_with_firstLineTop_and_lastLineBottom_topRatio_50_smaller_line_height_preserve_yes() {
+ val fontMetrics = createFontMetrics()
+
+ val span =
+ createMultiLineSpan(
+ topRatio = 0.5f,
+ trimFirstLineTop = false,
+ trimLastLineBottom = false,
+ newLineHeight = fontMetrics.lineHeight() / 2,
+ preserveMinimumHeight = true,
+ )
+
+ span.runFirstLine(fontMetrics)
+ span.runSecondLine(fontMetrics)
+ span.runLastLine(fontMetrics)
+
+ assertThat(span.firstAscentDiff).isEqualTo(0)
+ assertThat(span.lastDescentDiff).isEqualTo(0)
+ }
+
private fun proportionalDescentDiff(fontMetrics: FontMetricsInt): Int {
val ascent = abs(fontMetrics.ascent.toFloat())
val ascentRatio = ascent / fontMetrics.lineHeight()
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt
index cd7480f..5f5fd9a 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt
@@ -73,6 +73,7 @@
import androidx.compose.ui.text.android.style.getEllipsizedLeftPadding
import androidx.compose.ui.text.android.style.getEllipsizedRightPadding
import androidx.compose.ui.text.internal.requirePrecondition
+import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
@@ -326,29 +327,12 @@
layout.getLineEnd(lastLine) != charSequence.length
}
+ val verticalPaddings = getVerticalPaddings()
+
lineHeightSpans = getLineHeightSpans()
-
- // Even though it is an array of spans, we know that LineHeightStyle is part of
- // ParagraphStyle and there can only be one ParagraphStyle per text layout. So there should
- // also be only one LineHeightStyleSpan in this array. Please check getLastLineMetrics that
- // uses a similar approach.
- val shouldForceTrimTop =
- lineHeightSpans?.firstOrNull()?.let { it.trimFirstLineTop && !it.preserveMinimumHeight }
- ?: false
-
- val shouldForceTrimBottom =
- lineHeightSpans?.firstOrNull()?.let {
- it.trimLastLineBottom && !it.preserveMinimumHeight
- } ?: false
-
- if (shouldForceTrimTop && shouldForceTrimBottom) {
- topPadding = 0
- bottomPadding = 0
- } else {
- val verticalPaddings = getVerticalPaddings()
- topPadding = if (shouldForceTrimTop) 0 else verticalPaddings.topPadding
- bottomPadding = if (shouldForceTrimBottom) 0 else verticalPaddings.bottomPadding
- }
+ val lineHeightPaddings = lineHeightSpans?.getLineHeightPaddings() ?: ZeroVerticalPadding
+ topPadding = max(verticalPaddings.topPadding, lineHeightPaddings.topPadding)
+ bottomPadding = max(verticalPaddings.bottomPadding, lineHeightPaddings.bottomPadding)
val fontMetrics = getLastLineMetrics(textPaint, frameworkTextDir, lineHeightSpans)
lastLineExtra =
@@ -818,10 +802,6 @@
}
}
- /**
- * Checks certain configuration values to understand whether the created text layout comes with
- * fallback line spacing behavior included. Otherwise it needs to be handled here.
- */
internal fun isFallbackLinespacingApplied(): Boolean {
return if (isBoringLayout) {
BoringLayoutFactory.isFallbackLineSpacingEnabled(layout as BoringLayout)
@@ -988,11 +968,7 @@
get() = unpackInt2(packedValue)
}
-/**
- * When includeFontPadding is turned off and fallback line spacing is not applied, there remains a
- * chance that tall glyphs may be cut on top and bottom of the text layout. Vertical paddings is a
- * backport of `useFallbackLineSpacing` from the platform.
- */
+@OptIn(InternalPlatformTextApi::class)
private fun TextLayout.getVerticalPaddings(): VerticalPaddings {
if (includePadding || isFallbackLinespacingApplied()) return ZeroVerticalPadding
@@ -1041,6 +1017,27 @@
private val ZeroVerticalPadding = VerticalPaddings(0, 0)
@OptIn(InternalPlatformTextApi::class)
+private fun Array<LineHeightStyleSpan>.getLineHeightPaddings(): VerticalPaddings {
+ var firstAscentDiff = 0
+ var lastDescentDiff = 0
+
+ for (span in this) {
+ if (span.firstAscentDiff < 0) {
+ firstAscentDiff = max(firstAscentDiff, abs(span.firstAscentDiff))
+ }
+ if (span.lastDescentDiff < 0) {
+ lastDescentDiff = max(firstAscentDiff, abs(span.lastDescentDiff))
+ }
+ }
+
+ return if (firstAscentDiff == 0 && lastDescentDiff == 0) {
+ ZeroVerticalPadding
+ } else {
+ VerticalPaddings(firstAscentDiff, lastDescentDiff)
+ }
+}
+
+@OptIn(InternalPlatformTextApi::class)
private fun TextLayout.getLastLineMetrics(
textPaint: TextPaint,
frameworkTextDir: TextDirectionHeuristic,
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/style/LineHeightStyleSpan.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/style/LineHeightStyleSpan.android.kt
index a636910..1114446 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/style/LineHeightStyleSpan.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/style/LineHeightStyleSpan.android.kt
@@ -45,10 +45,10 @@
val lineHeight: Float,
private val startIndex: Int,
private val endIndex: Int,
- val trimFirstLineTop: Boolean,
+ private val trimFirstLineTop: Boolean,
val trimLastLineBottom: Boolean,
@FloatRange(from = -1.0, to = 1.0) private val topRatio: Float,
- val preserveMinimumHeight: Boolean,
+ private val preserveMinimumHeight: Boolean,
) : android.text.style.LineHeightSpan {
private var firstAscent: Int = Int.MIN_VALUE
@@ -56,6 +56,14 @@
private var descent: Int = Int.MIN_VALUE
private var lastDescent: Int = Int.MIN_VALUE
+ /** Holds the firstAscent - fontMetricsInt.ascent */
+ var firstAscentDiff = 0
+ private set
+
+ /** Holds the last lastDescent - fontMetricsInt.descent */
+ var lastDescentDiff = 0
+ private set
+
init {
checkPrecondition(topRatio in 0f..1f || topRatio == -1f) {
"topRatio should be in [0..1] range or -1"
@@ -77,6 +85,9 @@
val isFirstLine = (start == startIndex)
val isLastLine = (end == endIndex)
+ // if single line and should not apply, return
+ if (isFirstLine && isLastLine && trimFirstLineTop && trimLastLineBottom) return
+
if (firstAscent == Int.MIN_VALUE) {
calculateTargetMetrics(fontMetricsInt)
}
@@ -96,6 +107,8 @@
descent = fontMetricsInt.descent
firstAscent = ascent
lastDescent = descent
+ firstAscentDiff = 0
+ lastDescentDiff = 0
return
}
@@ -118,21 +131,10 @@
descent = fontMetricsInt.descent + descentDiff
ascent = descent - ceiledLineHeight
- // when trimming the first line, smaller ascent value means a larger line height.
- firstAscent =
- if (trimFirstLineTop) {
- maxOf(fontMetricsInt.ascent, ascent)
- } else {
- minOf(fontMetricsInt.ascent, ascent)
- }
-
- // when trimming the last line, larger descent value means a larger line height.
- lastDescent =
- if (trimLastLineBottom) {
- minOf(fontMetricsInt.descent, descent)
- } else {
- maxOf(fontMetricsInt.descent, descent)
- }
+ firstAscent = if (trimFirstLineTop) fontMetricsInt.ascent else ascent
+ lastDescent = if (trimLastLineBottom) fontMetricsInt.descent else descent
+ firstAscentDiff = fontMetricsInt.ascent - firstAscent
+ lastDescentDiff = lastDescent - fontMetricsInt.descent
}
internal fun copy(
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index 08c9f4f1..faaeb0c 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -116,7 +116,6 @@
inceptionYear = "2019"
description = "Compose tooling library. This library exposes information to our tools for better IDE support."
legacyDisableKotlinStrictApiMode = true
- metalavaK2UastEnabled = false
samples(project(":compose:animation:animation:animation-samples"))
// samples(project(":compose:animation:animation-core:animation-core-samples")) TODO(b/318840087)
kotlinTarget = KotlinTarget.KOTLIN_1_9
diff --git a/compose/ui/ui-util/build.gradle b/compose/ui/ui-util/build.gradle
index f6ee5c2..e2285aa 100644
--- a/compose/ui/ui-util/build.gradle
+++ b/compose/ui/ui-util/build.gradle
@@ -113,7 +113,6 @@
inceptionYear = "2020"
description = "Internal Compose utilities used by other modules"
legacyDisableKotlinStrictApiMode = true
- metalavaK2UastEnabled = false
}
androidxCompose {
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 021d09c..a12b66a 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -135,12 +135,10 @@
property public final boolean NewNestedScrollFlingDispatchingEnabled;
property public final boolean isRectTrackingEnabled;
property public final boolean isSemanticAutofillEnabled;
- property public final boolean isViewFocusFixEnabled;
field public static final androidx.compose.ui.ComposeUiFlags INSTANCE;
field public static boolean NewNestedScrollFlingDispatchingEnabled;
field public static boolean isRectTrackingEnabled;
field public static boolean isSemanticAutofillEnabled;
- field public static boolean isViewFocusFixEnabled;
}
public final class ComposedModifierKt {
@@ -2577,7 +2575,7 @@
}
public final class OnGlobalLayoutListenerKt {
- method public static kotlinx.coroutines.DisposableHandle registerOnGlobalLayoutListener(androidx.compose.ui.node.DelegatableNode, int throttleMs, int debounceMs, kotlin.jvm.functions.Function1<? super androidx.compose.ui.spatial.RectInfo,kotlin.Unit> callback);
+ method public static kotlinx.coroutines.DisposableHandle registerOnGlobalLayoutListener(androidx.compose.ui.node.DelegatableNode, long throttleMillis, long debounceMillis, kotlin.jvm.functions.Function1<? super androidx.compose.ui.spatial.RelativeLayoutBounds,kotlin.Unit> callback);
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface OnGloballyPositionedModifier extends androidx.compose.ui.Modifier.Element {
@@ -2588,6 +2586,11 @@
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier onGloballyPositioned(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,kotlin.Unit> onGloballyPositioned);
}
+ public final class OnLayoutRectChangedModifierKt {
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier onLayoutRectChanged(androidx.compose.ui.Modifier, optional long throttleMillis, optional long debounceMillis, kotlin.jvm.functions.Function1<? super androidx.compose.ui.spatial.RelativeLayoutBounds,kotlin.Unit> callback);
+ method public static kotlinx.coroutines.DisposableHandle registerOnLayoutRectChanged(androidx.compose.ui.node.DelegatableNode, long throttleMillis, long debounceMillis, kotlin.jvm.functions.Function1<? super androidx.compose.ui.spatial.RelativeLayoutBounds,kotlin.Unit> callback);
+ }
+
@kotlin.jvm.JvmDefaultWithCompatibility public interface OnPlacedModifier extends androidx.compose.ui.Modifier.Element {
method public void onPlaced(androidx.compose.ui.layout.LayoutCoordinates coordinates);
}
@@ -2596,11 +2599,6 @@
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier onPlaced(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,kotlin.Unit> onPlaced);
}
- public final class OnRectChangedModifierKt {
- method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier onRectChanged(androidx.compose.ui.Modifier, optional int throttleMs, optional int debounceMs, kotlin.jvm.functions.Function1<? super androidx.compose.ui.spatial.RectInfo,kotlin.Unit> callback);
- method public static kotlinx.coroutines.DisposableHandle registerOnRectChanged(androidx.compose.ui.node.DelegatableNode, int throttleMs, int debounceMs, kotlin.jvm.functions.Function1<? super androidx.compose.ui.spatial.RectInfo,kotlin.Unit> callback);
- }
-
@kotlin.jvm.JvmDefaultWithCompatibility public interface OnRemeasuredModifier extends androidx.compose.ui.Modifier.Element {
method public void onRemeasured(long size);
}
@@ -4016,24 +4014,24 @@
package androidx.compose.ui.spatial {
- public final class RectInfo {
+ public final class RelativeLayoutBounds {
method public java.util.List<androidx.compose.ui.unit.IntRect> calculateOcclusions();
+ method public androidx.compose.ui.unit.IntRect getBoundsInRoot();
+ method public androidx.compose.ui.unit.IntRect getBoundsInScreen();
+ method public androidx.compose.ui.unit.IntRect getBoundsInWindow();
method public int getHeight();
method public long getPositionInRoot();
method public long getPositionInScreen();
method public long getPositionInWindow();
- method public androidx.compose.ui.unit.IntRect getRootRect();
- method public androidx.compose.ui.unit.IntRect getScreenRect();
method public int getWidth();
- method public androidx.compose.ui.unit.IntRect getWindowRect();
+ property public final androidx.compose.ui.unit.IntRect boundsInRoot;
+ property public final androidx.compose.ui.unit.IntRect boundsInScreen;
+ property public final androidx.compose.ui.unit.IntRect boundsInWindow;
property public final int height;
property public final long positionInRoot;
property public final long positionInScreen;
property public final long positionInWindow;
- property public final androidx.compose.ui.unit.IntRect rootRect;
- property public final androidx.compose.ui.unit.IntRect screenRect;
property public final int width;
- property public final androidx.compose.ui.unit.IntRect windowRect;
}
}
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 23e436a..258aa94 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -135,12 +135,10 @@
property public final boolean NewNestedScrollFlingDispatchingEnabled;
property public final boolean isRectTrackingEnabled;
property public final boolean isSemanticAutofillEnabled;
- property public final boolean isViewFocusFixEnabled;
field public static final androidx.compose.ui.ComposeUiFlags INSTANCE;
field public static boolean NewNestedScrollFlingDispatchingEnabled;
field public static boolean isRectTrackingEnabled;
field public static boolean isSemanticAutofillEnabled;
- field public static boolean isViewFocusFixEnabled;
}
public final class ComposedModifierKt {
@@ -2585,7 +2583,7 @@
}
public final class OnGlobalLayoutListenerKt {
- method public static kotlinx.coroutines.DisposableHandle registerOnGlobalLayoutListener(androidx.compose.ui.node.DelegatableNode, int throttleMs, int debounceMs, kotlin.jvm.functions.Function1<? super androidx.compose.ui.spatial.RectInfo,kotlin.Unit> callback);
+ method public static kotlinx.coroutines.DisposableHandle registerOnGlobalLayoutListener(androidx.compose.ui.node.DelegatableNode, long throttleMillis, long debounceMillis, kotlin.jvm.functions.Function1<? super androidx.compose.ui.spatial.RelativeLayoutBounds,kotlin.Unit> callback);
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface OnGloballyPositionedModifier extends androidx.compose.ui.Modifier.Element {
@@ -2596,6 +2594,11 @@
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier onGloballyPositioned(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,kotlin.Unit> onGloballyPositioned);
}
+ public final class OnLayoutRectChangedModifierKt {
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier onLayoutRectChanged(androidx.compose.ui.Modifier, optional long throttleMillis, optional long debounceMillis, kotlin.jvm.functions.Function1<? super androidx.compose.ui.spatial.RelativeLayoutBounds,kotlin.Unit> callback);
+ method public static kotlinx.coroutines.DisposableHandle registerOnLayoutRectChanged(androidx.compose.ui.node.DelegatableNode, long throttleMillis, long debounceMillis, kotlin.jvm.functions.Function1<? super androidx.compose.ui.spatial.RelativeLayoutBounds,kotlin.Unit> callback);
+ }
+
@kotlin.jvm.JvmDefaultWithCompatibility public interface OnPlacedModifier extends androidx.compose.ui.Modifier.Element {
method public void onPlaced(androidx.compose.ui.layout.LayoutCoordinates coordinates);
}
@@ -2604,11 +2607,6 @@
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier onPlaced(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,kotlin.Unit> onPlaced);
}
- public final class OnRectChangedModifierKt {
- method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier onRectChanged(androidx.compose.ui.Modifier, optional int throttleMs, optional int debounceMs, kotlin.jvm.functions.Function1<? super androidx.compose.ui.spatial.RectInfo,kotlin.Unit> callback);
- method public static kotlinx.coroutines.DisposableHandle registerOnRectChanged(androidx.compose.ui.node.DelegatableNode, int throttleMs, int debounceMs, kotlin.jvm.functions.Function1<? super androidx.compose.ui.spatial.RectInfo,kotlin.Unit> callback);
- }
-
@kotlin.jvm.JvmDefaultWithCompatibility public interface OnRemeasuredModifier extends androidx.compose.ui.Modifier.Element {
method public void onRemeasured(long size);
}
@@ -4077,24 +4075,24 @@
package androidx.compose.ui.spatial {
- public final class RectInfo {
+ public final class RelativeLayoutBounds {
method public java.util.List<androidx.compose.ui.unit.IntRect> calculateOcclusions();
+ method public androidx.compose.ui.unit.IntRect getBoundsInRoot();
+ method public androidx.compose.ui.unit.IntRect getBoundsInScreen();
+ method public androidx.compose.ui.unit.IntRect getBoundsInWindow();
method public int getHeight();
method public long getPositionInRoot();
method public long getPositionInScreen();
method public long getPositionInWindow();
- method public androidx.compose.ui.unit.IntRect getRootRect();
- method public androidx.compose.ui.unit.IntRect getScreenRect();
method public int getWidth();
- method public androidx.compose.ui.unit.IntRect getWindowRect();
+ property public final androidx.compose.ui.unit.IntRect boundsInRoot;
+ property public final androidx.compose.ui.unit.IntRect boundsInScreen;
+ property public final androidx.compose.ui.unit.IntRect boundsInWindow;
property public final int height;
property public final long positionInRoot;
property public final long positionInScreen;
property public final long positionInWindow;
- property public final androidx.compose.ui.unit.IntRect rootRect;
- property public final androidx.compose.ui.unit.IntRect screenRect;
property public final int width;
- property public final androidx.compose.ui.unit.IntRect windowRect;
}
}
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index 60d27f7..73d3551 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -66,7 +66,7 @@
commonTest {
dependencies {
implementation(libs.kotlinReflect)
- api project(":compose:ui:ui-util")
+ api(project(":compose:ui:ui-util"))
}
}
@@ -219,6 +219,7 @@
metalavaK2UastEnabled = false
samples(project(":compose:ui:ui:ui-samples"))
kotlinTarget = KotlinTarget.KOTLIN_1_9
+ addGoldenImageAssets()
}
android {
@@ -229,12 +230,6 @@
}
// TODO(b/345531033)
experimentalProperties["android.lint.useK2Uast"] = false
-}
-
-// Screenshot tests related setup
-android {
- sourceSets.androidTest.assets.srcDirs +=
- project.rootDir.absolutePath + "/../../golden/compose/ui/ui"
namespace = "androidx.compose.ui"
// namespace has to be unique, but default androidx.compose.ui.test package is taken by
// the androidx.compose.ui:ui-test library
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/ClipboardDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/ClipboardDemo.kt
index 921e2c0..8c170e5 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/ClipboardDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/ClipboardDemo.kt
@@ -188,7 +188,7 @@
return clipMetadata.clipDescription.hasMimeType("image/*")
}
-@Suppress("ClassVerificationFailure", "DEPRECATION")
+@Suppress("DEPRECATION")
fun Uri.readImageBitmap(context: Context): ImageBitmap {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, this))
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/SimpleChatActivity.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/SimpleChatActivity.kt
index b76b3a6..7e69716 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/SimpleChatActivity.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/SimpleChatActivity.kt
@@ -56,7 +56,6 @@
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
-@SuppressLint("ClassVerificationFailure")
class SimpleChatActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index 3492d49..47bd70f 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -32,6 +32,7 @@
import androidx.compose.ui.demos.accessibility.ScaffoldSampleScrollDemo
import androidx.compose.ui.demos.accessibility.ScrollingColumnDemo
import androidx.compose.ui.demos.accessibility.SimpleRtlLayoutDemo
+import androidx.compose.ui.demos.autofill.AutofillNavigation
import androidx.compose.ui.demos.autofill.BTFResetCredentialsDemo
import androidx.compose.ui.demos.autofill.BasicSecureTextFieldAutofillDemo
import androidx.compose.ui.demos.autofill.BasicTextFieldAutofill
@@ -286,7 +287,8 @@
BasicSecureTextFieldAutofillDemo()
},
ComposableDemo("S: TextField Autofill") { LegacyTextFieldAutofillDemo() },
- ComposableDemo("S: OutlinedTextField Autofill") { OutlinedTextFieldAutofillDemo() }
+ ComposableDemo("S: OutlinedTextField Autofill") { OutlinedTextFieldAutofillDemo() },
+ ComposableDemo("Navigation Sample") { AutofillNavigation() }
)
)
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/AutofillNavigationDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/AutofillNavigationDemo.kt
index 7f9613e..0e1fc8a 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/AutofillNavigationDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/AutofillNavigationDemo.kt
@@ -34,6 +34,7 @@
import androidx.compose.material.Button
import androidx.compose.material.Checkbox
import androidx.compose.material.Icon
+import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
@@ -48,9 +49,9 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalAutofillManager
import androidx.compose.ui.semantics.contentType
import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
@@ -227,13 +228,7 @@
Spacer(modifier = Modifier.height(16.dp))
Text(text = "Enter your username and password below:")
Spacer(modifier = Modifier.height(8.dp))
- BasicTextField(
- state = remember { TextFieldState() },
- modifier =
- Modifier.fillMaxWidth().border(1.dp, Color.LightGray).semantics {
- contentType = ContentType.NewUsername
- }
- )
+ NavigationDemoTextField(contentType = ContentType.NewUsername)
Spacer(modifier = Modifier.height(8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -243,13 +238,7 @@
if (showPassword) {
Spacer(modifier = Modifier.height(8.dp))
- BasicTextField(
- state = remember { TextFieldState() },
- modifier =
- Modifier.fillMaxWidth().border(1.dp, Color.LightGray).semantics {
- contentType = ContentType.NewPassword
- }
- )
+ NavigationDemoTextField(contentType = ContentType.NewPassword)
}
}
}
@@ -269,21 +258,8 @@
Spacer(modifier = Modifier.height(16.dp))
Text(text = "Enter your username and password below:")
Spacer(modifier = Modifier.height(8.dp))
- BasicTextField(
- state = remember { TextFieldState() },
- modifier =
- Modifier.fillMaxWidth().border(1.dp, Color.LightGray).semantics {
- contentType = ContentType.Username
- }
- )
-
- BasicTextField(
- state = remember { TextFieldState() },
- modifier =
- Modifier.fillMaxWidth().border(1.dp, Color.LightGray).semantics {
- contentType = ContentType.Password
- }
- )
+ NavigationDemoTextField(contentType = ContentType.Username)
+ NavigationDemoTextField(contentType = ContentType.Password)
}
}
@@ -297,13 +273,7 @@
Spacer(modifier = Modifier.height(16.dp))
Text(text = "Enter your username and password below:")
Spacer(modifier = Modifier.height(8.dp))
- BasicTextField(
- state = remember { TextFieldState() },
- modifier =
- Modifier.fillMaxWidth().border(1.dp, Color.LightGray).semantics {
- contentType = ContentType.NewUsername
- }
- )
+ NavigationDemoTextField(contentType = ContentType.NewUsername)
repeat(50) {
Text(
@@ -313,13 +283,7 @@
)
}
- BasicTextField(
- state = remember { TextFieldState() },
- modifier =
- Modifier.fillMaxWidth().border(1.dp, Color.LightGray).semantics {
- contentType = ContentType.NewPassword
- }
- )
+ NavigationDemoTextField(contentType = ContentType.NewPassword)
}
}
@@ -334,21 +298,8 @@
Spacer(modifier = Modifier.height(16.dp))
Text(text = "Enter your username and password below:")
Spacer(modifier = Modifier.height(8.dp))
- BasicTextField(
- state = remember { TextFieldState() },
- modifier =
- Modifier.fillMaxWidth().border(1.dp, Color.LightGray).semantics {
- contentType = ContentType.NewUsername
- }
- )
-
- BasicTextField(
- state = remember { TextFieldState() },
- modifier =
- Modifier.fillMaxWidth().border(1.dp, Color.LightGray).semantics {
- contentType = ContentType.NewPassword
- }
- )
+ NavigationDemoTextField(contentType = ContentType.NewUsername)
+ NavigationDemoTextField(contentType = ContentType.NewPassword)
repeat(50) {
Text(
@@ -364,6 +315,23 @@
// Helper functions
// ============================================================================================
+@Composable
+fun NavigationDemoTextField(
+ modifier: Modifier = Modifier,
+ state: TextFieldState = remember { TextFieldState() },
+ contentType: ContentType,
+ textStyle: TextStyle = LocalTextStyle.current.copy(color = Color.White)
+) {
+ BasicTextField(
+ state = state,
+ modifier =
+ modifier.fillMaxWidth().border(1.dp, Color.LightGray).semantics {
+ this.contentType = contentType
+ },
+ textStyle = textStyle
+ )
+}
+
/** Template scaffold for the navigation demo with two buttons: forward and backwards. */
@Composable
fun TwoButtonNavigationScaffold(
@@ -372,8 +340,6 @@
backwardRoute: String,
content: @Composable () -> Unit
) {
- val autofillManager = LocalAutofillManager.current
-
Scaffold(
content = { padding ->
Column(modifier = Modifier.fillMaxSize().padding(padding)) {
@@ -392,11 +358,7 @@
// Navigation Button forwards
Button(
- onClick = {
- navController.navigate(forwardRoute)
- // Navigating forwards should commit an autofill context.
- autofillManager?.commit()
- },
+ onClick = { navController.navigate(forwardRoute) },
modifier = Modifier.align(Alignment.Start)
) {
Icon(
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
index 3e46712..6f44c0a 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
@@ -77,6 +77,7 @@
@SdkSuppress(minSdkVersion = 26)
@Test
fun onProvideAutofillVirtualStructure_populatesViewStructure() {
+ // TODO(b/383201236): Ensure the old API works when the new API is enabled.
if (isSemanticAutofillEnabled) return
// Arrange.
val viewStructure: ViewStructure = FakeViewStructure()
@@ -99,10 +100,11 @@
children.add(
FakeViewStructure().apply {
virtualId = autofillNode.id
+ autofillId = ownerView.autofillId
+ autofillType = View.AUTOFILL_TYPE_TEXT
+ autofillHints = mutableListOf(AUTOFILL_HINT_PERSON_NAME)
packageName = currentPackageName
- setAutofillType(View.AUTOFILL_TYPE_TEXT)
- setAutofillHints(arrayOf(AUTOFILL_HINT_PERSON_NAME))
- setDimens(0, 0, 0, 0, 0, 0)
+ bounds = android.graphics.Rect(0, 0, 0, 0)
}
)
}
@@ -112,6 +114,7 @@
@SdkSuppress(minSdkVersion = 26)
@Test
fun autofill_triggersOnFill() {
+ // TODO(b/383201236): Ensure the old API works when the new API is enabled.
if (isSemanticAutofillEnabled) return
// Arrange.
val expectedValue = "PersonName"
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
index df30b59f..face43c 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
@@ -17,30 +17,26 @@
package androidx.compose.ui.autofill
import android.os.Build
-import android.view.View
import androidx.annotation.RequiresApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
-import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
-import androidx.compose.ui.ComposeUiFlags.isSemanticAutofillEnabled
+import androidx.compose.ui.ComposeUiFlags
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.platform.AndroidComposeView
import androidx.compose.ui.platform.LocalAutofillManager
-import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.contentDataType
import androidx.compose.ui.semantics.contentType
import androidx.compose.ui.semantics.editableText
import androidx.compose.ui.semantics.focused
+import androidx.compose.ui.semantics.onAutofillText
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.test.TestActivity
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
@@ -50,12 +46,15 @@
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
+import kotlin.test.Ignore
import org.junit.After
+import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
@@ -65,15 +64,18 @@
class AndroidAutofillManagerTest {
@get:Rule val rule = createAndroidComposeRule<TestActivity>()
- private lateinit var androidComposeView: AndroidComposeView
- private lateinit var composeView: View
- private lateinit var autofillManagerMock: AutofillManagerWrapper
-
- private val autofillEventLoopIntervalMs = 100L
-
private val height = 200.dp
private val width = 200.dp
+ @OptIn(ExperimentalComposeUiApi::class)
+ private val previousFlagValue = ComposeUiFlags.isSemanticAutofillEnabled
+
+ @Before
+ fun enableAutofill() {
+ @OptIn(ExperimentalComposeUiApi::class)
+ ComposeUiFlags.isSemanticAutofillEnabled = true
+ }
+
@After
fun teardown() {
val instrumentation = InstrumentationRegistry.getInstrumentation()
@@ -85,15 +87,18 @@
}
}
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ ComposeUiFlags.isSemanticAutofillEnabled = previousFlagValue
}
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
fun autofillManager_empty() {
+ val am: PlatformAutofillManager = mock()
val usernameTag = "username_tag"
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
Box(
Modifier.semantics { contentType = ContentType.Username }
.size(height, width)
@@ -101,17 +106,162 @@
)
}
- rule.runOnIdle { verifyNoMoreInteractions(autofillManagerMock) }
+ rule.runOnIdle { verifyNoMoreInteractions(am) }
+ }
+
+ @Test
+ @SmallTest
+ fun autofillManager_doNotCallCommit_nodesAppeared() {
+ val am: PlatformAutofillManager = mock()
+ val usernameTag = "username_tag"
+ var isVisible by mutableStateOf(false)
+
+ rule.setContent {
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
+ if (isVisible) {
+ Box(
+ Modifier.semantics { contentType = ContentType.Username }
+ .size(height, width)
+ .testTag(usernameTag)
+ )
+ }
+ }
+
+ rule.runOnIdle { isVisible = true }
+
+ // `commit` should not be called when an autofillable component appears onscreen.
+ rule.runOnIdle { verify(am, times(0)).commit() }
+ }
+
+ @Test
+ @SmallTest
+ fun autofillManager_doNotCallCommit_autofillTagsAdded() {
+ val am: PlatformAutofillManager = mock()
+ val usernameTag = "username_tag"
+ var isRelatedToAutofill by mutableStateOf(false)
+
+ rule.setContent {
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
+ Box(
+ modifier =
+ if (isRelatedToAutofill)
+ Modifier.semantics {
+ contentType = ContentType.Username
+ contentDataType = ContentDataType.Text
+ }
+ .size(height, width)
+ .testTag(usernameTag)
+ else Modifier.size(height, width).testTag(usernameTag)
+ )
+ }
+
+ rule.runOnIdle { isRelatedToAutofill = true }
+
+ // `commit` should not be called a component becomes relevant to autofill.
+ rule.runOnIdle { verify(am, times(0)).commit() }
+ }
+
+ @Test
+ @SmallTest
+ fun autofillManager_callCommit_nodesDisappeared() {
+ val am: PlatformAutofillManager = mock()
+ val usernameTag = "username_tag"
+ var revealFirstUsername by mutableStateOf(true)
+
+ rule.setContent {
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
+ if (revealFirstUsername) {
+ Box(
+ Modifier.semantics { contentType = ContentType.Username }
+ .size(height, width)
+ .testTag(usernameTag)
+ )
+ }
+ }
+
+ rule.runOnIdle { revealFirstUsername = false }
+
+ // `commit` should be called when an autofillable component leaves the screen.
+ rule.runOnIdle { verify(am, times(1)).commit() }
+ }
+
+ @Test
+ @SmallTest
+ fun autofillManager_callCommit_nodesDisappearedAndAppeared() {
+ val am: PlatformAutofillManager = mock()
+ val username1Tag = "username_tag"
+ val username2Tag = "username_tag"
+ var revealFirstUsername by mutableStateOf(true)
+ var revealSecondUsername by mutableStateOf(false)
+
+ rule.setContent {
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
+ if (revealFirstUsername) {
+ Box(
+ Modifier.semantics { contentType = ContentType.Username }
+ .size(height, width)
+ .testTag(username1Tag)
+ )
+ }
+ if (revealSecondUsername) {
+ Box(
+ Modifier.semantics { contentType = ContentType.Username }
+ .size(height, width)
+ .testTag(username2Tag)
+ )
+ }
+ }
+
+ rule.runOnIdle { revealFirstUsername = false }
+ rule.runOnIdle { revealSecondUsername = true }
+
+ // `commit` should be called when an autofillable component leaves onscreen, even when
+ // another, different autofillable component is added.
+ rule.runOnIdle { verify(am, times(1)).commit() }
+ }
+
+ @Test
+ @SmallTest
+ fun autofillManager_callCommit_nodesBecomeAutofillRelatedAndDisappear() {
+ val am: PlatformAutofillManager = mock()
+ val usernameTag = "username_tag"
+ var isVisible by mutableStateOf(true)
+ var isRelatedToAutofill by mutableStateOf(false)
+
+ rule.setContent {
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
+ if (isVisible) {
+ Box(
+ modifier =
+ if (isRelatedToAutofill)
+ Modifier.semantics {
+ contentType = ContentType.Username
+ contentDataType = ContentDataType.Text
+ }
+ .size(height, width)
+ .testTag(usernameTag)
+ else Modifier.size(height, width).testTag(usernameTag)
+ )
+ }
+ }
+
+ rule.runOnIdle { isRelatedToAutofill = true }
+ rule.runOnIdle { isVisible = false }
+
+ // `commit` should be called when component becomes autofillable, then leaves the screen.
+ rule.runOnIdle { verify(am, times(1)).commit() }
}
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
fun autofillManager_notifyValueChanged() {
+ val am: PlatformAutofillManager = mock()
val usernameTag = "username_tag"
var changeText by mutableStateOf(false)
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
Box(
Modifier.semantics {
contentType = ContentType.Username
@@ -125,17 +275,46 @@
rule.runOnIdle { changeText = true }
- rule.runOnIdle { verify(autofillManagerMock).notifyValueChanged(any(), any()) }
+ rule.runOnIdle { verify(am).notifyValueChanged(any(), any(), any()) }
}
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
fun autofillManager_notifyViewEntered_previousFocusFalse() {
+ val am: PlatformAutofillManager = mock()
val usernameTag = "username_tag"
var hasFocus by mutableStateOf(false)
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
+ Box(
+ Modifier.semantics {
+ contentType = ContentType.Username
+ contentDataType = ContentDataType.Text
+ focused = hasFocus
+ onAutofillText { true }
+ }
+ .size(height, width)
+ .testTag(usernameTag)
+ )
+ }
+
+ rule.runOnIdle { hasFocus = true }
+
+ rule.runOnIdle { verify(am).notifyViewEntered(any(), any(), any()) }
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 26)
+ fun autofillManager_notAutofillable_notifyViewEntered_previousFocusFalse() {
+ val am: PlatformAutofillManager = mock()
+ val usernameTag = "username_tag"
+ var hasFocus by mutableStateOf(false)
+
+ rule.setContent {
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
Box(
Modifier.semantics {
contentType = ContentType.Username
@@ -149,17 +328,49 @@
rule.runOnIdle { hasFocus = true }
- rule.runOnIdle { verify(autofillManagerMock).notifyViewEntered(any(), any()) }
+ rule.runOnIdle { verifyNoMoreInteractions(am) }
}
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
fun autofillManager_notifyViewEntered_previousFocusNull() {
+ val am: PlatformAutofillManager = mock()
val usernameTag = "username_tag"
var hasFocus by mutableStateOf(false)
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
+ Box(
+ modifier =
+ if (hasFocus)
+ Modifier.semantics {
+ contentType = ContentType.Username
+ contentDataType = ContentDataType.Text
+ focused = hasFocus
+ onAutofillText { true }
+ }
+ .size(height, width)
+ .testTag(usernameTag)
+ else plainVisibleModifier(usernameTag)
+ )
+ }
+
+ rule.runOnIdle { hasFocus = true }
+
+ rule.runOnIdle { verify(am).notifyViewEntered(any(), any(), any()) }
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 26)
+ fun autofillManager_notifyViewEntered_itemNotAutofillable() {
+ val am: PlatformAutofillManager = mock()
+ val usernameTag = "username_tag"
+ var hasFocus by mutableStateOf(false)
+
+ rule.setContent {
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
Box(
modifier =
if (hasFocus)
@@ -176,17 +387,46 @@
rule.runOnIdle { hasFocus = true }
- rule.runOnIdle { verify(autofillManagerMock).notifyViewEntered(any(), any()) }
+ rule.runOnIdle { verifyNoMoreInteractions(am) }
}
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
fun autofillManager_notifyViewExited_previousFocusTrue() {
+ val am: PlatformAutofillManager = mock()
val usernameTag = "username_tag"
var hasFocus by mutableStateOf(true)
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
+ Box(
+ Modifier.semantics {
+ contentType = ContentType.Username
+ contentDataType = ContentDataType.Text
+ focused = hasFocus
+ onAutofillText { true }
+ }
+ .size(height, width)
+ .testTag(usernameTag)
+ )
+ }
+
+ rule.runOnIdle { hasFocus = false }
+
+ rule.runOnIdle { verify(am).notifyViewExited(any(), any()) }
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 26)
+ fun autofillManager_notifyViewExited_previouslyFocusedItemNotAutofillable() {
+ val am: PlatformAutofillManager = mock()
+ val usernameTag = "username_tag"
+ var hasFocus by mutableStateOf(true)
+
+ rule.setContent {
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
Box(
Modifier.semantics {
contentType = ContentType.Username
@@ -200,17 +440,20 @@
rule.runOnIdle { hasFocus = false }
- rule.runOnIdle { verify(autofillManagerMock).notifyViewExited(any()) }
+ rule.runOnIdle { verifyNoMoreInteractions(am) }
}
+ @Ignore // TODO(b/383198004): Add support for notifyVisibilityChanged.
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 27)
fun autofillManager_notifyVisibilityChanged_disappeared() {
+ val am: PlatformAutofillManager = mock()
val usernameTag = "username_tag"
var isVisible by mutableStateOf(true)
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
Box(
modifier =
if (isVisible) plainVisibleModifier(usernameTag)
@@ -220,17 +463,20 @@
rule.runOnIdle { isVisible = false }
- rule.runOnIdle { verify(autofillManagerMock).notifyViewVisibilityChanged(any(), any()) }
+ rule.runOnIdle { verify(am).notifyViewVisibilityChanged(any(), any(), any()) }
}
+ @Ignore // TODO(b/383198004): Add support for notifyVisibilityChanged.
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 27)
fun autofillManager_notifyVisibilityChanged_appeared() {
+ val am: PlatformAutofillManager = mock()
val usernameTag = "username_tag"
var isVisible by mutableStateOf(false)
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
Box(
modifier =
if (isVisible) plainVisibleModifier(usernameTag)
@@ -240,17 +486,19 @@
rule.runOnIdle { isVisible = true }
- rule.runOnIdle { verify(autofillManagerMock).notifyViewVisibilityChanged(any(), any()) }
+ rule.runOnIdle { verify(am).notifyViewVisibilityChanged(any(), any(), any()) }
}
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
fun autofillManager_notifyCommit() {
+ val am: PlatformAutofillManager = mock()
val forwardTag = "forward_button_tag"
var autofillManager: AutofillManager?
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
autofillManager = LocalAutofillManager.current
Box(
modifier =
@@ -262,18 +510,20 @@
rule.onNodeWithTag(forwardTag).performClick()
- rule.runOnIdle { verify(autofillManagerMock).commit() }
+ rule.runOnIdle { verify(am).commit() }
}
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
fun autofillManager_notifyCancel() {
+ val am: PlatformAutofillManager = mock()
val backTag = "back_button_tag"
var autofillManager: AutofillManager?
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
autofillManager = LocalAutofillManager.current
+ (autofillManager as AndroidAutofillManager).platformAutofillManager = am
Box(
modifier =
Modifier.clickable { autofillManager?.cancel() }
@@ -283,19 +533,58 @@
}
rule.onNodeWithTag(backTag).performClick()
- rule.runOnIdle { verify(autofillManagerMock).cancel() }
+ rule.runOnIdle { verify(am).cancel() }
}
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
fun autofillManager_requestAutofillAfterFocus() {
+ val am: PlatformAutofillManager = mock()
val contextMenuTag = "menu_tag"
var autofillManager: AutofillManager?
var hasFocus by mutableStateOf(false)
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
autofillManager = LocalAutofillManager.current
+ (autofillManager as AndroidAutofillManager).platformAutofillManager = am
+ Box(
+ modifier =
+ if (hasFocus)
+ Modifier.semantics {
+ contentType = ContentType.Username
+ contentDataType = ContentDataType.Text
+ focused = hasFocus
+ onAutofillText { true }
+ }
+ .clickable { autofillManager?.requestAutofillForActiveElement() }
+ .size(height, width)
+ .testTag(contextMenuTag)
+ else plainVisibleModifier(contextMenuTag)
+ )
+ }
+
+ // `requestAutofill` is always called after an element is focused
+ rule.runOnIdle { hasFocus = true }
+ rule.runOnIdle { verify(am).notifyViewEntered(any(), any(), any()) }
+
+ // then `requestAutofill` is called on that same previously focused element
+ rule.onNodeWithTag(contextMenuTag).performClick()
+ rule.runOnIdle { verify(am).requestAutofill(any(), any(), any()) }
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 26)
+ fun autofillManager_notAutofillable_doesNotrequestAutofillAfterFocus() {
+ val am: PlatformAutofillManager = mock()
+ val contextMenuTag = "menu_tag"
+ var autofillManager: AutofillManager?
+ var hasFocus by mutableStateOf(false)
+
+ rule.setContent {
+ autofillManager = LocalAutofillManager.current
+ (autofillManager as AndroidAutofillManager).platformAutofillManager = am
Box(
modifier =
if (hasFocus)
@@ -313,11 +602,7 @@
// `requestAutofill` is always called after an element is focused
rule.runOnIdle { hasFocus = true }
- rule.runOnIdle { verify(autofillManagerMock).notifyViewEntered(any(), any()) }
-
- // then `requestAutofill` is called on that same previously focused element
- rule.onNodeWithTag(contextMenuTag).performClick()
- rule.runOnIdle { verify(autofillManagerMock).requestAutofill(any(), any()) }
+ rule.runOnIdle { verifyNoMoreInteractions(am) }
}
// ============================================================================================
@@ -342,25 +627,4 @@
.size(width, height)
.testTag(testTag)
}
-
- @OptIn(ExperimentalComposeUiApi::class)
- @RequiresApi(Build.VERSION_CODES.O)
- private fun ComposeContentTestRule.setContentWithAutofillEnabled(
- content: @Composable () -> Unit
- ) {
- autofillManagerMock = mock()
-
- setContent {
- androidComposeView = LocalView.current as AndroidComposeView
- androidComposeView._autofillManager?.currentSemanticsNodesInvalidated = true
- androidComposeView._autofillManager?.autofillManager = autofillManagerMock
- isSemanticAutofillEnabled = true
-
- composeView = LocalView.current
-
- content()
- }
-
- runOnIdle { mainClock.advanceTimeBy(autofillEventLoopIntervalMs) }
- }
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt
index 73b2b54..aa3ffa8 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt
@@ -21,109 +21,83 @@
import android.os.Build
import android.os.Bundle
import android.os.LocaleList
-import android.os.Parcel
import android.view.View
import android.view.ViewStructure
import android.view.autofill.AutofillId
import android.view.autofill.AutofillValue
-import androidx.annotation.GuardedBy
import androidx.annotation.RequiresApi
/**
* A fake implementation of [ViewStructure] to use in tests.
*
- * @param virtualId An ID that is unique for each viewStructure node in the viewStructure tree.
- * @param packageName The package name of the app (Used as an autofill heuristic).
- * @param typeName The type name of the view's identifier, or null if there is none.
- * @param entryName The entry name of the view's identifier, or null if there is none.
- * @param children A list of [ViewStructure]s that are children of the current [ViewStructure].
- * @param bounds The bounds (Dimensions) of the component represented by this [ViewStructure].
- * @param autofillId The [autofillId] for the parent component. The same autofillId is used for
- * other child components.
- * @param autofillType The data type. Can be one of the following: [View.AUTOFILL_TYPE_DATE],
- * [View.AUTOFILL_TYPE_LIST], [View.AUTOFILL_TYPE_TEXT], [View.AUTOFILL_TYPE_TOGGLE] or
- * [View.AUTOFILL_TYPE_NONE].
- * @param autofillHints The autofill hint. If this value not specified, we use heuristics to
- * determine what data to use while performing autofill.
+ * We use a data class to get an equals and toString implementation. The properties are marked as
+ *
+ * @JvmField so that they don't clash with the set* functions in the ViewStructure interface that
+ * this class implements.
*/
@RequiresApi(Build.VERSION_CODES.M)
internal data class FakeViewStructure(
- var virtualId: Int = 0,
- var packageName: String? = null,
- var typeName: String? = null,
- var entryName: String? = null,
- var children: MutableList<FakeViewStructure> = mutableListOf(),
- var bounds: Rect? = null,
- private val autofillId: AutofillId? =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) generateAutofillId() else null,
- internal var autofillType: Int = View.AUTOFILL_TYPE_NONE,
- internal var autofillHints: Array<out String> = arrayOf()
+ @JvmField var virtualId: Int = 0,
+ @JvmField var packageName: String? = null,
+ @JvmField var typeName: String? = null,
+ @JvmField var entryName: String? = null,
+ @JvmField var children: MutableList<FakeViewStructure> = mutableListOf(),
+ @JvmField var bounds: Rect? = null,
+ @JvmField var autofillId: AutofillId? = null,
+ @JvmField var isCheckable: Boolean = false,
+ @JvmField var isFocusable: Boolean = false,
+ @JvmField var autofillType: Int = View.AUTOFILL_TYPE_NONE,
+ @JvmField var autofillHints: MutableList<String> = mutableListOf(),
+ @JvmField var activated: Boolean = false,
+ @JvmField var alpha: Float = 1f,
+ @JvmField var autofillOptions: MutableList<CharSequence>? = null,
+ @JvmField var autofillValue: AutofillValue? = null,
+ @JvmField var className: String? = null,
+ @JvmField var contentDescription: CharSequence? = null,
+ @JvmField var dataIsSensitive: Boolean = false,
+ @JvmField var elevation: Float = 0f,
+ @JvmField var hint: CharSequence? = null,
+ @JvmField var htmlInfo: HtmlInfo? = null,
+ @JvmField var inputType: Int = 0,
+ @JvmField var isEnabled: Boolean = false,
+ @JvmField var isAccessibilityFocused: Boolean = false,
+ @JvmField var isChecked: Boolean = false,
+ @JvmField var isClickable: Boolean = false,
+ @JvmField var isContextClickable: Boolean = false,
+ @JvmField var isFocused: Boolean = false,
+ @JvmField var isLongClickable: Boolean = false,
+ @JvmField var isOpaque: Boolean = false,
+ @JvmField var isSelected: Boolean = false,
+ @JvmField var text: CharSequence = "",
+ @JvmField var textLinesCharOffsets: MutableList<Int>? = null,
+ @JvmField var textLinesBaselines: MutableList<Int>? = null,
+ @JvmField var transformation: Matrix? = null,
+ @JvmField var visibility: Int = View.VISIBLE,
+ @JvmField var maxTextLength: Int = -1,
+ @JvmField var webDomain: String? = null,
) : ViewStructure() {
-
- private var activated: Boolean = false
- private var alpha: Float = 1f
- private var autofillOptions: Array<CharSequence>? = null
- private var autofillValue: AutofillValue? = null
- private var className: String? = null
- private var contentDescription: CharSequence? = null
- private var dataIsSensitive: Boolean = false
- private var elevation: Float = 0f
- private var extras: Bundle = Bundle()
- private var hint: CharSequence? = null
- private var htmlInfo: HtmlInfo? = null
- private var inputType: Int = 0
- private var isEnabled: Boolean = true
- private var isAccessibilityFocused: Boolean = false
- private var isCheckable: Boolean = false
- private var isChecked: Boolean = false
- private var isClickable: Boolean = true
- private var isContextClickable: Boolean = false
- private var isFocused: Boolean = false
- private var isFocusable: Boolean = false
- private var isLongClickable: Boolean = false
- private var isOpaque: Boolean = false
- private var selected: Boolean = false
- private var text: CharSequence = ""
- private var textLines: IntArray? = null
- private var transformation: Matrix? = null
- private var visibility: Int = View.VISIBLE
- private var webDomain: String? = null
-
- internal companion object {
- @GuardedBy("this") private var previousId = 0
- private val NO_SESSION = 0
-
- // Android API level 26 introduced Autofill. Prior to API level 26, no autofill ID will be
- // provided.
- @RequiresApi(Build.VERSION_CODES.O)
- @Synchronized
- private fun generateAutofillId(): AutofillId {
- var autofillId: AutofillId? = null
- useParcel { parcel ->
- parcel.writeInt(++previousId) // View Id.
- parcel.writeInt(NO_SESSION) // Flag.
- parcel.setDataPosition(0)
- autofillId = AutofillId.CREATOR.createFromParcel(parcel)
- }
- return autofillId ?: error("Could not generate autofill id")
- }
- }
+ @JvmField var extras: Bundle = Bundle()
override fun getChildCount() = children.count()
override fun addChildCount(childCount: Int): Int {
- repeat(childCount) { children.add(FakeViewStructure(autofillId = autofillId)) }
- return children.count() - childCount
+ repeat(childCount) { children.add(FakeViewStructure()) }
+ return children.size - childCount
}
override fun newChild(index: Int): FakeViewStructure {
- if (index >= children.count()) error("Call addChildCount() before calling newChild()")
+ if (index >= children.size) error("Call addChildCount() before calling newChild()")
return children[index]
}
- override fun getAutofillId() = autofillId
+ override fun getAutofillId(): AutofillId? = autofillId
+
+ override fun setAutofillId(id: AutofillId) {
+ autofillId = id
+ }
override fun setAutofillId(rootId: AutofillId, virtualId: Int) {
+ autofillId = rootId
this.virtualId = virtualId
}
@@ -144,53 +118,13 @@
}
override fun setAutofillHints(autofillHints: Array<out String>?) {
- autofillHints?.let { this.autofillHints = it }
+ autofillHints?.let { this.autofillHints = it.toMutableList() }
}
override fun setDimens(left: Int, top: Int, x: Int, y: Int, width: Int, height: Int) {
- this.bounds = Rect(left, top, width - left, height - top)
+ this.bounds = Rect(left, top, left + width, top + height)
}
- override fun equals(other: Any?) =
- other is FakeViewStructure &&
- other.virtualId == virtualId &&
- other.packageName == packageName &&
- other.typeName == typeName &&
- other.entryName == entryName &&
- other.autofillType == autofillType &&
- other.autofillHints.contentEquals(autofillHints) &&
- other.bounds.contentEquals(bounds) &&
- other.activated == activated &&
- other.alpha == alpha &&
- other.autofillOptions.contentEquals(autofillOptions) &&
- other.autofillValue == autofillValue &&
- other.className == className &&
- other.children.count() == children.count() &&
- other.contentDescription == contentDescription &&
- other.dataIsSensitive == dataIsSensitive &&
- other.elevation == elevation &&
- other.hint == hint &&
- other.htmlInfo == htmlInfo &&
- other.inputType == inputType &&
- other.isEnabled == isEnabled &&
- other.isCheckable == isCheckable &&
- other.isChecked == isChecked &&
- other.isClickable == isClickable &&
- other.isContextClickable == isContextClickable &&
- other.isAccessibilityFocused == isAccessibilityFocused &&
- other.isFocused == isFocused &&
- other.isLongClickable == isLongClickable &&
- other.isOpaque == isOpaque &&
- other.isFocusable == isFocusable &&
- other.selected == selected &&
- other.text == text &&
- other.textLines.contentEquals(textLines) &&
- other.transformation == transformation &&
- other.visibility == visibility &&
- other.webDomain == webDomain
-
- override fun hashCode() = super.hashCode()
-
override fun getExtras() = extras
override fun getHint() = hint ?: ""
@@ -199,116 +133,121 @@
override fun hasExtras() = !extras.isEmpty
- override fun setActivated(p0: Boolean) {
- activated = p0
+ override fun setActivated(state: Boolean) {
+ activated = state
}
- override fun setAccessibilityFocused(p0: Boolean) {
- isAccessibilityFocused = p0
+ override fun setAccessibilityFocused(state: Boolean) {
+ isAccessibilityFocused = state
}
- override fun setAlpha(p0: Float) {
- alpha = p0
+ override fun setAlpha(alpha: Float) {
+ this.alpha = alpha
}
- override fun setAutofillOptions(p0: Array<CharSequence>?) {
- autofillOptions = p0
+ override fun setAutofillOptions(options: Array<CharSequence>?) {
+ autofillOptions = options?.toMutableList()
}
- override fun setAutofillValue(p0: AutofillValue?) {
- autofillValue = p0
+ override fun setAutofillValue(value: AutofillValue?) {
+ autofillValue = value
}
- override fun setCheckable(p0: Boolean) {
- isCheckable = p0
+ override fun setCheckable(state: Boolean) {
+ isCheckable = state
}
- override fun setChecked(p0: Boolean) {
- isChecked = p0
+ override fun setChecked(state: Boolean) {
+ isChecked = state
}
- override fun setClassName(p0: String?) {
- className = p0
+ override fun setClassName(className: String?) {
+ this.className = className
}
- override fun setClickable(p0: Boolean) {
- isClickable = p0
+ override fun setClickable(state: Boolean) {
+ isClickable = state
}
- override fun setContentDescription(p0: CharSequence?) {
- contentDescription = p0
+ override fun setContentDescription(contentDescription: CharSequence?) {
+ this.contentDescription = contentDescription
}
- override fun setContextClickable(p0: Boolean) {
- isContextClickable = p0
+ override fun setContextClickable(state: Boolean) {
+ isContextClickable = state
}
- override fun setDataIsSensitive(p0: Boolean) {
- dataIsSensitive = p0
+ override fun setDataIsSensitive(sensitive: Boolean) {
+ dataIsSensitive = sensitive
}
- override fun setElevation(p0: Float) {
- elevation = p0
+ override fun setElevation(elevation: Float) {
+ this.elevation = elevation
}
- override fun setEnabled(p0: Boolean) {
- isEnabled = p0
+ override fun setEnabled(state: Boolean) {
+ isEnabled = state
}
- override fun setFocusable(p0: Boolean) {
- isFocusable = p0
+ override fun setFocusable(state: Boolean) {
+ isFocusable = state
}
- override fun setFocused(p0: Boolean) {
- isFocused = p0
+ override fun setFocused(state: Boolean) {
+ isFocused = state
}
- override fun setHtmlInfo(p0: HtmlInfo) {
- htmlInfo = p0
+ override fun setHtmlInfo(htmlInfo: HtmlInfo) {
+ this.htmlInfo = htmlInfo
}
- override fun setHint(p0: CharSequence?) {
- hint = p0
+ override fun setHint(hint: CharSequence?) {
+ this.hint = hint
}
- override fun setInputType(p0: Int) {
- inputType = p0
+ override fun setInputType(inputType: Int) {
+ this.inputType = inputType
}
- override fun setLongClickable(p0: Boolean) {
- isLongClickable = p0
+ override fun setLongClickable(state: Boolean) {
+ isLongClickable = state
}
- override fun setOpaque(p0: Boolean) {
- isOpaque = p0
+ override fun setMaxTextLength(maxLength: Int) {
+ maxTextLength = maxLength
}
- override fun setSelected(p0: Boolean) {
- selected = p0
+ override fun setOpaque(opaque: Boolean) {
+ isOpaque = opaque
}
- override fun setText(p0: CharSequence?) {
- p0?.let { text = it }
+ override fun setSelected(state: Boolean) {
+ isSelected = state
}
- override fun setText(p0: CharSequence?, p1: Int, p2: Int) {
- p0?.let { text = it.subSequence(p1, p2) }
+ override fun setText(charSequence: CharSequence?) {
+ charSequence?.let { text = it }
}
- override fun setTextLines(p0: IntArray?, p1: IntArray?) {
- textLines = p0
+ override fun setText(charSequence: CharSequence?, selectionStart: Int, selectionEnd: Int) {
+ charSequence?.let { text = it.subSequence(selectionStart, selectionEnd) }
}
- override fun setTransformation(p0: Matrix?) {
- transformation = p0
+ override fun setTextLines(charOffsets: IntArray?, baselines: IntArray?) {
+ textLinesCharOffsets = charOffsets?.toMutableList()
+ textLinesBaselines = baselines?.toMutableList()
}
- override fun setVisibility(p0: Int) {
- visibility = p0
+ override fun setTransformation(matrix: Matrix?) {
+ transformation = matrix
}
- override fun setWebDomain(p0: String?) {
- webDomain = p0
+ override fun setVisibility(visibility: Int) {
+ this.visibility = visibility
+ }
+
+ override fun setWebDomain(domain: String?) {
+ webDomain = domain
}
// Unimplemented methods.
@@ -316,7 +255,7 @@
TODO("not implemented")
}
- override fun asyncNewChild(p0: Int): ViewStructure {
+ override fun asyncNewChild(index: Int): ViewStructure {
TODO("not implemented")
}
@@ -328,42 +267,19 @@
TODO("not implemented")
}
- override fun newHtmlInfoBuilder(p0: String): HtmlInfo.Builder {
+ override fun newHtmlInfoBuilder(tagName: String): HtmlInfo.Builder {
TODO("not implemented")
}
- override fun setAutofillId(p0: AutofillId) {
+ override fun setChildCount(num: Int) {
TODO("not implemented")
}
- override fun setChildCount(p0: Int) {
+ override fun setLocaleList(localeList: LocaleList?) {
TODO("not implemented")
}
- override fun setLocaleList(p0: LocaleList?) {
+ override fun setTextStyle(size: Float, fgColor: Int, bgColor: Int, style: Int) {
TODO("not implemented")
}
-
- override fun setTextStyle(p0: Float, p1: Int, p2: Int, p3: Int) {
- TODO("not implemented")
- }
-}
-
-private fun Rect?.contentEquals(other: Rect?) =
- when {
- (other == null && this == null) -> true
- (other == null || this == null) -> false
- else ->
- other.left == left && other.right == right && other.bottom == bottom && other.top == top
- }
-
-/** Obtains a parcel and then recycles it correctly whether an exception is thrown or not. */
-private fun useParcel(block: (Parcel) -> Unit) {
- var parcel: Parcel? = null
- try {
- parcel = Parcel.obtain()
- block(parcel)
- } finally {
- parcel?.recycle()
- }
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/PerformAndroidAutofillManagerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/PerformAndroidAutofillManagerTest.kt
index a9adc07..c878248 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/PerformAndroidAutofillManagerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/PerformAndroidAutofillManagerTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.autofill
+import android.graphics.Rect
import android.os.Build
import android.text.InputType
import android.util.SparseArray
@@ -27,22 +28,19 @@
import androidx.annotation.RequiresApi
import androidx.autofill.HintConstants
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.input.TextFieldState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
-import androidx.compose.ui.ComposeUiFlags.isSemanticAutofillEnabled
+import androidx.compose.ui.ComposeUiFlags
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.node.RootForTest
-import androidx.compose.ui.platform.AndroidComposeView
-import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.Role
@@ -52,25 +50,29 @@
import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.semantics.maxTextLength
import androidx.compose.ui.semantics.onLongClick
+import androidx.compose.ui.semantics.password
+import androidx.compose.ui.semantics.requestFocus
import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.setText
import androidx.compose.ui.semantics.toggleableState
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.isEnabled
-import androidx.compose.ui.test.isFocusable
-import androidx.compose.ui.test.isFocused
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Ignore
import org.junit.After
+import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -82,14 +84,20 @@
// data types are supported.
class PerformAndroidAutofillManagerTest {
@get:Rule val rule = createAndroidComposeRule<TestActivity>()
- private lateinit var androidComposeView: AndroidComposeView
- private lateinit var composeView: View
-
private val height = 200.dp
private val width = 200.dp
private val contentTag = "content_tag"
+ @OptIn(ExperimentalComposeUiApi::class)
+ private val previousFlagValue = ComposeUiFlags.isSemanticAutofillEnabled
+
+ @Before
+ fun enableAutofill() {
+ @OptIn(ExperimentalComposeUiApi::class)
+ ComposeUiFlags.isSemanticAutofillEnabled = true
+ }
+
@After
fun teardown() {
val instrumentation = InstrumentationRegistry.getInstrumentation()
@@ -101,6 +109,8 @@
}
}
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ ComposeUiFlags.isSemanticAutofillEnabled = previousFlagValue
}
// The "filling" user journey consists of populating a viewStructure for the Autofill framework
@@ -116,27 +126,29 @@
@SdkSuppress(minSdkVersion = 26)
fun populateViewStructure_empty() {
// Arrange.
+ lateinit var view: View
val viewStructure: ViewStructure = FakeViewStructure()
+ rule.setContent { view = LocalView.current }
// Act.
- rule.setContentWithAutofillEnabled {
+ rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure.childCount).isEqualTo(0)
+ assertThat(viewStructure.childCount).isEqualTo(0)
}
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
- fun populateViewStructure_defaultValues_26() {
+ fun populateViewStructure_defaultValues() {
// Arrange.
+ lateinit var view: View
val viewStructure: ViewStructure = FakeViewStructure()
-
- // Act.
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Box(
Modifier
// TODO(333102566): for now we need this Autofill contentType to get the
@@ -149,70 +161,34 @@
)
}
- rule.runOnIdle {
- // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
- }
-
- // Assert.
- Truth.assertThat(viewStructure)
- .isEqualTo(
- FakeViewStructure().apply {
- children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
- setVisibility(View.VISIBLE)
- setLongClickable(false)
- setFocusable(false)
- setFocused(false)
- setEnabled(false)
- }
- )
- }
- )
- }
-
- @Test
- @SmallTest
- @SdkSuppress(minSdkVersion = 28)
- fun populateViewStructure_defaultValues_28() {
- // Arrange.
- val viewStructure: ViewStructure = FakeViewStructure()
-
// Act.
- rule.setContentWithAutofillEnabled {
- Box(
- Modifier
- // TODO(333102566): for now we need this Autofill contentType to get the
- // ViewStructure populated. Once Autofill is triggered for all semantics nodes
- // (not just ones related to Autofill) the semantics below will no longer be
- // necessary.
- .semantics { contentType = ContentType.Username }
- .size(height, width)
- .testTag(contentTag)
- )
- }
-
rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
FakeViewStructure().apply {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+ packageName = view.context.applicationInfo.packageName
+ bounds = Rect(0, 0, width.dpToPx(), height.dpToPx())
+ autofillId = view.autofillId
+ isEnabled = true
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
- setVisibility(View.VISIBLE)
- setLongClickable(false)
- setFocusable(false)
- setFocused(false)
- setEnabled(false)
- setMaxTextLength(-1)
+ FakeViewStructure().apply {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ packageName = view.context.applicationInfo.packageName
+ bounds = Rect(0, 0, width.dpToPx(), height.dpToPx())
+ autofillId = view.autofillId
+ isEnabled = true
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ visibility = View.VISIBLE
+ isLongClickable = false
+ isFocusable = false
+ isFocused = false
+ isEnabled = true
}
)
}
@@ -224,9 +200,10 @@
@SdkSuppress(minSdkVersion = 26)
fun populateViewStructure_contentType() {
// Arrange.
+ lateinit var view: View
val viewStructure: ViewStructure = FakeViewStructure()
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Box(
Modifier.semantics { contentType = ContentType.Username }
.size(height, width)
@@ -234,19 +211,21 @@
)
}
+ // Act.
rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
- FakeViewStructure().apply {
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
}
)
}
@@ -258,9 +237,10 @@
@SdkSuppress(minSdkVersion = 26)
fun populateViewStructure_contentDataType() {
// Arrange.
+ lateinit var view: View
val viewStructure: ViewStructure = FakeViewStructure()
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Box(
Modifier.semantics { contentDataType = ContentDataType.Text }
.size(height, width)
@@ -268,19 +248,21 @@
)
}
+ // Act.
rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
- FakeViewStructure().apply {
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
- setAutofillType(AUTOFILL_TYPE_TEXT)
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillType = AUTOFILL_TYPE_TEXT
}
)
}
@@ -292,9 +274,10 @@
@SdkSuppress(minSdkVersion = 26)
fun populateViewStructure_clickable() {
// Arrange.
+ lateinit var view: View
val viewStructure: ViewStructure = FakeViewStructure()
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Box(
Modifier.semantics { contentType = ContentType.Username }
.size(width, height)
@@ -303,21 +286,23 @@
)
}
+ // Act.
rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
- FakeViewStructure().apply {
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
- setClickable(true)
- setFocusable(true)
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ isClickable = true
+ isFocusable = true
}
)
}
@@ -329,9 +314,10 @@
@SdkSuppress(minSdkVersion = 26)
fun populateViewStructure_contentDescription() {
// Arrange.
+ lateinit var view: View
val viewStructure: ViewStructure = FakeViewStructure()
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Box(
Modifier.semantics {
contentType = ContentType.Username
@@ -342,20 +328,22 @@
)
}
+ // Act.
rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
- FakeViewStructure().apply {
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
- setContentDescription(contentTag)
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ contentDescription = contentTag
}
)
}
@@ -367,9 +355,10 @@
@SdkSuppress(minSdkVersion = 26)
fun populateViewStructure_role_tab() {
// Arrange.
+ lateinit var view: View
val viewStructure: ViewStructure = FakeViewStructure()
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Box(
Modifier.semantics { contentType = ContentType.Username }
.size(width, height)
@@ -385,23 +374,24 @@
)
}
+ // Act.
rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
- FakeViewStructure().apply {
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
- setClickable(true)
- setFocusable(true)
- setEnabled(true)
- setSelected(true)
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ isClickable = true
+ isFocusable = true
+ isSelected = true
}
)
}
@@ -411,11 +401,12 @@
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
- fun populateViewStructure_role_button() {
+ fun populateViewStructure_role_radioButton() {
// Arrange.
+ lateinit var view: View
val viewStructure: ViewStructure = FakeViewStructure()
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Box(
Modifier.semantics { contentType = ContentType.Username }
.size(width, height)
@@ -431,23 +422,129 @@
)
}
+ // Act.
rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
- FakeViewStructure().apply {
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
- setClickable(true)
- setFocusable(true)
- setEnabled(true)
- setSelected(true)
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ className = "android.widget.RadioButton"
+ isClickable = true
+ isFocusable = true
+ isCheckable = true
+ isChecked = true
+ isSelected = true
+ }
+ )
+ }
+ )
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 26)
+ fun populateViewStructure_role_dropdownList() {
+ // Arrange.
+ lateinit var view: View
+ val viewStructure: ViewStructure = FakeViewStructure()
+ rule.setContent {
+ view = LocalView.current
+ Box(
+ Modifier.semantics { contentType = ContentType.Username }
+ .size(width, height)
+ .selectable(
+ selected = true,
+ onClick = {},
+ enabled = true,
+ role = Role.DropdownList,
+ interactionSource = null,
+ indication = null
+ )
+ .testTag(contentTag)
+ )
+ }
+
+ // Act.
+ rule.runOnIdle {
+ // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
+ }
+
+ // Assert.
+ assertThat(viewStructure)
+ .isEqualTo(
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+ children.add(
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ className = "android.widget.Spinner"
+ isClickable = true
+ isFocusable = true
+ isCheckable = true
+ isChecked = true
+ isSelected = true
+ }
+ )
+ }
+ )
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 26)
+ fun populateViewStructure_role_valuePicker() {
+ // Arrange.
+ lateinit var view: View
+ val viewStructure: ViewStructure = FakeViewStructure()
+ rule.setContent {
+ view = LocalView.current
+ Box(
+ Modifier.semantics { contentType = ContentType.Username }
+ .size(width, height)
+ .selectable(
+ selected = true,
+ onClick = {},
+ enabled = true,
+ role = Role.ValuePicker,
+ interactionSource = null,
+ indication = null
+ )
+ .testTag(contentTag)
+ )
+ }
+
+ // Act.
+ rule.runOnIdle {
+ // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
+ }
+
+ // Assert.
+ assertThat(viewStructure)
+ .isEqualTo(
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+ children.add(
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ className = "android.widget.NumberPicker"
+ isClickable = true
+ isFocusable = true
+ isCheckable = true
+ isChecked = true
+ isSelected = true
}
)
}
@@ -459,9 +556,10 @@
@SdkSuppress(minSdkVersion = 26)
fun populateViewStructure_hideFromAccessibility() {
// Arrange.
+ lateinit var view: View
val viewStructure: ViewStructure = FakeViewStructure()
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Box(
Modifier.semantics {
contentType = ContentType.Username
@@ -472,35 +570,39 @@
)
}
+ // Act.
rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert that even if a component is unimportant for accessibility, it can still be
// accessed by autofill.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
- FakeViewStructure().apply {
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
- setVisibility(View.VISIBLE)
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ visibility = View.VISIBLE
}
)
}
)
}
+ @Ignore // TODO(b/383198004): Add support for notifyVisibilityChanged.
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
fun populateViewStructure_invisibility() {
// Arrange.
+ lateinit var view: View
val viewStructure: ViewStructure = FakeViewStructure()
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Box(
Modifier.alpha(0f)
.semantics { contentType = ContentType.Username }
@@ -509,34 +611,38 @@
)
}
+ // Act.
rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
- FakeViewStructure().apply {
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
- setVisibility(View.INVISIBLE)
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ visibility = View.INVISIBLE
}
)
}
)
}
+ @Ignore // TODO(b/383198004): Add support for notifyVisibilityChanged.
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
fun populateViewStructure_visibility() {
// Arrange.
+ lateinit var view: View
val viewStructure: ViewStructure = FakeViewStructure()
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Box(
Modifier.semantics { contentType = ContentType.Username }
.size(width, height)
@@ -544,34 +650,38 @@
)
}
+ // Act.
rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
- FakeViewStructure().apply {
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
- setVisibility(View.VISIBLE)
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ visibility = View.VISIBLE
}
)
}
)
}
+ @Ignore // TODO(b/383198004): Add support for notifyVisibilityChanged.
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
fun populateViewStructure_invisibility_alpha() {
// Arrange.
+ lateinit var view: View
val viewStructure: ViewStructure = FakeViewStructure()
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Box(
Modifier.semantics { contentType = ContentType.Username }
.alpha(0f) // this node should now be invisible
@@ -580,20 +690,22 @@
)
}
+ // Act.
rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
- FakeViewStructure().apply {
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
- setVisibility(View.INVISIBLE)
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ visibility = View.INVISIBLE
}
)
}
@@ -605,9 +717,10 @@
@SdkSuppress(minSdkVersion = 26)
fun populateViewStructure_longClickable() {
// Arrange.
+ lateinit var view: View
val viewStructure: ViewStructure = FakeViewStructure()
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Box(
Modifier.semantics {
contentType = ContentType.Username
@@ -618,20 +731,22 @@
)
}
+ // Act.
rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
- FakeViewStructure().apply {
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
- setLongClickable(true)
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ isLongClickable = true
}
)
}
@@ -641,37 +756,79 @@
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
- fun populateViewStructure_focused_focusable() {
+ fun populateViewStructure_focusable() {
// Arrange.
+ lateinit var view: View
val viewStructure: ViewStructure = FakeViewStructure()
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Box(
Modifier.semantics {
contentType = ContentType.Username
- isFocusable()
- isFocused()
+ requestFocus { true }
}
.size(width, height)
.testTag(contentTag)
)
}
+ // Act.
rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
- FakeViewStructure().apply {
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
- setFocusable(true)
- setFocused(true)
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ isFocusable = true
+ }
+ )
+ }
+ )
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 26)
+ fun populateViewStructure_focusable_focused() {
+ // Arrange.
+ lateinit var view: View
+ val viewStructure: ViewStructure = FakeViewStructure()
+ rule.setContent {
+ view = LocalView.current
+ Box(
+ Modifier.semantics { contentType = ContentType.Username }
+ .focusable()
+ .size(width, height)
+ .testTag(contentTag)
+ )
+ }
+ rule.onNodeWithTag(contentTag).requestFocus()
+
+ // Act.
+ rule.runOnIdle {
+ // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
+ }
+
+ // Assert.
+ assertThat(viewStructure)
+ .isEqualTo(
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+ children.add(
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ isFocusable = true
+ isFocused = true
}
)
}
@@ -683,9 +840,10 @@
@SdkSuppress(minSdkVersion = 26)
fun populateViewStructure_enabled() {
// Arrange.
+ lateinit var view: View
val viewStructure: ViewStructure = FakeViewStructure()
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Box(
Modifier.semantics {
contentType = ContentType.Username
@@ -696,20 +854,22 @@
)
}
+ // Act.
rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
- FakeViewStructure().apply {
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
- setEnabled(true)
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ isEnabled = true
}
)
}
@@ -721,9 +881,54 @@
@SdkSuppress(minSdkVersion = 28)
fun populateViewStructure_setMaxLength() {
// Arrange.
+ lateinit var view: View
val viewStructure: ViewStructure = FakeViewStructure()
+ rule.setContent {
+ view = LocalView.current
+ Box(
+ Modifier.semantics {
+ contentType = ContentType.Username
+ setText { true }
+ maxTextLength = 5
+ }
+ .size(width, height)
+ .testTag(contentTag)
+ )
+ }
- rule.setContentWithAutofillEnabled {
+ // Act.
+ rule.runOnIdle {
+ // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
+ }
+
+ // Assert.
+ assertThat(viewStructure)
+ .isEqualTo(
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+ children.add(
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillType = AUTOFILL_TYPE_TEXT
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ maxTextLength = 5
+ className = "android.widget.EditText"
+ }
+ )
+ }
+ )
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 28)
+ fun populateViewStructure_setMaxLength_notSetForNonTextItems() {
+ // Arrange.
+ lateinit var view: View
+ val viewStructure: ViewStructure = FakeViewStructure()
+ rule.setContent {
+ view = LocalView.current
Box(
Modifier.semantics {
contentType = ContentType.Username
@@ -734,20 +939,22 @@
)
}
+ // Act.
rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
- FakeViewStructure().apply {
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
- setMaxTextLength(5)
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ maxTextLength = -1
}
)
}
@@ -759,9 +966,10 @@
@SdkSuppress(minSdkVersion = 26)
fun populateViewStructure_checkable_unchecked() {
// Arrange.
+ lateinit var view: View
val viewStructure: ViewStructure = FakeViewStructure()
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Box(
Modifier.semantics {
contentType = ContentType.Username
@@ -772,22 +980,23 @@
)
}
+ // Act.
rule.runOnIdle {
- // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder.
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
- FakeViewStructure().apply {
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
- setAutofillType(View.AUTOFILL_TYPE_TOGGLE)
- setCheckable(true)
- setFocusable(true)
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ autofillType = View.AUTOFILL_TYPE_TOGGLE
+ isCheckable = true
}
)
}
@@ -799,9 +1008,10 @@
@SdkSuppress(minSdkVersion = 26)
fun populateViewStructure_checkable_checked() {
// Arrange.
+ lateinit var view: View
val viewStructure: ViewStructure = FakeViewStructure()
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Box(
Modifier.semantics {
contentType = ContentType.Username
@@ -812,23 +1022,24 @@
)
}
+ // Act.
rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
- FakeViewStructure().apply {
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
- setAutofillType(View.AUTOFILL_TYPE_TOGGLE)
- setCheckable(true)
- setChecked(true)
- setFocusable(true)
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ autofillType = View.AUTOFILL_TYPE_TOGGLE
+ isCheckable = true
+ isChecked = true
}
)
}
@@ -838,44 +1049,89 @@
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
- fun populateViewStructure_usernameChild() {
+ fun populateViewStructure_checkable() {
// Arrange.
- val viewStructure = FakeViewStructure()
+ lateinit var view: View
+ val viewStructure: ViewStructure = FakeViewStructure()
+ rule.setContent {
+ view = LocalView.current
+ Box(
+ Modifier.semantics { contentType = ContentType.Username }
+ .toggleable(true) {}
+ .size(width, height)
+ .testTag(contentTag)
+ )
+ }
- rule.setContentWithAutofillEnabled {
+ // Act.
+ rule.runOnIdle {
+ // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
+ }
+
+ // Assert.
+ assertThat(viewStructure)
+ .isEqualTo(
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+ children.add(
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ autofillType = View.AUTOFILL_TYPE_TOGGLE
+ isCheckable = true
+ isChecked = true
+ isFocusable = true
+ isClickable = true
+ }
+ )
+ }
+ )
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 26)
+ fun populateViewStructure_username_empty() {
+ // Arrange.
+ lateinit var view: View
+ val viewStructure = FakeViewStructure()
+ rule.setContent {
+ view = LocalView.current
Column {
BasicTextField(
state = remember { TextFieldState() },
modifier =
Modifier.semantics { contentType = ContentType.Username }
+ .size(height, width)
.testTag(contentTag)
)
}
}
+ // Act.
rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
- FakeViewStructure().apply {
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
text = ""
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
- setAutofillType(AUTOFILL_TYPE_TEXT)
- setAutofillValue(AutofillValue.forText(""))
- setClassName(
- AndroidComposeViewAccessibilityDelegateCompat.TextFieldClassName
- )
- setClickable(true)
- setFocusable(true)
- setLongClickable(true)
- setVisibility(View.VISIBLE)
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ autofillType = AUTOFILL_TYPE_TEXT
+ autofillValue = AutofillValue.forText("")
+ className = "android.widget.EditText"
+ isClickable = true
+ isFocusable = true
+ isLongClickable = true
+ visibility = View.VISIBLE
}
)
}
@@ -885,11 +1141,61 @@
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
- fun populateViewStructure_passwordChild() {
+ fun populateViewStructure_username_specified() {
// Arrange.
+ lateinit var view: View
val viewStructure = FakeViewStructure()
+ rule.setContent {
+ view = LocalView.current
+ Column {
+ BasicTextField(
+ state = remember { TextFieldState("testUsername") },
+ modifier =
+ Modifier.semantics { contentType = ContentType.Username }
+ .size(height, width)
+ .testTag(contentTag)
+ )
+ }
+ }
- rule.setContentWithAutofillEnabled {
+ // Act.
+ rule.runOnIdle {
+ // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
+ }
+
+ // Assert.
+ assertThat(viewStructure)
+ .isEqualTo(
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+ children.add(
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ text = ""
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ autofillType = AUTOFILL_TYPE_TEXT
+ autofillValue = AutofillValue.forText("testUsername")
+ className = "android.widget.EditText"
+ isClickable = true
+ isFocusable = true
+ isLongClickable = true
+ visibility = View.VISIBLE
+ }
+ )
+ }
+ )
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 26)
+ fun populateViewStructure_password_empty() {
+ // Arrange.
+ lateinit var view: View
+ val viewStructure = FakeViewStructure()
+ rule.setContent {
+ view = LocalView.current
val passwordState = remember { TextFieldState() }
Column {
@@ -897,38 +1203,133 @@
state = passwordState,
modifier =
Modifier.semantics { contentType = ContentType.Password }
+ .size(height, width)
.testTag(contentTag)
)
}
}
+ // Act.
rule.runOnIdle {
// Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
- androidComposeView.onProvideAutofillVirtualStructure(viewStructure, 0)
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
}
// Assert.
- Truth.assertThat(viewStructure)
+ assertThat(viewStructure)
.isEqualTo(
- FakeViewStructure().apply {
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
- FakeViewStructure {
- virtualId = contentTag.semanticsId()
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
text = ""
- setAutofillHints(arrayOf(HintConstants.AUTOFILL_HINT_USERNAME))
- setAutofillType(AUTOFILL_TYPE_TEXT)
- setAutofillValue(AutofillValue.forText(""))
- setClassName(
- AndroidComposeViewAccessibilityDelegateCompat.TextFieldClassName
- )
- setClickable(true)
- setDataIsSensitive(true)
- setInputType(
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_PASSWORD)
+ autofillType = AUTOFILL_TYPE_TEXT
+ autofillValue = AutofillValue.forText("")
+ className = "android.widget.EditText"
+ isClickable = true
+ dataIsSensitive = true
+ inputType =
InputType.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
- )
- setFocusable(true)
- setLongClickable(true)
- setVisibility(View.VISIBLE)
+ isFocusable = true
+ isLongClickable = true
+ visibility = View.VISIBLE
+ }
+ )
+ }
+ )
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 26)
+ fun populateViewStructure_password_asContentType() {
+ // Arrange.
+ lateinit var view: View
+ val viewStructure = FakeViewStructure()
+ rule.setContent {
+ view = LocalView.current
+ Column {
+ BasicTextField(
+ state = remember { TextFieldState("testPassword") },
+ modifier =
+ Modifier.semantics { contentType = ContentType.Password }
+ .size(height, width)
+ .testTag(contentTag)
+ )
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
+ }
+
+ // Assert.
+ assertThat(viewStructure)
+ .isEqualTo(
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+ children.add(
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ text = ""
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_PASSWORD)
+ autofillType = AUTOFILL_TYPE_TEXT
+ autofillValue = AutofillValue.forText("testPassword")
+ className = "android.widget.EditText"
+ isClickable = true
+ dataIsSensitive = true
+ inputType =
+ InputType.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
+ isFocusable = true
+ isLongClickable = true
+ visibility = View.VISIBLE
+ }
+ )
+ }
+ )
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 26)
+ fun populateViewStructure_password_asSemanticProperty() {
+ // Arrange.
+ lateinit var view: View
+ val viewStructure = FakeViewStructure()
+
+ rule.setContent {
+ view = LocalView.current
+ Box(
+ modifier =
+ Modifier.semantics {
+ contentType = ContentType.Username
+ password()
+ }
+ .size(height, width)
+ .testTag(contentTag)
+ )
+ }
+
+ // Act.
+ rule.runOnIdle {
+ // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
+ }
+
+ // Assert.
+ assertThat(viewStructure)
+ .isEqualTo(
+ ViewStructure(view) {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+ children.add(
+ ViewStructure(view) {
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ dataIsSensitive = true
}
)
}
@@ -943,13 +1344,12 @@
@SmallTest
@SdkSuppress(minSdkVersion = 26)
fun performAutofill_credentials_customBTF() {
- val expectedUsername = "test_username"
- val expectedPassword = "test_password1111"
-
+ // Arrange.
+ lateinit var view: View
val usernameTag = "username_tag"
val passwordTag = "password_tag"
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Column {
BasicTextField(
state = remember { TextFieldState() },
@@ -966,40 +1366,43 @@
}
}
- val autofillValues =
- SparseArray<AutofillValue>().apply {
- append(usernameTag.semanticsId(), AutofillValue.forText(expectedUsername))
- append(passwordTag.semanticsId(), AutofillValue.forText(expectedPassword))
- }
+ // Act.
+ val usernameId = rule.onNodeWithTag(usernameTag).semanticsId()
+ val passwordId = rule.onNodeWithTag(passwordTag).semanticsId()
+ rule.runOnIdle {
+ view.autofill(
+ SparseArray<AutofillValue>().apply {
+ append(usernameId, AutofillValue.forText("testUsername"))
+ append(passwordId, AutofillValue.forText("testPassword"))
+ }
+ )
+ }
- rule.runOnIdle { androidComposeView.autofill(autofillValues) }
-
- rule.onNodeWithTag(usernameTag).assertTextEquals(expectedUsername)
- rule.onNodeWithTag(passwordTag).assertTextEquals(expectedPassword)
+ // Assert,
+ rule.onNodeWithTag(usernameTag).assertTextEquals("testUsername")
+ rule.onNodeWithTag(passwordTag).assertTextEquals("testPassword")
}
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
fun performAutofill_payment_customBTF() {
- val expectedCreditCardNumber = "123 456 789"
- val expectedSecurityCode = "123"
-
+ // Arrange.
+ lateinit var view: View
val creditCardTag = "credit_card_tag"
val securityCodeTag = "security_code_tag"
- rule.setContentWithAutofillEnabled {
- val creditCardInput = remember { TextFieldState() }
- val securityCodeInput = remember { TextFieldState() }
+ rule.setContent {
+ view = LocalView.current
Column {
BasicTextField(
- state = creditCardInput,
+ state = remember { TextFieldState() },
modifier =
Modifier.semantics { contentType = ContentType.CreditCardNumber }
.testTag(creditCardTag)
)
BasicTextField(
- state = securityCodeInput,
+ state = remember { TextFieldState() },
modifier =
Modifier.semantics { contentType = ContentType.CreditCardSecurityCode }
.testTag(securityCodeTag)
@@ -1007,50 +1410,38 @@
}
}
- val autofillValues =
- SparseArray<AutofillValue>().apply {
- append(creditCardTag.semanticsId(), AutofillValue.forText(expectedCreditCardNumber))
- append(securityCodeTag.semanticsId(), AutofillValue.forText(expectedSecurityCode))
- }
+ // Act.
+ val creditCardId = rule.onNodeWithTag(creditCardTag).semanticsId()
+ val securityCodeId = rule.onNodeWithTag(securityCodeTag).semanticsId()
+ rule.runOnIdle {
+ view.autofill(
+ SparseArray<AutofillValue>().apply {
+ append(creditCardId, AutofillValue.forText("0123 4567 8910 1112"))
+ append(securityCodeId, AutofillValue.forText("123"))
+ }
+ )
+ }
- rule.runOnIdle { androidComposeView.autofill(autofillValues) }
-
- rule.onNodeWithTag(creditCardTag).assertTextEquals(expectedCreditCardNumber)
- rule.onNodeWithTag(securityCodeTag).assertTextEquals(expectedSecurityCode)
+ // Assert.
+ rule.onNodeWithTag(creditCardTag).assertTextEquals("0123 4567 8910 1112")
+ rule.onNodeWithTag(securityCodeTag).assertTextEquals("123")
}
// ============================================================================================
// Helper functions
// ============================================================================================
-
- private fun String.semanticsId() = rule.onNodeWithTag(this).fetchSemanticsNode().id
-
private fun Dp.dpToPx() = with(rule.density) { [email protected]() }
- private inline fun FakeViewStructure(block: FakeViewStructure.() -> Unit): FakeViewStructure {
- return FakeViewStructure()
- .apply {
- packageName = composeView.context.applicationInfo.packageName
- setDimens(0, 0, 0, 0, width.dpToPx(), height.dpToPx())
- }
- .apply(block)
- }
-
- @OptIn(ExperimentalComposeUiApi::class)
- private fun ComposeContentTestRule.setContentWithAutofillEnabled(
- content: @Composable () -> Unit
- ) {
- setContent {
- androidComposeView = LocalView.current as AndroidComposeView
- androidComposeView._autofillManager?.currentSemanticsNodesInvalidated = true
- isSemanticAutofillEnabled = true
-
- composeView = LocalView.current
- LaunchedEffect(Unit) {
- // Make sure the delay between batches of events is set to zero.
- (composeView as RootForTest).setAccessibilityEventBatchIntervalMillis(0L)
- }
- content()
+ private inline fun ViewStructure(
+ view: View,
+ block: FakeViewStructure.() -> Unit
+ ): FakeViewStructure {
+ return FakeViewStructure().apply {
+ packageName = view.context.applicationInfo.packageName
+ bounds = Rect(0, 0, width.dpToPx(), height.dpToPx())
+ autofillId = view.autofillId
+ isEnabled = true
+ block()
}
}
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/TextFieldStateSemanticAutofillTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/TextFieldStateSemanticAutofillTest.kt
index d00795c..c53af64 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/TextFieldStateSemanticAutofillTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/TextFieldStateSemanticAutofillTest.kt
@@ -25,26 +25,23 @@
import androidx.compose.foundation.text.BasicSecureTextField
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.input.TextFieldState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
-import androidx.compose.ui.ComposeUiFlags.isSemanticAutofillEnabled
+import androidx.compose.ui.ComposeUiFlags
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.node.RootForTest
-import androidx.compose.ui.platform.AndroidComposeView
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.contentType
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.assertTextEquals
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
+import org.junit.After
+import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -54,8 +51,21 @@
@RequiresApi(Build.VERSION_CODES.O)
class TextFieldStateSemanticAutofillTest {
@get:Rule val rule = createAndroidComposeRule<TestActivity>()
- private lateinit var androidComposeView: AndroidComposeView
- private lateinit var composeView: View
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ private val previousFlagValue = ComposeUiFlags.isSemanticAutofillEnabled
+
+ @Before
+ fun enableAutofill() {
+ @OptIn(ExperimentalComposeUiApi::class)
+ ComposeUiFlags.isSemanticAutofillEnabled = true
+ }
+
+ @After
+ fun disableAutofill() {
+ @OptIn(ExperimentalComposeUiApi::class)
+ ComposeUiFlags.isSemanticAutofillEnabled = previousFlagValue
+ }
// ============================================================================================
// Tests to verify BasicTextField populating and filling.
@@ -64,13 +74,12 @@
@Test
@SmallTest
fun performAutofill_credentials_BTF() {
- val expectedUsername = "test_username"
- val expectedPassword = "test_password1111"
-
+ // Arrange.
+ lateinit var view: View
val usernameTag = "username_tag"
val passwordTag = "password_tag"
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Column {
BasicTextField(
state = remember { TextFieldState() },
@@ -89,25 +98,31 @@
}
}
- val autofillValues =
- SparseArray<AutofillValue>().apply {
- append(usernameTag.semanticsId(), AutofillValue.forText(expectedUsername))
- append(passwordTag.semanticsId(), AutofillValue.forText(expectedPassword))
- }
+ // Act.
+ val usernameId = rule.onNodeWithTag(usernameTag).semanticsId()
+ val passwordId = rule.onNodeWithTag(passwordTag).semanticsId()
+ rule.runOnIdle {
+ view.autofill(
+ SparseArray<AutofillValue>().apply {
+ append(usernameId, AutofillValue.forText("testUsername"))
+ append(passwordId, AutofillValue.forText("testPassword"))
+ }
+ )
+ }
- rule.runOnIdle { androidComposeView.autofill(autofillValues) }
-
- rule.onNodeWithTag(usernameTag).assertTextEquals(expectedUsername)
- rule.onNodeWithTag(passwordTag).assertTextEquals(expectedPassword)
+ // Assert.
+ rule.onNodeWithTag(usernameTag).assertTextEquals("testUsername")
+ rule.onNodeWithTag(passwordTag).assertTextEquals("testPassword")
}
@Test
@SmallTest
fun performAutofill_credentials_BSTF() {
- val expectedUsername = "test_username"
+ // Arrange.
+ lateinit var view: View
val usernameTag = "username_tag"
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Column {
BasicSecureTextField(
state = remember { TextFieldState() },
@@ -119,39 +134,19 @@
}
}
- val autofillValues =
- SparseArray<AutofillValue>().apply {
- append(usernameTag.semanticsId(), AutofillValue.forText(expectedUsername))
- }
+ // Act.
+ val usernameId = rule.onNodeWithTag(usernameTag).semanticsId()
+ rule.runOnIdle {
+ view.autofill(
+ SparseArray<AutofillValue>().apply {
+ append(usernameId, AutofillValue.forText("testUsername"))
+ }
+ )
+ }
- rule.runOnIdle { androidComposeView.autofill(autofillValues) }
-
- rule.onNodeWithTag(usernameTag).assertTextEquals(expectedUsername)
+ // Assert.
+ rule.onNodeWithTag(usernameTag).assertTextEquals("testUsername")
}
// TODO(mnuzen): Mat3 dependencies are pinned, will add Autofill tests in Material3 module
-
- // ============================================================================================
- // Helper functions
- // ============================================================================================
-
- private fun String.semanticsId() = rule.onNodeWithTag(this).fetchSemanticsNode().id
-
- @OptIn(ExperimentalComposeUiApi::class)
- private fun ComposeContentTestRule.setContentWithAutofillEnabled(
- content: @Composable () -> Unit
- ) {
- setContent {
- androidComposeView = LocalView.current as AndroidComposeView
- androidComposeView._autofillManager?.currentSemanticsNodesInvalidated = true
- isSemanticAutofillEnabled = true
-
- composeView = LocalView.current
- LaunchedEffect(Unit) {
- // Make sure the delay between batches of events is set to zero.
- (composeView as RootForTest).setAccessibilityEventBatchIntervalMillis(0L)
- }
- content()
- }
- }
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/TextFieldsSemanticAutofillTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/TextFieldsSemanticAutofillTest.kt
index 75bc772..1b5a6e5 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/TextFieldsSemanticAutofillTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/TextFieldsSemanticAutofillTest.kt
@@ -27,20 +27,16 @@
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.TextField
-import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.testutils.assertContainsColor
import androidx.compose.testutils.assertDoesNotContainColor
-import androidx.compose.ui.ComposeUiFlags.isSemanticAutofillEnabled
+import androidx.compose.ui.ComposeUiFlags
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.node.RootForTest
-import androidx.compose.ui.platform.AndroidComposeView
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.contentType
@@ -48,13 +44,14 @@
import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import junit.framework.TestCase.assertEquals
+import org.junit.After
+import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -64,9 +61,21 @@
@RequiresApi(Build.VERSION_CODES.O)
class TextFieldsSemanticAutofillTest {
@get:Rule val rule = createAndroidComposeRule<TestActivity>()
- private lateinit var androidComposeView: AndroidComposeView
- private lateinit var composeView: View
- private var autofillHighlight: Color? = null
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ private val previousFlagValue = ComposeUiFlags.isSemanticAutofillEnabled
+
+ @Before
+ fun enableAutofill() {
+ @OptIn(ExperimentalComposeUiApi::class)
+ ComposeUiFlags.isSemanticAutofillEnabled = true
+ }
+
+ @After
+ fun disableAutofill() {
+ @OptIn(ExperimentalComposeUiApi::class)
+ ComposeUiFlags.isSemanticAutofillEnabled = previousFlagValue
+ }
// ============================================================================================
// Tests to verify legacy TextField populating and filling.
@@ -75,16 +84,15 @@
@Test
@SmallTest
fun performAutofill_credentials_BTF() {
- val expectedUsername = "test_username"
- val expectedPassword = "test_password1111"
-
+ // Arrange.
+ lateinit var view: View
val usernameTag = "username_tag"
val passwordTag = "password_tag"
-
var usernameInput by mutableStateOf("")
var passwordInput by mutableStateOf("")
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Column {
BasicTextField(
value = usernameInput,
@@ -105,16 +113,21 @@
}
}
- val autofillValues =
- SparseArray<AutofillValue>().apply {
- append(usernameTag.semanticsId(), AutofillValue.forText(expectedUsername))
- append(passwordTag.semanticsId(), AutofillValue.forText(expectedPassword))
- }
+ // Act.
+ val usernameId = rule.onNodeWithTag(usernameTag).semanticsId()
+ val passwordId = rule.onNodeWithTag(passwordTag).semanticsId()
+ rule.runOnIdle {
+ view.autofill(
+ SparseArray<AutofillValue>().apply {
+ append(usernameId, AutofillValue.forText("testUsername"))
+ append(passwordId, AutofillValue.forText("testPassword"))
+ }
+ )
+ }
- rule.runOnIdle { androidComposeView.autofill(autofillValues) }
-
- rule.onNodeWithTag(usernameTag).assertTextEquals(expectedUsername)
- rule.onNodeWithTag(passwordTag).assertTextEquals(expectedPassword)
+ // Assert
+ rule.onNodeWithTag(usernameTag).assertTextEquals("testUsername")
+ rule.onNodeWithTag(passwordTag).assertTextEquals("testPassword")
}
// ============================================================================================
@@ -124,11 +137,12 @@
@Test
@SmallTest
fun performAutofill_credentials_legacyTF() {
- val expectedUsername = "test_username"
+ // Arrange.
+ lateinit var view: View
val usernameTag = "username_tag"
var usernameInput by mutableStateOf("")
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Column {
TextField(
value = usernameInput,
@@ -142,24 +156,29 @@
}
}
- val autofillValues =
- SparseArray<AutofillValue>().apply {
- append(usernameTag.semanticsId(), AutofillValue.forText(expectedUsername))
- }
+ // Act.
+ val usernameId = rule.onNodeWithTag(usernameTag).semanticsId()
+ rule.runOnIdle {
+ view.autofill(
+ SparseArray<AutofillValue>().apply {
+ append(usernameId, AutofillValue.forText("testUsername"))
+ }
+ )
+ }
- rule.runOnIdle { androidComposeView.autofill(autofillValues) }
-
- assertEquals(usernameInput, expectedUsername)
+ // Assert.
+ assertEquals(usernameInput, "testUsername")
}
@Test
@SmallTest
fun performAutofill_credentials_outlinedTF() {
- val expectedUsername = "test_username"
+ // Arrange.
+ lateinit var view: View
val usernameTag = "username_tag"
var usernameInput by mutableStateOf("")
-
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
Column {
OutlinedTextField(
value = usernameInput,
@@ -173,26 +192,31 @@
}
}
- val autofillValues =
- SparseArray<AutofillValue>().apply {
- append(usernameTag.semanticsId(), AutofillValue.forText(expectedUsername))
- }
+ // Act.
+ val usernameId = rule.onNodeWithTag(usernameTag).semanticsId()
+ rule.runOnIdle {
+ view.autofill(
+ SparseArray<AutofillValue>().apply {
+ append(usernameId, AutofillValue.forText("testUsername"))
+ }
+ )
+ }
- rule.runOnIdle { androidComposeView.autofill(autofillValues) }
-
- assertEquals(usernameInput, expectedUsername)
+ // Assert.
+ assertEquals(usernameInput, "testUsername")
}
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 26)
fun performAutofill_customHighlight_legacyTF() {
- val expectedUsername = "test_username"
+ lateinit var view: View
val usernameTag = "username_tag"
var usernameInput by mutableStateOf("")
val customHighlightColor = Color.Red
- rule.setContentWithAutofillEnabled {
+ rule.setContent {
+ view = LocalView.current
CompositionLocalProvider(LocalAutofillHighlightColor provides customHighlightColor) {
Column {
TextField(
@@ -208,46 +232,23 @@
}
}
- val autofillValues =
- SparseArray<AutofillValue>().apply {
- append(usernameTag.semanticsId(), AutofillValue.forText(expectedUsername))
- }
-
// Custom autofill highlight color should not appear prior to autofill being performed
rule
.onNodeWithTag(usernameTag)
.captureToImage()
.assertDoesNotContainColor(customHighlightColor)
- rule.runOnIdle { androidComposeView.autofill(autofillValues) }
+ val usernameId = rule.onNodeWithTag(usernameTag).semanticsId()
+ rule.runOnIdle {
+ view.autofill(
+ SparseArray<AutofillValue>().apply {
+ append(usernameId, AutofillValue.forText("testUsername"))
+ }
+ )
+ }
rule.waitForIdle()
// Custom autofill highlight color should now appear
rule.onNodeWithTag(usernameTag).captureToImage().assertContainsColor(customHighlightColor)
}
-
- // ============================================================================================
- // Helper functions
- // ============================================================================================
-
- private fun String.semanticsId() = rule.onNodeWithTag(this).fetchSemanticsNode().id
-
- @OptIn(ExperimentalComposeUiApi::class)
- private fun ComposeContentTestRule.setContentWithAutofillEnabled(
- content: @Composable () -> Unit
- ) {
- setContent {
- androidComposeView = LocalView.current as AndroidComposeView
- androidComposeView._autofillManager?.currentSemanticsNodesInvalidated = true
- isSemanticAutofillEnabled = true
-
- composeView = LocalView.current
- autofillHighlight = LocalAutofillHighlightColor.current
- LaunchedEffect(Unit) {
- // Make sure the delay between batches of events is set to zero.
- (composeView as RootForTest).setAccessibilityEventBatchIntervalMillis(0L)
- }
- content()
- }
- }
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt
index baafca3..8d7bed4 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt
@@ -19,9 +19,7 @@
import android.content.Context
import android.graphics.Rect as AndroidRect
import android.view.View
-import android.widget.Button
import android.widget.EditText
-import android.widget.LinearLayout
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
@@ -35,8 +33,6 @@
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.material.Button
-import androidx.compose.material.Text
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -45,24 +41,14 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusDirection.Companion.Down
-import androidx.compose.ui.focus.FocusDirection.Companion.Left
-import androidx.compose.ui.focus.FocusDirection.Companion.Next
-import androidx.compose.ui.focus.FocusDirection.Companion.Previous
-import androidx.compose.ui.focus.FocusDirection.Companion.Right
-import androidx.compose.ui.focus.FocusDirection.Companion.Up
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.test.requestFocus
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.dp
@@ -225,268 +211,6 @@
assertThat(thirdFocused).isTrue()
}
- @Test
- fun moveFocusThroughUnfocusableComposeViewNext() {
- lateinit var topEditText: EditText
- lateinit var composeView: ComposeView
- lateinit var bottomEditText: EditText
- lateinit var focusManager: FocusManager
-
- rule.setContent {
- focusManager = LocalFocusManager.current
- AndroidView(
- modifier = Modifier.fillMaxSize(),
- factory = { context ->
- LinearLayout(context).also { linearLayout ->
- linearLayout.orientation = LinearLayout.VERTICAL
- EditText(context).also {
- linearLayout.addView(it)
- topEditText = it
- }
- ComposeView(context).also {
- it.setContent { Box(Modifier.size(10.dp)) }
- linearLayout.addView(it)
- composeView = it
- }
- EditText(context).also {
- linearLayout.addView(it)
- bottomEditText = it
- }
- }
- }
- )
- }
-
- rule.runOnIdle { topEditText.requestFocus() }
-
- rule.runOnIdle { focusManager.moveFocus(Next) }
-
- rule.runOnIdle {
- assertThat(topEditText.isFocused).isFalse()
- assertThat(composeView.isFocused).isFalse()
- assertThat(bottomEditText.isFocused).isTrue()
- }
- }
-
- @Test
- fun moveFocusThroughUnfocusableComposeViewDown() {
- lateinit var topEditText: EditText
- lateinit var composeView: ComposeView
- lateinit var bottomEditText: EditText
- lateinit var focusManager: FocusManager
-
- rule.setContent {
- focusManager = LocalFocusManager.current
- AndroidView(
- modifier = Modifier.fillMaxSize(),
- factory = { context ->
- LinearLayout(context).also { linearLayout ->
- linearLayout.orientation = LinearLayout.VERTICAL
- EditText(context).also {
- linearLayout.addView(it)
- topEditText = it
- }
- ComposeView(context).also {
- it.setContent { Box(Modifier.size(10.dp)) }
- linearLayout.addView(it)
- composeView = it
- }
- EditText(context).also {
- linearLayout.addView(it)
- bottomEditText = it
- }
- }
- }
- )
- }
-
- rule.runOnIdle { topEditText.requestFocus() }
-
- rule.runOnIdle { focusManager.moveFocus(Down) }
-
- rule.runOnIdle {
- assertThat(topEditText.isFocused).isFalse()
- assertThat(composeView.isFocused).isFalse()
- assertThat(bottomEditText.isFocused).isTrue()
- }
- }
-
- @Test
- fun focusBetweenComposeViews_NextPrevious() {
- lateinit var focusManager: FocusManager
-
- rule.setContent {
- focusManager = LocalFocusManager.current
- AndroidView(
- modifier = Modifier.fillMaxSize(),
- factory = { context ->
- LinearLayout(context).also { linearLayout ->
- linearLayout.orientation = LinearLayout.VERTICAL
- ComposeView(context).also {
- it.setContent {
- Box(
- Modifier.size(10.dp)
- .focusProperties { canFocus = true }
- .focusable()
- .testTag("button1")
- )
- }
- linearLayout.addView(it)
- }
- ComposeView(context).also {
- it.setContent {
- Box(
- Modifier.size(10.dp)
- .focusProperties { canFocus = true }
- .focusable()
- .testTag("button2")
- )
- }
- linearLayout.addView(it)
- }
- ComposeView(context).also {
- it.setContent {
- Box(
- Modifier.size(10.dp)
- .focusProperties { canFocus = true }
- .focusable()
- .testTag("button3")
- )
- }
- linearLayout.addView(it)
- }
- }
- }
- )
- }
- rule.onNodeWithTag("button1").requestFocus()
- rule.onNodeWithTag("button1").assertIsFocused()
- rule.runOnIdle { focusManager.moveFocus(Next) }
- rule.onNodeWithTag("button2").assertIsFocused()
- rule.runOnIdle { focusManager.moveFocus(Next) }
- rule.onNodeWithTag("button3").assertIsFocused()
- rule.runOnIdle { focusManager.moveFocus(Previous) }
- rule.onNodeWithTag("button2").assertIsFocused()
- rule.runOnIdle { focusManager.moveFocus(Previous) }
- rule.onNodeWithTag("button1").assertIsFocused()
- }
-
- @Test
- fun focusBetweenComposeViews_DownUp() {
- lateinit var focusManager: FocusManager
-
- rule.setContent {
- focusManager = LocalFocusManager.current
- AndroidView(
- modifier = Modifier.fillMaxSize(),
- factory = { context ->
- LinearLayout(context).also { linearLayout ->
- linearLayout.orientation = LinearLayout.VERTICAL
- ComposeView(context).also {
- it.setContent {
- Box(
- Modifier.size(10.dp)
- .focusProperties { canFocus = true }
- .focusable()
- .testTag("button1")
- )
- }
- linearLayout.addView(it)
- }
- ComposeView(context).also {
- it.setContent {
- Box(
- Modifier.size(10.dp)
- .focusProperties { canFocus = true }
- .focusable()
- .testTag("button2")
- )
- }
- linearLayout.addView(it)
- }
- ComposeView(context).also {
- it.setContent {
- Box(
- Modifier.size(10.dp)
- .focusProperties { canFocus = true }
- .focusable()
- .testTag("button3")
- )
- }
- linearLayout.addView(it)
- }
- }
- }
- )
- }
- rule.onNodeWithTag("button1").requestFocus()
- rule.onNodeWithTag("button1").assertIsFocused()
- rule.runOnIdle { focusManager.moveFocus(Down) }
- rule.onNodeWithTag("button2").assertIsFocused()
- rule.runOnIdle { focusManager.moveFocus(Down) }
- rule.onNodeWithTag("button3").assertIsFocused()
- rule.runOnIdle { focusManager.moveFocus(Up) }
- rule.onNodeWithTag("button2").assertIsFocused()
- rule.runOnIdle { focusManager.moveFocus(Up) }
- rule.onNodeWithTag("button1").assertIsFocused()
- }
-
- @Test
- fun requestFocusFromViewMovesToComposeView() {
- lateinit var androidButton1: Button
- lateinit var composeView: View
- val composeButton = FocusRequester()
- rule.setContent {
- composeView = LocalView.current
- Column(Modifier.fillMaxSize()) {
- Button(
- onClick = {},
- Modifier.testTag("button")
- .focusProperties { canFocus = true }
- .focusRequester(composeButton)
- ) {
- Text("Compose Button")
- }
- AndroidView(
- factory = { context ->
- LinearLayout(context).also { linearLayout ->
- linearLayout.orientation = LinearLayout.VERTICAL
- linearLayout.addView(
- Button(context).apply {
- setText("Android Button")
- isFocusableInTouchMode = true
- androidButton1 = this
- }
- )
- linearLayout.addView(
- Button(context).apply {
- setText("Android Button 2")
- isFocusableInTouchMode = true
- }
- )
- }
- }
- )
- }
- }
-
- for (direction in arrayOf(Left, Up, Right, Down, Next, Previous)) {
- rule.runOnIdle { androidButton1.requestFocus() }
-
- rule.runOnIdle {
- assertThat(androidButton1.isFocused).isTrue()
- composeButton.requestFocus(direction)
- }
-
- rule.onNodeWithTag("button").assertIsFocused()
-
- rule.runOnIdle {
- assertThat(composeView.isFocused).isTrue()
- assertThat(androidButton1.isFocused).isFalse()
- }
- }
- }
-
private fun View.getFocusedRect() =
AndroidRect().run {
rule.runOnIdle { getFocusedRect(this) }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
index 334d095..71f1d7e 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
@@ -248,8 +248,7 @@
// Assert.
rule.runOnIdle {
- // b/369256395 we must return true when we advertise that we're focusable
- assertThat(success).isTrue()
+ assertThat(success).isFalse()
assertThat(ownerView.isFocused).isFalse()
}
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnGlobalLayoutListenerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnGlobalLayoutListenerTest.kt
index 089021b..3ba5d10 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnGlobalLayoutListenerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnGlobalLayoutListenerTest.kt
@@ -42,7 +42,7 @@
import androidx.compose.ui.node.requireOwner
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.spatial.RectInfo
+import androidx.compose.ui.spatial.RelativeLayoutBounds
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertAll
@@ -513,8 +513,13 @@
}
}
- private fun Modifier.testGlobalLayoutListener(callback: (RectInfo) -> Unit) =
- this then OnGlobaLayoutListenerElement(throttleMs = 0, debounceMs = 0, callback = callback)
+ private fun Modifier.testGlobalLayoutListener(callback: (RelativeLayoutBounds) -> Unit) =
+ this then
+ OnGlobaLayoutListenerElement(
+ throttleMillis = 0,
+ debounceMillis = 0,
+ callback = callback
+ )
}
/**
@@ -555,28 +560,28 @@
}
private data class OnGlobaLayoutListenerElement(
- val throttleMs: Int,
- val debounceMs: Int,
- val callback: (RectInfo) -> Unit,
+ val throttleMillis: Long,
+ val debounceMillis: Long,
+ val callback: (RelativeLayoutBounds) -> Unit,
) : ModifierNodeElement<OnGlobalLayoutListenerNode>() {
override fun create(): OnGlobalLayoutListenerNode =
OnGlobalLayoutListenerNode(
- throttleMs = throttleMs,
- debounceMs = debounceMs,
+ throttleMillis = throttleMillis,
+ debounceMillis = debounceMillis,
callback = callback
)
override fun update(node: OnGlobalLayoutListenerNode) {
- node.throttleMs = throttleMs
- node.debounceMs = debounceMs
+ node.throttleMillis = throttleMillis
+ node.debounceMillis = debounceMillis
node.callback = callback
node.diposeAndRegister()
}
override fun InspectorInfo.inspectableProperties() {
name = "onLayoutCalculatorChanged"
- properties["throttleMs"] = throttleMs
- properties["debounceMs"] = debounceMs
+ properties["throttleMillis"] = throttleMillis
+ properties["debounceMillis"] = debounceMillis
properties["callback"] = callback
}
@@ -586,31 +591,31 @@
other as OnGlobaLayoutListenerElement
- if (throttleMs != other.throttleMs) return false
- if (debounceMs != other.debounceMs) return false
+ if (throttleMillis != other.throttleMillis) return false
+ if (debounceMillis != other.debounceMillis) return false
if (callback != other.callback) return false
return true
}
override fun hashCode(): Int {
- var result = throttleMs
- result = 31 * result + debounceMs
+ var result = throttleMillis.hashCode()
+ result = 31 * result + debounceMillis.hashCode()
result = 31 * result + callback.hashCode()
return result
}
}
private class OnGlobalLayoutListenerNode(
- var throttleMs: Int,
- var debounceMs: Int,
- var callback: (RectInfo) -> Unit,
+ var throttleMillis: Long,
+ var debounceMillis: Long,
+ var callback: (RelativeLayoutBounds) -> Unit,
) : Modifier.Node() {
var handle: DisposableHandle? = null
fun diposeAndRegister() {
handle?.dispose()
- handle = registerOnGlobalLayoutListener(throttleMs, debounceMs, callback)
+ handle = registerOnGlobalLayoutListener(throttleMillis, debounceMillis, callback)
}
override fun onAttach() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnGlobalRectChangedTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnGlobalRectChangedTest.kt
index 3ba9202..fd4df19 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnGlobalRectChangedTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnGlobalRectChangedTest.kt
@@ -48,7 +48,7 @@
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.spatial.RectInfo
+import androidx.compose.ui.spatial.RelativeLayoutBounds
import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.unit.IntOffset
@@ -92,14 +92,18 @@
minWidth = size,
minHeight = size,
modifier =
- Modifier.onRectChanged(0, 0) { wrap1Position = it.windowRect }
+ Modifier.onLayoutRectChanged(0, 0) {
+ wrap1Position = it.boundsInWindow
+ }
)
} else {
Wrap(
minWidth = size,
minHeight = size,
modifier =
- Modifier.onRectChanged(0, 0) { wrap2Position = it.windowRect }
+ Modifier.onLayoutRectChanged(0, 0) {
+ wrap2Position = it.boundsInWindow
+ }
)
}
}
@@ -128,7 +132,9 @@
minWidth = size,
minHeight = size,
modifier =
- Modifier.onRectChanged(0, 0) { realChildSize = it.rootRect.size.width }
+ Modifier.onLayoutRectChanged(0, 0) {
+ realChildSize = it.boundsInRoot.size.width
+ }
)
}
}
@@ -159,8 +165,8 @@
minWidth = 10,
minHeight = 10,
modifier =
- Modifier.onRectChanged(0, 0) { rect ->
- childGlobalPosition = rect.rootRect.offset()
+ Modifier.onLayoutRectChanged(0, 0) { rect ->
+ childGlobalPosition = rect.boundsInRoot.offset()
latch.countDown()
}
)
@@ -192,17 +198,19 @@
Wrap(
minWidth = 10,
minHeight = 10,
- modifier = Modifier.onRectChanged(0, 0) { wrap1OnPositionedCalled = true }
+ modifier =
+ Modifier.onLayoutRectChanged(0, 0) { wrap1OnPositionedCalled = true }
)
Wrap(
minWidth = 10,
minHeight = 10,
- modifier = Modifier.onRectChanged(0, 0) { wrap2OnPositionedCalled = true }
+ modifier =
+ Modifier.onLayoutRectChanged(0, 0) { wrap2OnPositionedCalled = true }
) {
Wrap(
minWidth = 10,
minHeight = 10,
- modifier = Modifier.onRectChanged(0, 0) { latch.countDown() }
+ modifier = Modifier.onLayoutRectChanged(0, 0) { latch.countDown() }
)
}
}
@@ -220,8 +228,8 @@
var lambda2Called = false
var layoutCalled = false
var placementCalled = false
- val lambda1: (RectInfo) -> Unit = { lambda1Called = true }
- val lambda2: (RectInfo) -> Unit = { lambda2Called = true }
+ val lambda1: (RelativeLayoutBounds) -> Unit = { lambda1Called = true }
+ val lambda2: (RelativeLayoutBounds) -> Unit = { lambda2Called = true }
val changeLambda = mutableStateOf(true)
@@ -240,7 +248,7 @@
modifier =
Modifier.then(layoutModifier)
.size(10.dp)
- .onRectChanged(0, 0, if (changeLambda.value) lambda1 else lambda2)
+ .onLayoutRectChanged(0, 0, if (changeLambda.value) lambda1 else lambda2)
)
}
@@ -268,13 +276,13 @@
@Test
fun callbacksAreCalledOnlyOnceWhenLambdaChangesAndLayoutChanges() {
var lambda1Called = false
- val lambda1: (RectInfo) -> Unit = {
+ val lambda1: (RelativeLayoutBounds) -> Unit = {
assert(!lambda1Called)
lambda1Called = true
}
var lambda2Called = false
- val lambda2: (RectInfo) -> Unit = {
+ val lambda2: (RelativeLayoutBounds) -> Unit = {
assert(!lambda2Called)
lambda2Called = true
}
@@ -285,7 +293,7 @@
Box(
modifier =
Modifier.size(size.value)
- .onRectChanged(0, 0, if (changeLambda.value) lambda1 else lambda2)
+ .onLayoutRectChanged(0, 0, if (changeLambda.value) lambda1 else lambda2)
)
}
@@ -309,13 +317,13 @@
@Test
fun callbacksAreCalledOnlyOnceWhenLayoutBelowItAndLambdaChanged() {
var lambda1Called = false
- val lambda1: (RectInfo) -> Unit = {
+ val lambda1: (RelativeLayoutBounds) -> Unit = {
assert(!lambda1Called)
lambda1Called = true
}
var lambda2Called = false
- val lambda2: (RectInfo) -> Unit = {
+ val lambda2: (RelativeLayoutBounds) -> Unit = {
assert(!lambda2Called)
lambda2Called = true
}
@@ -326,7 +334,7 @@
Box(
modifier =
Modifier.padding(10.dp)
- .onRectChanged(0, 0, if (changeLambda.value) lambda1 else lambda2)
+ .onLayoutRectChanged(0, 0, if (changeLambda.value) lambda1 else lambda2)
.padding(size.value)
.size(10.dp)
)
@@ -364,8 +372,8 @@
Layout(
{},
modifier =
- Modifier.onRectChanged(0, 0) {
- coordinates = it.windowRect
+ Modifier.onLayoutRectChanged(0, 0) {
+ coordinates = it.boundsInWindow
positionedLatch.countDown()
}
) { _, _ ->
@@ -403,8 +411,8 @@
{},
modifier =
Modifier.graphicsLayer { translationX = offsetX }
- .onRectChanged(0, 0) {
- coordinates = it.windowRect
+ .onLayoutRectChanged(0, 0) {
+ coordinates = it.boundsInWindow
positionedLatch.countDown()
}
) { _, _ ->
@@ -459,8 +467,8 @@
Layout(
{},
modifier =
- Modifier.onRectChanged(0, 0) {
- coordinates = it.windowRect
+ Modifier.onLayoutRectChanged(0, 0) {
+ coordinates = it.boundsInWindow
positionedLatch.countDown()
}
) { _, constraints ->
@@ -496,15 +504,15 @@
DelayedMeasure(50) {
Box(Modifier.requiredSize(25.toDp())) {
Box(
- Modifier.requiredSize(size.toDp()).onRectChanged(0, 0) {
- coordinates1 = it.rootRect
+ Modifier.requiredSize(size.toDp()).onLayoutRectChanged(0, 0) {
+ coordinates1 = it.boundsInRoot
}
)
}
Box(Modifier.requiredSize(25.toDp())) {
Box(
- Modifier.requiredSize(size.toDp()).onRectChanged(0, 0) {
- coordinates2 = it.rootRect
+ Modifier.requiredSize(size.toDp()).onLayoutRectChanged(0, 0) {
+ coordinates2 = it.boundsInRoot
}
)
}
@@ -541,12 +549,12 @@
composeView.setContent {
Box(
- Modifier.fillMaxSize().onRectChanged(0, 0) {
+ Modifier.fillMaxSize().onLayoutRectChanged(0, 0) {
val position = IntArray(2)
composeView.getLocationInWindow(position)
frameGlobalPosition = IntOffset(position[0], position[1])
- realGlobalPosition = it.windowRect.offset()
+ realGlobalPosition = it.boundsInWindow.offset()
positionedLatch.countDown()
}
@@ -572,12 +580,9 @@
with(LocalDensity.current) {
Box {
Box(
- Modifier.fillMaxSize().padding(start = left.value.toDp()).onRectChanged(
- 0,
- 0
- ) {
- realLeft = it.rootRect.left
- }
+ Modifier.fillMaxSize()
+ .padding(start = left.value.toDp())
+ .onLayoutRectChanged(0, 0) { realLeft = it.boundsInRoot.left }
)
}
}
@@ -603,8 +608,8 @@
Box(Modifier.requiredSize(10.toDp())) {
Box(Modifier.requiredSize(10.toDp())) {
Box(
- Modifier.onRectChanged(0, 0) {
- realLeft = it.rootRect.left
+ Modifier.onLayoutRectChanged(0, 0) {
+ realLeft = it.boundsInRoot.left
positionedLatch.countDown()
}
.requiredSize(10.toDp())
@@ -626,13 +631,16 @@
@Test
fun testLayerBoundsPositionInRotatedView() {
- var rect: RectInfo? = null
+ var rect: RelativeLayoutBounds? = null
var view: View? = null
var toggle by mutableStateOf(false)
rule.setContent {
view = LocalView.current
if (toggle) {
- FixedSize(30, Modifier.padding(10).onRectChanged(0, 0) { rect = it }) { /* no-op */
+ FixedSize(
+ 30,
+ Modifier.padding(10).onLayoutRectChanged(0, 0) { rect = it }
+ ) { /* no-op */
}
}
}
@@ -648,10 +656,10 @@
rule.runOnIdle {
val layoutCoordinates = rect!!
- assertEquals(IntOffset(10, 10), layoutCoordinates.rootRect.offset())
- assertEquals(IntRect(10, 10, 40, 40), layoutCoordinates.rootRect)
+ assertEquals(IntOffset(10, 10), layoutCoordinates.boundsInRoot.offset())
+ assertEquals(IntRect(10, 10, 40, 40), layoutCoordinates.boundsInRoot)
- val boundsInWindow = layoutCoordinates.windowRect
+ val boundsInWindow = layoutCoordinates.boundsInWindow
assertEquals(10f * sqrt(2f), boundsInWindow.top.toFloat(), 1f)
assertEquals(30f * sqrt(2f) / 2f, boundsInWindow.right.toFloat(), 1f)
assertEquals(-30f * sqrt(2f) / 2f, boundsInWindow.left.toFloat(), 1f)
@@ -668,8 +676,8 @@
Popup(alignment = alignment) {
FixedSize(
30,
- Modifier.padding(10).background(Color.Red).onRectChanged(0, 0) {
- coords = it.windowRect
+ Modifier.padding(10).background(Color.Red).onLayoutRectChanged(0, 0) {
+ coords = it.boundsInWindow
}
) { /* no-op */
}
@@ -699,11 +707,11 @@
rule.setContent {
Box(
Modifier.fillMaxSize()
- .onRectChanged(0, 0) { coords1 = it.windowRect }
+ .onLayoutRectChanged(0, 0) { coords1 = it.boundsInWindow }
.padding(2.dp)
- .onRectChanged(0, 0) { coords2 = it.windowRect }
+ .onLayoutRectChanged(0, 0) { coords2 = it.boundsInWindow }
.padding(3.dp)
- .onRectChanged(0, 0) { coords3 = it.windowRect }
+ .onLayoutRectChanged(0, 0) { coords3 = it.boundsInWindow }
)
}
@@ -719,18 +727,21 @@
@Test
@SmallTest
fun modifierIsReturningEqualObjectForTheSameLambda() {
- val lambda: (RectInfo) -> Unit = {}
- assertEquals(Modifier.onRectChanged(0, 0, lambda), Modifier.onRectChanged(0, 0, lambda))
+ val lambda: (RelativeLayoutBounds) -> Unit = {}
+ assertEquals(
+ Modifier.onLayoutRectChanged(0, 0, lambda),
+ Modifier.onLayoutRectChanged(0, 0, lambda)
+ )
}
@Test
@SmallTest
fun modifierIsReturningNotEqualObjectForDifferentLambdas() {
- val lambda1: (RectInfo) -> Unit = { print("foo") }
- val lambda2: (RectInfo) -> Unit = { print("bar") }
+ val lambda1: (RelativeLayoutBounds) -> Unit = { print("foo") }
+ val lambda2: (RelativeLayoutBounds) -> Unit = { print("bar") }
Assert.assertNotEquals(
- Modifier.onRectChanged(0, 0, lambda1),
- Modifier.onRectChanged(0, 0, lambda2)
+ Modifier.onLayoutRectChanged(0, 0, lambda1),
+ Modifier.onLayoutRectChanged(0, 0, lambda2)
)
}
@@ -748,16 +759,16 @@
Box(
Modifier.fillMaxSize()
.offset { offset }
- .onRectChanged(0, 0) {
+ .onLayoutRectChanged(0, 0) {
if (offset != IntOffset.Zero) {
- position = it.rootRect.offset()
+ position = it.boundsInRoot.offset()
}
}
)
Box(
Modifier.fillMaxSize()
.offset { offset }
- .onRectChanged(0, 0) {
+ .onLayoutRectChanged(0, 0) {
if (offset != IntOffset.Zero && !hasSent) {
hasSent = true
val now = SystemClock.uptimeMillis()
@@ -784,7 +795,7 @@
Box(
Modifier.fillMaxSize()
.offset { offset }
- .onRectChanged(0, 0) { position = it.rootRect.offset() }
+ .onLayoutRectChanged(0, 0) { position = it.boundsInRoot.offset() }
)
}
}
@@ -805,14 +816,14 @@
val modifier =
if (callbackPresent.value) {
// Remember lambdas to avoid triggering a node update when the lambda changes
- Modifier.onRectChanged(0, 0, remember { { positionCalled1Count++ } })
+ Modifier.onLayoutRectChanged(0, 0, remember { { positionCalled1Count++ } })
} else {
Modifier
}
Box(
Modifier
// Remember lambdas to avoid triggering a node update when the lambda changes
- .onRectChanged(0, 0, remember { { positionCalled2Count++ } })
+ .onLayoutRectChanged(0, 0, remember { { positionCalled2Count++ } })
.then(modifier)
.fillMaxSize()
)
@@ -838,7 +849,7 @@
fun occlusionCalculationOnRectChangedCallbacks() {
var box2Fraction by mutableStateOf(1f)
- var box0RectInfo: RectInfo? = null
+ var box0Bounds: RelativeLayoutBounds? = null
var box0Occlusions = emptyList<IntRect>()
var box1Occlusions = emptyList<IntRect>()
@@ -853,8 +864,8 @@
Modifier.fillMaxWidth()
.fillMaxHeight(0.5f)
.align(Alignment.TopStart)
- .onRectChanged(0, 0) { rectInfo ->
- box0RectInfo = rectInfo
+ .onLayoutRectChanged(0, 0) { rectInfo ->
+ box0Bounds = rectInfo
box0CallbackCount++
box0Occlusions = rectInfo.calculateOcclusions()
}
@@ -864,20 +875,19 @@
Modifier.fillMaxWidth()
.fillMaxHeight(0.7f)
.align(Alignment.BottomStart)
- .onRectChanged(0, 0) { rectInfo ->
+ .onLayoutRectChanged(0, 0) { rectInfo ->
box1CallbackCount++
box1Occlusions = rectInfo.calculateOcclusions()
}
)
Box(
// Should initially occlude both boxes
- Modifier.fillMaxSize(box2Fraction).align(Alignment.BottomStart).onRectChanged(
- 0,
- 0
- ) { rectInfo ->
- box2CallbackCount++
- box2Occlusions = rectInfo.calculateOcclusions()
- }
+ Modifier.fillMaxSize(box2Fraction)
+ .align(Alignment.BottomStart)
+ .onLayoutRectChanged(0, 0) { rectInfo ->
+ box2CallbackCount++
+ box2Occlusions = rectInfo.calculateOcclusions()
+ }
)
}
}
@@ -908,6 +918,6 @@
// Currently, it's possible to capture rectInfo and re-calculate occlusions
// The new calculation should reflect one less occluding box
- assertThat(box0RectInfo!!.calculateOcclusions().size).isEqualTo(1)
+ assertThat(box0Bounds!!.calculateOcclusions().size).isEqualTo(1)
}
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementReusableNodeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementReusableNodeTest.kt
new file mode 100644
index 0000000..e144b09
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementReusableNodeTest.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.compose.ui.layout
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.LayoutAwareModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.OnUnplacedModifierNode
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class PlacementReusableNodeTest {
+
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun onPlacedCalledOnReuseInsideLazyColumn() {
+ lateinit var density: Density
+ val items = 200
+ val visibleItems = 2
+ val itemSize = 50.dp
+ val invocations = arrayOf(0, 0)
+
+ // It's important to share lambda across all iterations
+ val placedCallback0: (LayoutCoordinates) -> Unit = { invocations[0] = invocations[0] + 1 }
+ val placedCallback1: (LayoutCoordinates) -> Unit = { invocations[1] = invocations[1] + 1 }
+ val scrollState = LazyListState()
+ rule.setContent {
+ density = LocalDensity.current
+ LazyColumn(Modifier.size(itemSize, itemSize * visibleItems), scrollState) {
+ items(items) {
+ Box(Modifier.size(itemSize).onPlaced(placedCallback0)) {
+ Box(Modifier.size(itemSize).onPlaced(placedCallback1))
+ }
+ }
+ }
+ }
+
+ var expectedInvocations = visibleItems
+ val delta = with(density) { (itemSize * visibleItems).toPx() }
+ repeat(items / visibleItems) {
+ rule.runOnIdle {
+ assertThat(invocations[0]).isAtLeast(expectedInvocations)
+ assertThat(invocations[1]).isAtLeast(expectedInvocations)
+
+ scrollState.dispatchRawDelta(delta)
+ expectedInvocations += visibleItems
+ }
+ }
+ }
+
+ @Test
+ fun placeMultiplatformInteropView() {
+ val showPlatformInterop = mutableStateOf(true)
+ var currentlyVisible = false
+ rule.setContent {
+ if (showPlatformInterop.value) {
+ TestMultiplatformInteropView(
+ onAddedToPlatformHierarchy = { currentlyVisible = true },
+ onRemovedFromPlatformHierarchy = { currentlyVisible = false },
+ modifier = Modifier.size(100.dp)
+ )
+ }
+ }
+
+ rule.runOnIdle { assertThat(currentlyVisible).isTrue() }
+
+ showPlatformInterop.value = false
+ rule.runOnIdle { assertThat(currentlyVisible).isFalse() }
+ }
+
+ @Test
+ fun placeMultiplatformInteropViewInsideLazyColumn() {
+ lateinit var density: Density
+
+ val currentlyVisible = mutableSetOf<Int>()
+ val scrollState = LazyListState()
+ rule.setContent {
+ density = LocalDensity.current
+ LazyColumn(Modifier.size(100.dp, 250.dp), scrollState) {
+ items(100) { index ->
+ TestMultiplatformInteropView(
+ onAddedToPlatformHierarchy = { currentlyVisible += index },
+ onRemovedFromPlatformHierarchy = { currentlyVisible -= index },
+ modifier = Modifier.size(100.dp)
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle { assertThat(currentlyVisible).containsExactly(0, 1, 2) }
+
+ rule.runOnIdle { scrollState.dispatchRawDelta(with(density) { 1010.dp.toPx() }) }
+ rule.runOnIdle { assertThat(currentlyVisible).containsExactly(10, 11, 12) }
+
+ rule.runOnIdle { scrollState.dispatchRawDelta(with(density) { -1010.dp.toPx() }) }
+ rule.runOnIdle { assertThat(currentlyVisible).containsExactly(0, 1, 2) }
+ }
+}
+
+/**
+ * This emulates multiplatform interop element that placed and drawn outside of Compose.
+ * onPlaced/onUnplaced callbacks are used to control their lifecycle in platform's views hierarchy.
+ */
+@Composable
+private fun TestMultiplatformInteropView(
+ onAddedToPlatformHierarchy: () -> Unit,
+ onRemovedFromPlatformHierarchy: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ modifier then
+ TrackInteropPlacementModifierElement(
+ onAddedToPlatformHierarchy = onAddedToPlatformHierarchy,
+ onRemovedFromPlatformHierarchy = onRemovedFromPlatformHierarchy
+ )
+ )
+}
+
+private data class TrackInteropPlacementModifierElement(
+ var onAddedToPlatformHierarchy: () -> Unit,
+ var onRemovedFromPlatformHierarchy: () -> Unit,
+) : ModifierNodeElement<TrackInteropPlacementModifierNode>() {
+ override fun create() =
+ TrackInteropPlacementModifierNode(
+ onAddedToPlatformHierarchy = onAddedToPlatformHierarchy,
+ onRemovedFromPlatformHierarchy = onRemovedFromPlatformHierarchy
+ )
+
+ override fun update(node: TrackInteropPlacementModifierNode) {
+ node.onAddedToPlatformHierarchy = onAddedToPlatformHierarchy
+ node.onRemovedFromPlatformHierarchy = onRemovedFromPlatformHierarchy
+ }
+}
+
+private class TrackInteropPlacementModifierNode(
+ var onAddedToPlatformHierarchy: () -> Unit,
+ var onRemovedFromPlatformHierarchy: () -> Unit,
+) : Modifier.Node(), LayoutAwareModifierNode, OnUnplacedModifierNode {
+ private var isPlaced = false
+
+ override fun onPlaced(coordinates: LayoutCoordinates) {
+ onAddedToPlatformHierarchy()
+ isPlaced = true
+ }
+
+ override fun onUnplaced() {
+ onRemovedFromPlatformHierarchy()
+ isPlaced = false
+ }
+
+ override fun onDetach() {
+ // TODO(b/309776096): Remove workaround for missing [onUnplaced]
+ // once it will be reliable implemented
+ if (isPlaced) {
+ onUnplaced()
+ }
+ super.onDetach()
+ }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/MergedSemanticsConfigurationTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/MergedSemanticsConfigurationTest.kt
new file mode 100644
index 0000000..6d87877
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/MergedSemanticsConfigurationTest.kt
@@ -0,0 +1,389 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.compose.ui.semantics
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.RootForTest
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.text.AnnotatedString
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.collections.listOf
+import kotlin.test.Test
+import org.junit.Rule
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MergedSemanticsConfigurationTest {
+
+ @get:Rule val rule = createComposeRule()
+
+ lateinit var semanticsOwner: SemanticsOwner
+
+ @Test
+ fun singleModifier() {
+ // Arrange.
+ rule.setTestContent {
+ Box(
+ Modifier.semantics {
+ testTag = "box"
+ text = AnnotatedString("1")
+ }
+ )
+ }
+
+ // Act.
+ val id = rule.onNodeWithTag("box").semanticsId()
+ val (unmerged, merged) =
+ with(checkNotNull(semanticsOwner[id])) {
+ Pair(semanticsConfiguration, mergedSemanticsConfiguration())
+ }
+
+ // Assert.
+ assertThat(unmerged?.getOrNull(SemanticsProperties.Text))
+ .isEqualTo(listOf(AnnotatedString("1")))
+ assertThat(merged).isEqualTo(unmerged)
+ }
+
+ @Test
+ fun multipleModifierOnSameNode() {
+ // Arrange.
+ rule.setTestContent {
+ Box(
+ Modifier.semantics {
+ testTag = "box"
+ text = AnnotatedString("1")
+ }
+ .semantics { text = AnnotatedString("2") }
+ )
+ }
+
+ // Act.
+ val id = rule.onNodeWithTag("box").semanticsId()
+ val (unmerged, merged) =
+ with(checkNotNull(semanticsOwner[id])) {
+ Pair(semanticsConfiguration, mergedSemanticsConfiguration())
+ }
+
+ // Assert.
+ assertThat(unmerged?.getOrNull(SemanticsProperties.Text))
+ .isEqualTo(listOf(AnnotatedString("1")))
+ assertThat(merged).isEqualTo(unmerged)
+ }
+
+ @Test
+ fun multipleModifierOnSameNode_mergingDescendants() {
+ // Arrange.
+ rule.setTestContent {
+ Box(
+ Modifier.semantics(mergeDescendants = true) {
+ testTag = "box"
+ text = AnnotatedString("1")
+ }
+ .semantics { text = AnnotatedString("2") }
+ )
+ }
+
+ // Act.
+ val id = rule.onNodeWithTag("box").semanticsId()
+ val (unmerged, merged) =
+ with(checkNotNull(semanticsOwner[id])) {
+ Pair(semanticsConfiguration, mergedSemanticsConfiguration())
+ }
+
+ // Assert.
+ assertThat(unmerged?.getOrNull(SemanticsProperties.Text))
+ .isEqualTo(listOf(AnnotatedString("1")))
+ assertThat(merged).isEqualTo(unmerged)
+ }
+
+ @Test
+ fun multipleLayoutNodes_default() {
+ // Arrange.
+ rule.setTestContent {
+ Box(
+ Modifier.semantics {
+ testTag = "box"
+ text = AnnotatedString("1")
+ }
+ ) {
+ Box(Modifier.semantics { text = AnnotatedString("2") })
+ }
+ }
+
+ // Act.
+ val id = rule.onNodeWithTag("box").semanticsId()
+ val (unmerged, merged) =
+ with(checkNotNull(semanticsOwner[id])) {
+ Pair(semanticsConfiguration, mergedSemanticsConfiguration())
+ }
+
+ // Assert.
+ assertThat(unmerged?.getOrNull(SemanticsProperties.Text))
+ .isEqualTo(listOf(AnnotatedString("1")))
+ assertThat(merged).isEqualTo(unmerged)
+ }
+
+ @Test
+ fun multipleLayoutNodes_isMergingDescendants_oneChild() {
+ // Arrange.
+ rule.setTestContent {
+ Box(
+ Modifier.semantics(mergeDescendants = true) {
+ testTag = "box"
+ text = AnnotatedString("1")
+ }
+ ) {
+ Box(Modifier.semantics { text = AnnotatedString("2") })
+ }
+ }
+
+ // Act.
+ val id = rule.onNodeWithTag("box").semanticsId()
+ val (unmerged, merged) =
+ with(checkNotNull(semanticsOwner[id])) {
+ Pair(semanticsConfiguration, mergedSemanticsConfiguration())
+ }
+
+ // Assert.
+ assertThat(unmerged?.getOrNull(SemanticsProperties.Text))
+ .isEqualTo(listOf(AnnotatedString("1")))
+ assertThat(merged?.getOrNull(SemanticsProperties.Text))
+ .isEqualTo(listOf(AnnotatedString("1"), AnnotatedString("2")))
+ }
+
+ @Test
+ fun multipleLayoutNodes_isMergingDescendants_twoChildren() {
+ // Arrange.
+ rule.setTestContent {
+ Row(
+ Modifier.semantics(mergeDescendants = true) {
+ testTag = "row"
+ text = AnnotatedString("1")
+ }
+ ) {
+ Box(Modifier.semantics { text = AnnotatedString("2.1") })
+ Box(Modifier.semantics { text = AnnotatedString("2.2") })
+ }
+ }
+
+ // Act.
+ val id = rule.onNodeWithTag("row").semanticsId()
+ val (unmerged, merged) =
+ with(checkNotNull(semanticsOwner[id])) {
+ Pair(semanticsConfiguration, mergedSemanticsConfiguration())
+ }
+
+ // Assert.
+ assertThat(unmerged?.getOrNull(SemanticsProperties.Text))
+ .isEqualTo(listOf(AnnotatedString("1")))
+ assertThat(merged?.getOrNull(SemanticsProperties.Text))
+ .isEqualTo(listOf(AnnotatedString("1"), AnnotatedString("2.2"), AnnotatedString("2.1")))
+ }
+
+ @Test
+ fun multipleLayoutNodes_isMergingDescendants_clearAndSetSemantics_twoChildren() {
+ // Arrange.
+ rule.setTestContent {
+ Row(
+ Modifier.semantics(mergeDescendants = true) {
+ testTag = "row"
+ text = AnnotatedString("1")
+ }
+ .clearAndSetSemantics {}
+ ) {
+ Box(Modifier.semantics { text = AnnotatedString("2.1") })
+ Box(Modifier.semantics { text = AnnotatedString("2.2") })
+ }
+ }
+
+ // Act.
+ val id = rule.onNodeWithTag("row").semanticsId()
+ val (unmerged, merged) =
+ with(checkNotNull(semanticsOwner[id])) {
+ Pair(semanticsConfiguration, mergedSemanticsConfiguration())
+ }
+
+ // Assert.
+ assertThat(unmerged?.getOrNull(SemanticsProperties.Text))
+ .isEqualTo(listOf(AnnotatedString("1")))
+ assertThat(merged?.getOrNull(SemanticsProperties.Text))
+ .isEqualTo(listOf(AnnotatedString("1")))
+ }
+
+ @Test
+ fun multipleLayoutNodes_isClearingSemantics_twoChildren() {
+ // Arrange.
+ rule.setTestContent {
+ Row(
+ Modifier.clearAndSetSemantics {
+ testTag = "row"
+ text = AnnotatedString("1")
+ }
+ ) {
+ Box(Modifier.semantics { text = AnnotatedString("2.1") })
+ Box(Modifier.semantics { text = AnnotatedString("2.2") })
+ }
+ }
+
+ // Act.
+ val id = rule.onNodeWithTag("row").semanticsId()
+ val (unmerged, merged) =
+ with(checkNotNull(semanticsOwner[id])) {
+ Pair(semanticsConfiguration, mergedSemanticsConfiguration())
+ }
+
+ // Assert.
+ assertThat(unmerged?.getOrNull(SemanticsProperties.Text))
+ .isEqualTo(listOf(AnnotatedString("1")))
+ assertThat(merged?.getOrNull(SemanticsProperties.Text))
+ .isEqualTo(listOf(AnnotatedString("1")))
+ }
+
+ @Test
+ fun deepHierarchy() {
+ // Arrange.
+ rule.setTestContent {
+ Row(
+ Modifier.semantics(mergeDescendants = true) {
+ testTag = "row"
+ text = AnnotatedString("1")
+ }
+ ) {
+ Box(Modifier.semantics { text = AnnotatedString("2.1") })
+ Column(Modifier.semantics { text = AnnotatedString("2.2") }) {
+ Box(Modifier.semantics { text = AnnotatedString("3.1") })
+ Box(Modifier.semantics { text = AnnotatedString("3.2") })
+ }
+ Box(Modifier.semantics { text = AnnotatedString("2.3") })
+ }
+ }
+
+ // Act.
+ val id = rule.onNodeWithTag("row").semanticsId()
+ val (unmerged, merged) =
+ with(checkNotNull(semanticsOwner[id])) {
+ Pair(semanticsConfiguration, mergedSemanticsConfiguration())
+ }
+
+ // Assert.
+ assertThat(unmerged?.getOrNull(SemanticsProperties.Text))
+ .isEqualTo(listOf(AnnotatedString("1")))
+ assertThat(merged?.getOrNull(SemanticsProperties.Text))
+ .isEqualTo(
+ listOf(
+ AnnotatedString("1"),
+ AnnotatedString("2.3"),
+ AnnotatedString("2.2"),
+ AnnotatedString("3.2"),
+ AnnotatedString("3.1"),
+ AnnotatedString("2.1")
+ )
+ )
+ }
+
+ @Test
+ fun doesNotMergeMergedItems() {
+ // Arrange.
+ rule.setTestContent {
+ Row(
+ Modifier.semantics(mergeDescendants = true) {
+ testTag = "row"
+ text = AnnotatedString("1")
+ }
+ ) {
+ Box(Modifier.semantics { text = AnnotatedString("2.1") })
+ Column(
+ Modifier.semantics(mergeDescendants = true) { text = AnnotatedString("2.2") }
+ ) {
+ Box(Modifier.semantics { text = AnnotatedString("3.1") })
+ Box(Modifier.semantics { text = AnnotatedString("3.2") })
+ }
+ Box(Modifier.semantics { text = AnnotatedString("2.3") })
+ }
+ }
+
+ // Act.
+ val id = rule.onNodeWithTag("row").semanticsId()
+ val (unmerged, merged) =
+ with(checkNotNull(semanticsOwner[id])) {
+ Pair(semanticsConfiguration, mergedSemanticsConfiguration())
+ }
+
+ // Assert.
+ assertThat(unmerged?.getOrNull(SemanticsProperties.Text))
+ .isEqualTo(listOf(AnnotatedString("1")))
+ assertThat(merged?.getOrNull(SemanticsProperties.Text))
+ .isEqualTo(listOf(AnnotatedString("1"), AnnotatedString("2.3"), AnnotatedString("2.1")))
+ }
+
+ @Test
+ fun doesNotIncludeChildrenThatAreCleared() {
+ // Arrange.
+ rule.setTestContent {
+ Row(
+ Modifier.semantics(mergeDescendants = true) {
+ testTag = "row"
+ text = AnnotatedString("1")
+ }
+ ) {
+ Box(Modifier.semantics { text = AnnotatedString("2.1") })
+ Column(Modifier.clearAndSetSemantics { text = AnnotatedString("2.2") }) {
+ Box(Modifier.semantics { text = AnnotatedString("3.1") })
+ Box(Modifier.semantics { text = AnnotatedString("3.2") })
+ }
+ Box(Modifier.semantics { text = AnnotatedString("2.3") })
+ }
+ }
+
+ // Act.
+ val id = rule.onNodeWithTag("row").semanticsId()
+ val (unmerged, merged) =
+ with(checkNotNull(semanticsOwner[id])) {
+ Pair(semanticsConfiguration, mergedSemanticsConfiguration())
+ }
+
+ // Assert.
+ assertThat(unmerged?.getOrNull(SemanticsProperties.Text))
+ .isEqualTo(listOf(AnnotatedString("1")))
+ assertThat(merged?.getOrNull(SemanticsProperties.Text))
+ .isEqualTo(
+ listOf(
+ AnnotatedString("1"),
+ AnnotatedString("2.3"),
+ AnnotatedString("2.2"),
+ AnnotatedString("2.1")
+ )
+ )
+ }
+
+ private fun ComposeContentTestRule.setTestContent(composable: @Composable () -> Unit) {
+ setContent {
+ semanticsOwner = (LocalView.current as RootForTest).semanticsOwner
+ composable()
+ }
+ }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsInfoTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsInfoTest.kt
index dcabb22..8dcd4e2 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsInfoTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsInfoTest.kt
@@ -25,6 +25,7 @@
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
@@ -61,12 +62,13 @@
// Assert.
assertThat(rootSemantics).isNotNull()
assertThat(rootSemantics.parentInfo).isNull()
- assertThat(rootSemantics.childrenInfo.size).isEqualTo(1)
+ assertThat(rootSemantics.childrenInfo.map { it.semanticsConfiguration })
+ .comparingElementsUsing(SemanticsConfigurationComparator)
+ .containsExactly(null)
// Assert extension Functions.
assertThat(rootSemantics.nearestParentThatHasSemantics()).isNull()
assertThat(rootSemantics.findMergingSemanticsParent()).isNull()
- assertThat(rootSemantics.findSemanticsChildren()).isEmpty()
}
@Test
@@ -81,21 +83,21 @@
// Assert.
assertThat(rootSemantics.parentInfo).isNull()
- assertThat(rootSemantics.childrenInfo.asMutableList()).containsExactly(semantics)
+ assertThat(rootSemantics.childrenInfo).containsExactly(semantics)
assertThat(semantics.parentInfo).isEqualTo(rootSemantics)
- assertThat(semantics.childrenInfo.size).isEqualTo(0)
+ assertThat(semantics.childrenInfo).isEmpty()
// Assert extension Functions.
assertThat(rootSemantics.nearestParentThatHasSemantics()).isNull()
assertThat(rootSemantics.findMergingSemanticsParent()).isNull()
- assertThat(rootSemantics.findSemanticsChildren().map { it.semanticsConfiguration })
+ assertThat(rootSemantics.childrenInfo.map { it.semanticsConfiguration })
.comparingElementsUsing(SemanticsConfigurationComparator)
.containsExactly(SemanticsConfiguration().apply { testTag = "testTag" })
assertThat(semantics.nearestParentThatHasSemantics()).isEqualTo(rootSemantics)
assertThat(semantics.findMergingSemanticsParent()).isNull()
- assertThat(semantics.findSemanticsChildren()).isEmpty()
+ assertThat(semantics.childrenInfo).isEmpty()
}
@Test
@@ -122,7 +124,7 @@
)
.inOrder()
- assertThat(rootSemantics.findSemanticsChildren().map { it.semanticsConfiguration })
+ assertThat(rootSemantics.childrenInfo.map { it.semanticsConfiguration })
.comparingElementsUsing(SemanticsConfigurationComparator)
.containsExactly(
SemanticsConfiguration().apply { testTag = "item1" },
@@ -132,16 +134,16 @@
checkNotNull(semantics1)
assertThat(semantics1.parentInfo).isEqualTo(rootSemantics)
- assertThat(semantics1.childrenInfo.size).isEqualTo(0)
+ assertThat(semantics1.childrenInfo).isEmpty()
checkNotNull(semantics2)
assertThat(semantics2.parentInfo).isEqualTo(rootSemantics)
- assertThat(semantics2.childrenInfo.size).isEqualTo(0)
+ assertThat(semantics2.childrenInfo).isEmpty()
// Assert extension Functions.
assertThat(rootSemantics.nearestParentThatHasSemantics()).isNull()
assertThat(rootSemantics.findMergingSemanticsParent()).isNull()
- assertThat(rootSemantics.findSemanticsChildren().map { it.semanticsConfiguration })
+ assertThat(rootSemantics.childrenInfo.map { it.semanticsConfiguration })
.comparingElementsUsing(SemanticsConfigurationComparator)
.containsExactly(
SemanticsConfiguration().apply { testTag = "item1" },
@@ -151,11 +153,11 @@
assertThat(semantics1.nearestParentThatHasSemantics()).isEqualTo(rootSemantics)
assertThat(semantics1.findMergingSemanticsParent()).isNull()
- assertThat(semantics1.findSemanticsChildren()).isEmpty()
+ assertThat(semantics1.childrenInfo).isEmpty()
assertThat(semantics2.nearestParentThatHasSemantics()).isEqualTo(rootSemantics)
assertThat(semantics2.findMergingSemanticsParent()).isNull()
- assertThat(semantics2.findSemanticsChildren()).isEmpty()
+ assertThat(semantics2.childrenInfo).isEmpty()
}
// TODO(ralu): Split this into multiple tests.
@@ -203,13 +205,14 @@
assertThat(testTarget.parentInfo).isNotEqualTo(row)
assertThat(testTarget.nearestParentThatHasSemantics()).isEqualTo(row)
assertThat(testTarget.findMergingSemanticsParent()).isEqualTo(column)
- assertThat(testTarget.childrenInfo.size).isEqualTo(5)
- assertThat(testTarget.findSemanticsChildren().map { it.semanticsConfiguration })
+ assertThat(testTarget.childrenInfo.map { it.semanticsConfiguration })
.comparingElementsUsing(SemanticsConfigurationComparator)
.containsExactly(
- SemanticsConfiguration().apply { testTag = "child1" },
+ null,
SemanticsConfiguration().apply { testTag = "child2" },
- SemanticsConfiguration().apply { testTag = "child3" }
+ null,
+ null,
+ null
)
.inOrder()
assertThat(testTarget.semanticsConfiguration?.getOrNull(TestTag)).isEqualTo("testTarget")
@@ -241,6 +244,66 @@
}
}
+ @Test
+ fun transparent() {
+ // Arrange.
+ rule.setTestContent { Box(Modifier.alpha(0.0f)) { Box(Modifier.testTag("item")) } }
+ rule.waitForIdle()
+
+ // Act.
+ val semantics = rule.getSemanticsInfoForTag("item")
+
+ // Assert.
+ assertThat(semantics?.isTransparent()).isTrue()
+ }
+
+ @Test
+ fun semiTransparent() {
+ // Arrange.
+ rule.setTestContent { Box(Modifier.alpha(0.5f)) { Box(Modifier.testTag("item")) } }
+ rule.waitForIdle()
+
+ // Act.
+ val semantics = rule.getSemanticsInfoForTag("item")
+
+ // Assert.
+ assertThat(semantics?.isTransparent()).isFalse()
+ }
+
+ @Test
+ fun nonTransparent() {
+ // Arrange.
+ rule.setTestContent { Box(Modifier.alpha(1.0f)) { Box(Modifier.testTag("item")) } }
+ rule.waitForIdle()
+
+ // Act.
+ val semantics = rule.getSemanticsInfoForTag("item")
+
+ // Assert.
+ assertThat(semantics?.isTransparent()).isFalse()
+ }
+
+ @Test
+ fun transparencyOfStackedItems() {
+ // Arrange.
+ rule.setTestContent {
+ Box(Modifier.alpha(1.0f)) { Box(Modifier.testTag("item1")) }
+ Box(Modifier.alpha(1.0f)) { Box(Modifier.testTag("item2")) }
+ Box(Modifier.alpha(0.0f)) { Box(Modifier.testTag("item3")) }
+ }
+ rule.waitForIdle()
+
+ // Act.
+ val semantics1 = rule.getSemanticsInfoForTag("item1")
+ val semantics2 = rule.getSemanticsInfoForTag("item2")
+ val semantics3 = rule.getSemanticsInfoForTag("item3")
+
+ // Assert.
+ assertThat(semantics1?.isTransparent()).isFalse()
+ assertThat(semantics2?.isTransparent()).isFalse()
+ assertThat(semantics3?.isTransparent()).isTrue()
+ }
+
private fun ComposeContentTestRule.setTestContent(composable: @Composable () -> Unit) {
setContent {
semanticsOwner = (LocalView.current as RootForTest).semanticsOwner
@@ -248,13 +311,6 @@
}
}
- /** Helper function that returns a list of children that is easier to assert on in tests. */
- private fun SemanticsInfo.findSemanticsChildren(): List<SemanticsInfo> {
- val children = mutableListOf<SemanticsInfo>()
- [email protected] { children.add(it) }
- return children
- }
-
private fun ComposeContentTestRule.getSemanticsInfoForTag(
tag: String,
useUnmergedTree: Boolean = true
@@ -266,9 +322,10 @@
private val SemanticsConfigurationComparator =
Correspondence.from<SemanticsConfiguration, SemanticsConfiguration>(
{ actual, expected ->
- actual != null &&
- expected != null &&
- actual.getOrNull(TestTag) == expected.getOrNull(TestTag)
+ (actual == null && expected == null) ||
+ (actual != null &&
+ expected != null &&
+ actual.getOrNull(TestTag) == expected.getOrNull(TestTag))
},
"has same test tag as "
)
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsListenerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsListenerTest.kt
index 77965fe..4311904 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsListenerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsListenerTest.kt
@@ -53,6 +53,7 @@
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
+import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.runner.RunWith
@@ -72,12 +73,21 @@
fun initParameters() = listOf(false, true)
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ private val previousFlagValue = ComposeUiFlags.isSemanticAutofillEnabled
+
@Before
- fun setup() {
+ fun enableAutofill() {
@OptIn(ExperimentalComposeUiApi::class)
ComposeUiFlags.isSemanticAutofillEnabled = isSemanticAutofillEnabled
}
+ @After
+ fun disableAutofill() {
+ @OptIn(ExperimentalComposeUiApi::class)
+ ComposeUiFlags.isSemanticAutofillEnabled = previousFlagValue
+ }
+
// Initial layout does not trigger listeners. Users have to detect the initial semantics
// values by detecting first layout (You can get the bounds from RectManager.RectList).
@Test
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsModifierNodeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsModifierNodeTest.kt
index d2cbd28..7cfa2d4c 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsModifierNodeTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsModifierNodeTest.kt
@@ -35,6 +35,7 @@
import androidx.compose.ui.unit.dp
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
+import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -52,12 +53,21 @@
fun initParameters() = listOf(false, true)
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ private val previousFlagValue = ComposeUiFlags.isSemanticAutofillEnabled
+
@Before
- fun setup() {
+ fun enableAutofill() {
@OptIn(ExperimentalComposeUiApi::class)
ComposeUiFlags.isSemanticAutofillEnabled = precomputedSemantics
}
+ @After
+ fun disableAutofill() {
+ @OptIn(ExperimentalComposeUiApi::class)
+ ComposeUiFlags.isSemanticAutofillEnabled = previousFlagValue
+ }
+
@Test
fun applySemantics_firstComposition() {
// Arrange.
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchLeftInteropTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchLeftInteropTest.kt
index aa67089..dacdc31 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchLeftInteropTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchLeftInteropTest.kt
@@ -456,6 +456,7 @@
AndroidView({ FocusableView(it).apply { view = this } })
}
}
+ rule.waitForIdle()
// Act.
rule.focusSearchLeft()
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt
index 0b5f51d..1a3e98e 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt
@@ -21,9 +21,12 @@
import android.util.SparseArray
import android.view.View
import android.view.ViewStructure
+import android.view.autofill.AutofillId
import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue
import androidx.annotation.RequiresApi
+import androidx.compose.ui.internal.checkPreconditionNotNull
+import androidx.compose.ui.platform.coreshims.ViewCompatShims
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastRoundToInt
@@ -39,9 +42,12 @@
val autofillManager =
view.context.getSystemService(AutofillManager::class.java)
?: error("Autofill service could not be located.")
+ var rootAutofillId: AutofillId
init {
view.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_YES
+ rootAutofillId =
+ checkPreconditionNotNull(ViewCompatShims.getAutofillId(view)?.toAutofillId())
}
override fun requestAutofillForNode(autofillNode: AutofillNode) {
@@ -82,8 +88,8 @@
var index = AutofillApi26Helper.addChildCount(root, autofillTree.children.count())
for ((id, autofillNode) in autofillTree.children) {
- AutofillApi26Helper.newChild(root, index)?.also { child ->
- AutofillApi26Helper.setAutofillId(child, AutofillApi26Helper.getAutofillId(root)!!, id)
+ AutofillApi26Helper.newChild(root, index).also { child ->
+ AutofillApi26Helper.setAutofillId(child, rootAutofillId, id)
AutofillApi26Helper.setId(child, id, view.context.packageName, null, null)
AutofillApi26Helper.setAutofillType(child, ContentDataType.Text.dataType)
AutofillApi26Helper.setAutofillHints(
@@ -93,7 +99,7 @@
val boundingBox = autofillNode.boundingBox
if (boundingBox == null) {
- // Do we need an exception here? warning? silently ignore? If the boundingbox is
+ // Do we need an exception here? warning? silently ignore? If the bounding box is
// null, the autofill overlay will not be shown.
Log.w(
"Autofill Warning",
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
index 4f63ead..371a90a 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
@@ -18,509 +18,247 @@
import android.graphics.Rect
import android.os.Build
-import android.os.Handler
-import android.os.Looper
-import android.text.InputType
import android.util.Log
import android.util.SparseArray
import android.view.View
import android.view.ViewStructure
-import android.view.autofill.AutofillManager as PlatformAndroidManager
+import android.view.autofill.AutofillId
import android.view.autofill.AutofillValue
-import android.view.inputmethod.EditorInfo
import androidx.annotation.RequiresApi
-import androidx.collection.IntObjectMap
-import androidx.collection.MutableIntObjectMap
-import androidx.collection.intObjectMapOf
-import androidx.collection.mutableIntObjectMapOf
+import androidx.collection.MutableIntSet
+import androidx.collection.mutableObjectListOf
import androidx.compose.ui.internal.checkPreconditionNotNull
-import androidx.compose.ui.platform.AndroidComposeView
-import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat.Companion.TextClassName
-import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat.Companion.TextFieldClassName
-import androidx.compose.ui.platform.SemanticsNodeCopy
-import androidx.compose.ui.platform.SemanticsNodeWithAdjustedBounds
-import androidx.compose.ui.platform.getAllUncoveredSemanticsNodesToIntObjectMap
-import androidx.compose.ui.semantics.Role.Companion.Tab
-import androidx.compose.ui.semantics.SemanticsActions.OnAutofillText
-import androidx.compose.ui.semantics.SemanticsActions.OnClick
-import androidx.compose.ui.semantics.SemanticsActions.OnLongClick
-import androidx.compose.ui.semantics.SemanticsActions.SetText
-import androidx.compose.ui.semantics.SemanticsNode
-import androidx.compose.ui.semantics.SemanticsProperties.ContentDataType as SemanticsContentDataType
-import androidx.compose.ui.semantics.SemanticsProperties.ContentDescription
-import androidx.compose.ui.semantics.SemanticsProperties.ContentType
-import androidx.compose.ui.semantics.SemanticsProperties.Disabled
-import androidx.compose.ui.semantics.SemanticsProperties.EditableText
-import androidx.compose.ui.semantics.SemanticsProperties.Focused
-import androidx.compose.ui.semantics.SemanticsProperties.MaxTextLength
-import androidx.compose.ui.semantics.SemanticsProperties.Password
-import androidx.compose.ui.semantics.SemanticsProperties.Role
-import androidx.compose.ui.semantics.SemanticsProperties.Selected
-import androidx.compose.ui.semantics.SemanticsProperties.Text
-import androidx.compose.ui.semantics.SemanticsProperties.ToggleableState
+import androidx.compose.ui.platform.coreshims.ViewCompatShims
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.SemanticsInfo
+import androidx.compose.ui.semantics.SemanticsListener
+import androidx.compose.ui.semantics.SemanticsOwner
+import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.getOrNull
-import androidx.compose.ui.state.ToggleableState.On
+import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.util.fastForEach
+private const val logTag = "ComposeAutofillManager"
+
/**
* Semantic autofill implementation for Android.
*
* @param view The parent compose view.
*/
@RequiresApi(Build.VERSION_CODES.O)
-internal class AndroidAutofillManager(val view: AndroidComposeView) :
- AutofillManager, View.OnAttachStateChangeListener {
- internal var autofillManager: AutofillManagerWrapper = AutofillManagerWrapperImpl(view)
+internal class AndroidAutofillManager(
+ var platformAutofillManager: PlatformAutofillManager,
+ private val semanticsOwner: SemanticsOwner,
+ private val view: View,
+ private val rectManager: RectManager,
+ private val packageName: String,
+) : AutofillManager, SemanticsListener {
+ private var reusableRect = Rect()
+ private var rootAutofillId: AutofillId
init {
view.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_YES
- }
-
- private val handler = Handler(Looper.getMainLooper())
-
- // `previousSemanticsNodes` holds the previous pruned semantics tree so that we can compare the
- // current and previous trees in onSemanticsChange(). We use SemanticsNodeCopy here because
- // SemanticsNode's children are dynamically generated and always reflect the current children.
- // We need to keep a copy of its old structure for comparison.
- private var previousSemanticsNodes: MutableIntObjectMap<SemanticsNodeCopy> =
- mutableIntObjectMapOf()
- private var previousSemanticsRoot =
- SemanticsNodeCopy(view.semanticsOwner.unmergedRootSemanticsNode, intObjectMapOf())
- private var checkingForSemanticsChanges = false
-
- internal var currentSemanticsNodesInvalidated = true
- // This will be used to request autofill when `AutofillManager.requestAutofill()` is called
- // (e.g. from the text toolbar).
- private var previouslyFocusedId = 0
-
- internal var currentSemanticsNodes: IntObjectMap<SemanticsNodeWithAdjustedBounds> =
- intObjectMapOf()
- get() {
- if (currentSemanticsNodesInvalidated) { // first instance of retrieving all nodes
- currentSemanticsNodesInvalidated = false
- field = view.semanticsOwner.getAllUncoveredSemanticsNodesToIntObjectMap()
- }
- return field
- }
-
- private fun updateSemanticsCopy() {
- previousSemanticsNodes.clear()
- currentSemanticsNodes.forEach { key, value ->
- previousSemanticsNodes[key] =
- SemanticsNodeCopy(value.semanticsNode, currentSemanticsNodes)
- }
- previousSemanticsRoot =
- SemanticsNodeCopy(view.semanticsOwner.unmergedRootSemanticsNode, currentSemanticsNodes)
- }
-
- private val autofillChangeChecker = Runnable {
- checkForAutofillPropertyChanges(currentSemanticsNodes)
- updateSemanticsCopy()
- checkingForSemanticsChanges = false
- }
-
- private fun checkForAutofillPropertyChanges(
- newSemanticsNodes: IntObjectMap<SemanticsNodeWithAdjustedBounds>
- ) {
- newSemanticsNodes.forEachKey { id ->
- // We do this search because the new configuration is set as a whole, so we
- // can't indicate which property is changed when setting the new configuration.
- val previousNode = previousSemanticsNodes[id]
- val currNode =
- checkPreconditionNotNull(newSemanticsNodes[id]?.semanticsNode) {
- "no value for specified key"
- }
-
- if (previousNode == null) {
- return@forEachKey
- }
-
- // Notify Autofill that the value has changed if there is a difference between
- // the previous and current values.
-
- // Check Editable Text —————————
- val previousText = previousNode.unmergedConfig.getOrNull(EditableText)?.text
- val newText = currNode.unmergedConfig.getOrNull(EditableText)?.text
- if (previousText != newText && newText != null) {
- notifyAutofillValueChanged(id, newText)
- }
-
- // Check Focus —————————
- val previousFocus = previousNode.unmergedConfig.getOrNull(Focused)
- val currFocus = currNode.unmergedConfig.getOrNull(Focused)
- if (previousFocus != true && currFocus == true) {
- notifyViewEntered(id)
- previouslyFocusedId = id
- }
- if (previousFocus == true && currFocus != true) {
- notifyViewExited(id)
- }
-
- // Check Visibility —————————
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
- val prevTransparency = previousNode.isTransparent
- val currTransparency = currNode.isTransparent
- if (prevTransparency != currTransparency) {
- notifyVisibilityChanged(id, currTransparency)
- }
- }
- }
- }
-
- internal fun onSemanticsChange() {
- currentSemanticsNodesInvalidated = true
- if (!checkingForSemanticsChanges) {
- checkingForSemanticsChanges = true
- handler.post(autofillChangeChecker)
- }
- }
-
- private fun notifyViewEntered(semanticsId: Int) {
- currentSemanticsNodes[semanticsId]?.adjustedBounds?.let {
- autofillManager.notifyViewEntered(semanticsId, it)
- }
- }
-
- private fun notifyViewExited(semanticsId: Int) {
- autofillManager.notifyViewExited(semanticsId)
- }
-
- private fun notifyAutofillValueChanged(semanticsId: Int, newAutofillValue: Any) {
- val currSemanticsNode = currentSemanticsNodes[semanticsId]?.semanticsNode
- val currDataType =
- currSemanticsNode?.unmergedConfig?.getOrNull(SemanticsContentDataType) ?: return
- when (currDataType) {
- ContentDataType.Text ->
- autofillManager.notifyValueChanged(
- semanticsId,
- AutofillValue.forText(newAutofillValue.toString())
- )
- ContentDataType.Date ->
- TODO("b/138604541: Add Autofill support for ContentDataType.Date")
- ContentDataType.List ->
- TODO("b/138604541: Add Autofill support for ContentDataType.List")
- ContentDataType.Toggle ->
- TODO("b/138604541: Add Autofill support for ContentDataType.Toggle")
- else -> {
- TODO("b/138604541: Add Autofill support for ContentDataType.None")
- }
- }
- }
-
- private fun notifyVisibilityChanged(semanticsId: Int, isInvisible: Boolean) {
- autofillManager.notifyViewVisibilityChanged(semanticsId, !isInvisible)
+ rootAutofillId =
+ checkPreconditionNotNull(ViewCompatShims.getAutofillId(view)?.toAutofillId())
}
override fun commit() {
- autofillManager.commit()
+ platformAutofillManager.commit()
}
override fun cancel() {
- autofillManager.cancel()
+ platformAutofillManager.cancel()
}
+ // This will be used to request autofill when
+ // `AutofillManager.requestAutofillForActiveElement()` is called (e.g. from the text toolbar).
+ private var previouslyFocusedId = -1
+
override fun requestAutofillForActiveElement() {
- currentSemanticsNodes[previouslyFocusedId]?.let {
- autofillManager.requestAutofill(previouslyFocusedId, it.adjustedBounds)
+ if (previouslyFocusedId <= 0) return
+
+ rectManager.rects.withRect(previouslyFocusedId) { left, top, right, bottom ->
+ reusableRect.set(left, top, right, bottom)
+ platformAutofillManager.requestAutofill(view, previouslyFocusedId, reusableRect)
}
}
- internal fun onTextFillHelper(toFillId: Int, autofillValue: String) {
- // Use mapping to find lambda corresponding w semanticsNodeId,
- // then invoke the lambda. This will change the field text.
- val currSemanticsNode = currentSemanticsNodes[toFillId]?.semanticsNode
- currSemanticsNode
- ?.unmergedConfig
- ?.getOrNull(OnAutofillText)
- ?.action
- ?.invoke(AnnotatedString(autofillValue))
- }
+ /** Send events to the autofill service in response to semantics changes. */
+ override fun onSemanticsChanged(
+ semanticsInfo: SemanticsInfo,
+ previousSemanticsConfiguration: SemanticsConfiguration?
+ ) {
+ val config = semanticsInfo.semanticsConfiguration
+ val prevConfig = previousSemanticsConfiguration
+ val semanticsId = semanticsInfo.semanticsId
- companion object {
- /**
- * Autofill Manager callback.
- *
- * This callback is called when we receive autofill events. It adds some logs that can be
- * useful for debug purposes.
- */
- internal object AutofillSemanticCallback : PlatformAndroidManager.AutofillCallback() {
- override fun onAutofillEvent(view: View, virtualId: Int, event: Int) {
- super.onAutofillEvent(view, virtualId, event)
- Log.d(
- "Autofill Status",
- when (event) {
- EVENT_INPUT_SHOWN -> "Autofill popup was shown."
- EVENT_INPUT_HIDDEN -> "Autofill popup was hidden."
- EVENT_INPUT_UNAVAILABLE ->
- """
- |Autofill popup isn't shown because autofill is not available.
- |
- |Did you set up autofill?
- |1. Go to Settings > System > Languages&input > Advanced > Autofill Service
- |2. Pick a service
- |
- |Did you add an account?
- |1. Go to Settings > System > Languages&input > Advanced
- |2. Click on the settings icon next to the Autofill Service
- |3. Add your account
- """
- .trimMargin()
- else -> "Unknown status event."
- }
+ // Check Editable Text.
+ val previousText = prevConfig?.getOrNull(SemanticsProperties.EditableText)?.text
+ val newText = config?.getOrNull(SemanticsProperties.EditableText)?.text
+ if (previousText != newText && newText != null) {
+ val contentDataType = config.getOrNull(SemanticsProperties.ContentDataType)
+ if (contentDataType == ContentDataType.Text) {
+ platformAutofillManager.notifyValueChanged(
+ view,
+ semanticsId,
+ AutofillApi26Helper.getAutofillTextValue(newText.toString())
)
}
+ }
- /** Registers the autofill debug callback. */
- fun register(androidAutofillManager: AndroidAutofillManager) {
- androidAutofillManager.autofillManager.autofillManager.registerCallback(this)
+ // Check Focus.
+ // TODO: Instead of saving the focused item here, add some internal API to focusManager
+ // so that this could be more efficient.
+ val previousFocus = prevConfig?.getOrNull(SemanticsProperties.Focused)
+ val currFocus = config?.getOrNull(SemanticsProperties.Focused)
+ val supportsAutofill = config?.getOrNull(SemanticsActions.OnAutofillText) != null
+ if (previousFocus != true && currFocus == true && supportsAutofill) {
+ previouslyFocusedId = semanticsId
+ rectManager.rects.withRect(semanticsId) { left, top, right, bottom ->
+ platformAutofillManager.notifyViewEntered(
+ view,
+ semanticsId,
+ Rect(left, top, right, bottom)
+ )
}
+ }
+ val previouslySupportedAutofill = config?.getOrNull(SemanticsActions.OnAutofillText) != null
+ if (previousFocus == true && currFocus != true && previouslySupportedAutofill) {
+ platformAutofillManager.notifyViewExited(view, semanticsId)
+ }
- /** Unregisters the autofill debug callback. */
- fun unregister(androidAutofillManager: AndroidAutofillManager) {
- androidAutofillManager.autofillManager.autofillManager.unregisterCallback(this)
+ // Update currentlyDisplayedIDs if relevance to Autofill has changed.
+ val prevRelatedToAutofill = prevConfig?.isRelatedToAutofill()
+ val currRelatedToAutofill = config?.isRelatedToAutofill()
+ if (prevRelatedToAutofill != currRelatedToAutofill) {
+ if (currRelatedToAutofill == true) {
+ currentlyDisplayedIDs.add(semanticsId)
+ } else {
+ currentlyDisplayedIDs.remove(semanticsId)
}
+ executeAutoCommit()
}
}
- override fun onViewAttachedToWindow(v: View) {}
+ /** Populate the structure of the entire view hierarchy when the framework requests it. */
+ fun populateViewStructure(rootViewStructure: ViewStructure) {
+ val autofillApi = AutofillApi26Helper
+ val rootSemanticInfo = semanticsOwner.rootInfo
- override fun onViewDetachedFromWindow(v: View) {
- handler.removeCallbacks(autofillChangeChecker)
- }
-}
+ // Populate view structure for the root.
+ rootViewStructure.populate(rootSemanticInfo, rootAutofillId, packageName, rectManager)
-@RequiresApi(Build.VERSION_CODES.O)
-internal fun AndroidAutofillManager.populateViewStructure(root: ViewStructure) {
- // Add child nodes. The function returns the index to the first item.
- val count =
- currentSemanticsNodes.count { _, semanticsNodeWithAdjustedBounds ->
- // TODO(333102566): remove the `isRelatedToAutofill` check below
- // for heuristics based autofill support.
- semanticsNodeWithAdjustedBounds.semanticsNode.unmergedConfig.contains(ContentType) ||
- semanticsNodeWithAdjustedBounds.semanticsNode.unmergedConfig.contains(
- SemanticsContentDataType
- )
- }
+ // We save the semanticInfo and viewStructure of the item in a list. These are always stored
+ // as pairs, and we need to cast them back to the required types when we extract them.
+ val populateChildren = mutableObjectListOf<Any>(rootSemanticInfo, rootViewStructure)
- // TODO(b/138549623): Instead of creating a flattened tree by using the nodes from the map, we
- // can use SemanticsOwner to get the root SemanticsInfo and create a more representative tree.
- var index = AutofillApi26Helper.addChildCount(root, count)
+ @Suppress("Range") // isNotEmpty ensures removeAt is not called with -1.
+ while (populateChildren.isNotEmpty()) {
- // Iterate through currentSemanticsNodes, finding autofill-related nodes
- // and call corresponding APIs on the viewStructure as listed above
- currentSemanticsNodes.forEach { semanticsId, adjustedNode ->
- if (
- adjustedNode.semanticsNode.unmergedConfig.contains(ContentType) ||
- adjustedNode.semanticsNode.unmergedConfig.contains(SemanticsContentDataType)
- ) {
- AutofillApi26Helper.newChild(root, index)?.also { child ->
- AutofillApi26Helper.setAutofillId(
- child,
- AutofillApi26Helper.getAutofillId(root)!!,
- semanticsId
- )
- AutofillApi26Helper.setId(child, semanticsId, view.context.packageName, null, null)
+ val parentStructure =
+ populateChildren.removeAt(populateChildren.lastIndex) as ViewStructure
+ val parentInfo = populateChildren.removeAt(populateChildren.lastIndex) as SemanticsInfo
- adjustedNode.semanticsNode.unmergedConfig.getOrNull(SemanticsContentDataType)?.let {
- AutofillApi26Helper.setAutofillTypeForViewStruct(child, it)
+ parentInfo.childrenInfo.fastForEach { childInfo ->
+ if (childInfo.isDeactivated || !childInfo.isAttached || !childInfo.isPlaced) {
+ return@fastForEach
}
- adjustedNode.semanticsNode.unmergedConfig
- .getOrNull(ContentType)
- ?.contentHints
- ?.toTypedArray()
- ?.let { AutofillApi26Helper.setAutofillHints(child, it) }
-
- adjustedNode.adjustedBounds.run {
- AutofillApi26Helper.setDimens(child, left, top, 0, 0, width(), height())
+ // TODO(b/378160001): For now we only populate autofill-able nodes. Populate the
+ // structure for all nodes in the future.
+ val semanticsConfigurationChild = childInfo.semanticsConfiguration
+ if (semanticsConfigurationChild?.isRelatedToAutofill() != true) {
+ populateChildren.add(childInfo)
+ populateChildren.add(parentStructure)
+ return@fastForEach
}
- adjustedNode.semanticsNode.populateViewStructure(child)
+ val childIndex = autofillApi.addChildCount(parentStructure, 1)
+ val childStructure = autofillApi.newChild(parentStructure, childIndex)
+ childStructure.populate(childInfo, rootAutofillId, packageName, rectManager)
+ populateChildren.add(childInfo)
+ populateChildren.add(childStructure)
}
- index++
}
}
+
+ /** When the autofill service provides data, perform autofill using semantic actions. */
+ fun performAutofill(values: SparseArray<AutofillValue>) {
+ for (index in 0 until values.size()) {
+ val itemId = values.keyAt(index)
+ val value = values[itemId]
+ when {
+ AutofillApi26Helper.isText(value) ->
+ semanticsOwner[itemId]
+ ?.semanticsConfiguration
+ ?.getOrNull(SemanticsActions.OnAutofillText)
+ ?.action
+ ?.invoke(AnnotatedString(AutofillApi26Helper.textValue(value).toString()))
+
+ // TODO(b/138604541): Add Autofill support for date fields.
+ AutofillApi26Helper.isDate(value) ->
+ Log.w(logTag, "Auto filling Date fields is not yet supported.")
+
+ // TODO(b/138604541): Add Autofill support for dropdown lists.
+ AutofillApi26Helper.isList(value) ->
+ Log.w(logTag, "Auto filling dropdown lists is not yet supported.")
+
+ // TODO(b/138604541): Add Autofill support for toggle fields.
+ AutofillApi26Helper.isToggle(value) ->
+ Log.w(logTag, "Auto filling toggle fields are not yet supported.")
+ }
+ }
+ }
+
+ // Consider moving the currently displayed IDs to a separate VisibilityManager class. This might
+ // be needed by ContentCapture and Accessibility.
+ private var currentlyDisplayedIDs = MutableIntSet()
+ private var pendingChangesToDisplayedIds = false
+
+ internal fun onPostAttach(semanticsInfo: SemanticsInfo) {
+ if (semanticsInfo.semanticsConfiguration?.isRelatedToAutofill() == true) {
+ currentlyDisplayedIDs.add(semanticsInfo.semanticsId)
+ pendingChangesToDisplayedIds = true
+ // TODO(MNUZEN): Notify autofill manager that a node has been added.
+ // platformAutofillManager
+ // .notifyViewVisibilityChanged(view, semanticsInfo.semanticsId, true)
+ }
+ }
+
+ internal fun onDetach(semanticsInfo: SemanticsInfo) {
+ if (semanticsInfo.semanticsConfiguration?.isRelatedToAutofill() == true) {
+ currentlyDisplayedIDs.remove(semanticsInfo.semanticsId)
+ pendingChangesToDisplayedIds = true
+ // TODO(MNUZEN): Notify autofill manager that a node has been removed.
+ // platformAutofillManager
+ // .notifyViewVisibilityChanged(view, semanticsInfo.semanticsId, false)
+ }
+ }
+
+ internal fun onEndApplyChanges() {
+ if (pendingChangesToDisplayedIds) {
+ executeAutoCommit()
+ pendingChangesToDisplayedIds = false
+ }
+ }
+
+ // We maintain a copy of the previously displayed IDs, and call AutofillManager.commit() when
+ // all the previously displayed IDs were removed.
+ private var previouslyDisplayedIDs = MutableIntSet()
+
+ private fun executeAutoCommit() {
+ // Check for screen changes or complete removal.
+ if (!currentlyDisplayedIDs.containsAll(previouslyDisplayedIDs)) {
+ platformAutofillManager.commit()
+ }
+ previouslyDisplayedIDs.copyFrom(currentlyDisplayedIDs)
+ }
}
-@RequiresApi(Build.VERSION_CODES.O)
-internal fun SemanticsNode.populateViewStructure(child: ViewStructure) {
- // ———————— Interactions (clicking, checking, selecting, etc.)
- AutofillApi26Helper.setClickable(child, unmergedConfig.contains(OnClick))
- AutofillApi26Helper.setCheckable(child, unmergedConfig.contains(ToggleableState))
- AutofillApi26Helper.setEnabled(child, (!config.contains(Disabled)))
- AutofillApi26Helper.setFocused(child, unmergedConfig.getOrNull(Focused) == true)
- AutofillApi26Helper.setFocusable(child, unmergedConfig.contains(Focused))
- AutofillApi26Helper.setLongClickable(child, unmergedConfig.contains(OnLongClick))
- unmergedConfig.getOrNull(Selected)?.let { AutofillApi26Helper.setSelected(child, it) }
-
- unmergedConfig.getOrNull(ToggleableState)?.let {
- AutofillApi26Helper.setChecked(child, it == On)
- }
- // TODO(MNUZEN): Set setAccessibilityFocused as well
-
- // ———————— Visibility, elevation, alpha
- // Transparency should be the only thing affecting View.VISIBLE (pruning will take care of all
- // covered nodes).
- // TODO(mnuzen): since we are removing pruning in semantics/accessibility with `semanticInfo`,
- // double check that this is the correct behavior even after switching.
- AutofillApi26Helper.setVisibility(
- child,
- if (!isTransparent || isRoot) View.VISIBLE else View.INVISIBLE
- )
-
- // TODO(335726351): will call the below method when b/335726351 has been fulfilled and
- // `isOpaque` is added back.
- // AutofillApi26Helper.setOpaque(child, isOpaque)
-
- // ———————— Text, role, content description
- config.getOrNull(Text)?.let { textList ->
- var concatenatedText = ""
- textList.fastForEach { text -> concatenatedText += text.text + "\n" }
- AutofillApi26Helper.setText(child, concatenatedText)
- AutofillApi26Helper.setClassName(child, TextClassName)
- }
-
- unmergedConfig.getOrNull(MaxTextLength)?.let {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- AutofillApi28Helper.setMaxTextLength(child, it)
- }
- }
-
- val role = unmergedConfig.getOrNull(Role)
- role?.let {
- if (isFake || replacedChildren.isEmpty()) {
- AutofillApi26Helper.setClassName(child, it.toLegacyClassName())
- }
- }
-
- if (unmergedConfig.contains(SetText)) {
- // If `SetText` action exists, then the element is a TextField
- AutofillApi26Helper.setClassName(child, TextFieldClassName)
- // If the element is a TextField, then we also want to set the current text value for
- // autofill. (This data is used when we save autofilled values.)
- unmergedConfig.getOrNull(Text)?.let { textList ->
- var concatenatedText = ""
- textList.fastForEach { text -> concatenatedText += text.text + "\n" }
- AutofillApi26Helper.setAutofillValue(
- child,
- AutofillApi26Helper.getAutofillTextValue(concatenatedText)
- )
- }
- }
-
- unmergedConfig.getOrNull(Selected)?.let {
- if (role == Tab) {
- AutofillApi26Helper.setSelected(child, it)
- } else {
- AutofillApi26Helper.setCheckable(child, true)
- AutofillApi26Helper.setChecked(child, it)
- }
- }
-
- unmergedConfig.getOrNull(ContentDescription)?.firstOrNull()?.let {
- AutofillApi26Helper.setContentDescription(child, it)
- }
-
- // ———————— Parsing autofill hints and types
- // If there is no explicitly set data type, parse it from semantics.
- if (unmergedConfig.contains(SetText)) {
- if (!unmergedConfig.contains(SemanticsContentDataType)) {
- AutofillApi26Helper.setAutofillType(child, View.AUTOFILL_TYPE_TEXT)
- }
- }
-
- // If it's a password, setInputType and the sensitive flag
- if (unmergedConfig.contains(Password)) {
- AutofillApi26Helper.setInputType(
- child,
- InputType.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
- )
- AutofillApi26Helper.setDataIsSensitive(child, true)
- }
-
- // if the toggleableState is not null, set autofillType to AUTOFILL_TYPE_TOGGLE
- if (unmergedConfig.contains(ToggleableState)) {
- AutofillApi26Helper.setAutofillType(child, View.AUTOFILL_TYPE_TOGGLE)
- }
-}
-
-@RequiresApi(Build.VERSION_CODES.O)
-internal fun AndroidAutofillManager.performAutofill(values: SparseArray<AutofillValue>) {
- for (index in 0 until values.size()) {
- val itemId = values.keyAt(index)
- val value = values[itemId]
- when {
- AutofillApi26Helper.isText(value) -> // passing in the autofillID
- onTextFillHelper(itemId, AutofillApi26Helper.textValue(value).toString())
- AutofillApi26Helper.isDate(value) ->
- TODO("b/138604541: Add Autofill support for ContentDataType.Date")
- AutofillApi26Helper.isList(value) ->
- TODO("b/138604541: Add Autofill support for ContentDataType.List")
- AutofillApi26Helper.isToggle(value) ->
- TODO("b/138604541: Add Autofill support for ContentDataType.Toggle")
- }
- }
-}
-
-/** Wrapper for the final AutofillManager class. This can be mocked in testing. */
-@RequiresApi(Build.VERSION_CODES.O)
-internal interface AutofillManagerWrapper {
- val autofillManager: PlatformAndroidManager
-
- fun notifyViewEntered(semanticsId: Int, bounds: Rect)
-
- fun notifyViewExited(semanticsId: Int)
-
- fun notifyValueChanged(semanticsId: Int, autofillValue: AutofillValue)
-
- fun notifyViewVisibilityChanged(semanticsId: Int, isVisible: Boolean)
-
- fun commit()
-
- fun cancel()
-
- fun requestAutofill(semanticsId: Int, bounds: Rect)
-}
-
-@RequiresApi(Build.VERSION_CODES.O)
-private class AutofillManagerWrapperImpl(val view: View) : AutofillManagerWrapper {
- override val autofillManager =
- view.context.getSystemService(PlatformAndroidManager::class.java)
- ?: error("Autofill service could not be located.")
-
- override fun notifyViewEntered(semanticsId: Int, bounds: Rect) {
- autofillManager.notifyViewEntered(view, semanticsId, bounds)
- }
-
- override fun notifyViewExited(semanticsId: Int) {
- autofillManager.notifyViewExited(view, semanticsId)
- }
-
- override fun notifyValueChanged(semanticsId: Int, autofillValue: AutofillValue) {
- autofillManager.notifyValueChanged(view, semanticsId, autofillValue)
- }
-
- override fun notifyViewVisibilityChanged(semanticsId: Int, isVisible: Boolean) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
- AutofillApi27Helper.notifyViewVisibilityChanged(
- view,
- autofillManager,
- semanticsId,
- isVisible
- )
- }
- }
-
- override fun commit() {
- autofillManager.commit()
- }
-
- override fun cancel() {
- autofillManager.cancel()
- }
-
- override fun requestAutofill(semanticsId: Int, bounds: Rect) {
- autofillManager.requestAutofill(view, semanticsId, bounds)
- }
+private fun SemanticsConfiguration.isRelatedToAutofill(): Boolean {
+ return props.contains(SemanticsActions.OnAutofillText) ||
+ props.contains(SemanticsProperties.ContentType) ||
+ props.contains(SemanticsProperties.ContentDataType)
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AutofillDebugUtils.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AutofillDebugUtils.android.kt
new file mode 100644
index 0000000..8656b41
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AutofillDebugUtils.android.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.compose.ui.autofill
+
+import android.os.Build
+import android.util.Log
+import android.view.View
+import android.view.autofill.AutofillManager
+import androidx.annotation.RequiresApi
+
+/**
+ * Autofill Manager callback.
+ *
+ * This callback is called when we receive autofill events. It adds some logs that can be useful for
+ * debug purposes.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+internal object AutofillSemanticCallback : AutofillManager.AutofillCallback() {
+ override fun onAutofillEvent(view: View, virtualId: Int, event: Int) {
+ super.onAutofillEvent(view, virtualId, event)
+ Log.d(
+ "Autofill Status",
+ when (event) {
+ EVENT_INPUT_SHOWN -> "Autofill popup was shown."
+ EVENT_INPUT_HIDDEN -> "Autofill popup was hidden."
+ EVENT_INPUT_UNAVAILABLE ->
+ """
+ |Autofill popup isn't shown because autofill is not available.
+ |
+ |Did you set up autofill?
+ |1. Go to Settings > System > Languages&input > Advanced > Autofill Service
+ |2. Pick a service
+ |
+ |Did you add an account?
+ |1. Go to Settings > System > Languages&input > Advanced
+ |2. Click on the settings icon next to the Autofill Service
+ |3. Add your account
+ """
+ .trimMargin()
+ else -> "Unknown status event."
+ }
+ )
+ }
+
+ /** Registers the autofill debug callback. */
+ fun register(androidAutofillManager: AndroidAutofillManager) {
+ (androidAutofillManager.platformAutofillManager as PlatformAutofillManagerImpl)
+ .platformAndroidManager
+ .registerCallback(this)
+ }
+
+ /** Unregisters the autofill debug callback. */
+ fun unregister(androidAutofillManager: AndroidAutofillManager) {
+ (androidAutofillManager.platformAutofillManager as PlatformAutofillManagerImpl)
+ .platformAndroidManager
+ .unregisterCallback(this)
+ }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AutofillUtils.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AutofillUtils.android.kt
index 6309c56..60bf15f 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AutofillUtils.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AutofillUtils.android.kt
@@ -22,8 +22,7 @@
import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue
import androidx.annotation.RequiresApi
-import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat.Companion.ClassName
-import androidx.compose.ui.semantics.Role
+import androidx.collection.MutableIntSet
/**
* This class is here to ensure that the classes that use this API will get verified and can be AOT
@@ -62,7 +61,7 @@
@RequiresApi(26)
internal object AutofillApi26Helper {
@RequiresApi(26)
- fun newChild(structure: ViewStructure, index: Int): ViewStructure? = structure.newChild(index)
+ fun newChild(structure: ViewStructure, index: Int): ViewStructure = structure.newChild(index)
@RequiresApi(26)
fun addChildCount(structure: ViewStructure, num: Int) = structure.addChildCount(num)
@@ -183,28 +182,14 @@
fun getAutofillTextValue(value: String): AutofillValue {
return AutofillValue.forText(value)
}
-
- @RequiresApi(26)
- fun setAutofillTypeForViewStruct(child: ViewStructure, dataType: ContentDataType) {
- val autofillType =
- when (dataType) {
- ContentDataType.Text -> View.AUTOFILL_TYPE_TEXT
- ContentDataType.Date -> View.AUTOFILL_TYPE_DATE
- ContentDataType.Toggle -> View.AUTOFILL_TYPE_TOGGLE
- ContentDataType.List -> View.AUTOFILL_TYPE_LIST
- else -> View.AUTOFILL_TYPE_NONE
- }
- setAutofillType(child, autofillType)
- }
}
-internal fun Role.toLegacyClassName(): String =
- when (this) {
- Role.Button -> "android.widget.Button"
- Role.Checkbox -> "android.widget.CheckBox"
- Role.RadioButton -> "android.widget.RadioButton"
- Role.Image -> "android.widget.ImageView"
- Role.DropdownList -> "android.widget.Spinner"
- Role.ValuePicker -> "android.widget.NumberPicker"
- else -> ClassName
- }
+// Copy all elements from `other` to `this`.
+internal fun MutableIntSet.copyFrom(other: MutableIntSet) {
+ this.clear()
+ this.addAll(other)
+}
+
+internal fun MutableIntSet.containsAll(other: MutableIntSet): Boolean {
+ return other.all { this.contains(it) }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/PlatformAutofillManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/PlatformAutofillManager.android.kt
new file mode 100644
index 0000000..3923fa4
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/PlatformAutofillManager.android.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.compose.ui.autofill
+
+import android.graphics.Rect
+import android.os.Build
+import android.view.View
+import android.view.autofill.AutofillManager
+import android.view.autofill.AutofillValue
+import androidx.annotation.RequiresApi
+
+/** Wrapper for the final AutofillManager class. This can be mocked in testing. */
+@RequiresApi(Build.VERSION_CODES.O)
+internal interface PlatformAutofillManager {
+ fun notifyViewEntered(view: View, semanticsId: Int, bounds: Rect)
+
+ fun notifyViewExited(view: View, semanticsId: Int)
+
+ fun notifyValueChanged(view: View, semanticsId: Int, autofillValue: AutofillValue)
+
+ fun notifyViewVisibilityChanged(view: View, semanticsId: Int, isVisible: Boolean)
+
+ fun commit()
+
+ fun cancel()
+
+ fun requestAutofill(view: View, semanticsId: Int, bounds: Rect)
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+internal class PlatformAutofillManagerImpl(val platformAndroidManager: AutofillManager) :
+ PlatformAutofillManager {
+
+ override fun notifyViewEntered(view: View, semanticsId: Int, bounds: Rect) {
+ platformAndroidManager.notifyViewEntered(view, semanticsId, bounds)
+ }
+
+ override fun notifyViewExited(view: View, semanticsId: Int) {
+ platformAndroidManager.notifyViewExited(view, semanticsId)
+ }
+
+ override fun notifyValueChanged(view: View, semanticsId: Int, autofillValue: AutofillValue) {
+ platformAndroidManager.notifyValueChanged(view, semanticsId, autofillValue)
+ }
+
+ override fun notifyViewVisibilityChanged(view: View, semanticsId: Int, isVisible: Boolean) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+ AutofillApi27Helper.notifyViewVisibilityChanged(
+ view,
+ platformAndroidManager,
+ semanticsId,
+ isVisible
+ )
+ }
+ }
+
+ override fun commit() {
+ platformAndroidManager.commit()
+ }
+
+ override fun cancel() {
+ platformAndroidManager.cancel()
+ }
+
+ override fun requestAutofill(view: View, semanticsId: Int, bounds: Rect) {
+ platformAndroidManager.requestAutofill(view, semanticsId, bounds)
+ }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/PopulateViewStructure.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/PopulateViewStructure.android.kt
new file mode 100644
index 0000000..db2789e
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/PopulateViewStructure.android.kt
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.compose.ui.autofill
+
+import android.os.Build
+import android.text.InputType
+import android.view.View
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+import android.view.ViewStructure
+import android.view.autofill.AutofillId
+import android.view.inputmethod.EditorInfo
+import androidx.annotation.RequiresApi
+import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat.Companion.TextClassName
+import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat.Companion.TextFieldClassName
+import androidx.compose.ui.platform.toLegacyClassName
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsInfo
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.mergedSemanticsConfiguration
+import androidx.compose.ui.spatial.RectManager
+import androidx.compose.ui.state.ToggleableState
+import androidx.compose.ui.state.ToggleableState.On
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.util.fastForEach
+import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
+
+@RequiresApi(Build.VERSION_CODES.O)
+internal fun ViewStructure.populate(
+ semanticsInfo: SemanticsInfo,
+ rootAutofillId: AutofillId,
+ packageName: String?,
+ rectManager: RectManager
+) {
+ val autofillApi = AutofillApi26Helper
+ val properties = SemanticsProperties
+ val actions = SemanticsActions
+
+ // Semantics properties.
+ var contentDataTypeProp: ContentDataType? = null
+ var contentTypeProp: ContentType? = null
+ var editableTextProp: AnnotatedString? = null
+ var isPasswordProp = false
+ var maxTextLengthProp: Int? = null
+ var roleProp: Role? = null
+ var selectedProp: Boolean? = null
+ var toggleableStateProp: ToggleableState? = null
+
+ // Semantics properties form merged configuration.
+ var disabledMergedProp: Boolean? = null
+ var textMergedProp: List<AnnotatedString>? = null
+
+ // Semantics actions.
+ var hasSetTextAction = false
+
+ semanticsInfo.semanticsConfiguration?.props?.forEach { property, value ->
+ @Suppress("UNCHECKED_CAST")
+ when (property) {
+ properties.ContentDataType -> contentDataTypeProp = value as ContentDataType
+ properties.ContentDescription ->
+ (value as List<String>).firstOrNull()?.let {
+ autofillApi.setContentDescription(this, it)
+ }
+ properties.ContentType -> contentTypeProp = value as ContentType
+ properties.EditableText -> editableTextProp = value as AnnotatedString
+ properties.Focused -> autofillApi.setFocused(this, value as Boolean)
+ properties.MaxTextLength -> maxTextLengthProp = value as Int
+ properties.Password -> isPasswordProp = true
+ properties.Role -> roleProp = value as Role
+ properties.Selected -> selectedProp = value as Boolean
+ properties.ToggleableState -> toggleableStateProp = value as ToggleableState
+ actions.OnClick -> autofillApi.setClickable(this, true)
+ actions.OnLongClick -> autofillApi.setLongClickable(this, true)
+ actions.RequestFocus -> autofillApi.setFocusable(this, true)
+ actions.SetText -> hasSetTextAction = true
+ }
+ }
+
+ semanticsInfo.mergedSemanticsConfiguration()?.props?.forEach { property, value ->
+ @Suppress("UNCHECKED_CAST")
+ when (property) {
+ properties.Disabled -> disabledMergedProp = value as Boolean
+ properties.Text -> textMergedProp = value as List<AnnotatedString>
+ }
+ }
+
+ // Id.
+ val semanticsId =
+ semanticsInfo.semanticsId.takeUnless { semanticsInfo.parentInfo == null }
+ ?: AccessibilityNodeProviderCompat.HOST_VIEW_ID
+ autofillApi.setAutofillId(this, rootAutofillId, semanticsId)
+ autofillApi.setId(this, semanticsId, packageName, null, null)
+
+ // Autofill Type.
+ val autofillType =
+ contentDataTypeProp?.dataType
+ ?: when {
+ hasSetTextAction -> View.AUTOFILL_TYPE_TEXT
+ toggleableStateProp != null -> View.AUTOFILL_TYPE_TOGGLE
+ else -> null
+ }
+ autofillType?.let { autofillApi.setAutofillType(this, it) }
+
+ // Autofill Hints.
+ contentTypeProp?.contentHints?.let { autofillApi.setAutofillHints(this, it.toTypedArray()) }
+
+ // Dimensions.
+ rectManager.rects.withRect(semanticsInfo.semanticsId) { left, top, right, bottom ->
+ autofillApi.setDimens(this, left, top, 0, 0, right - left, bottom - top)
+ }
+
+ // Enabled.
+ autofillApi.setEnabled(this, disabledMergedProp != true)
+
+ // Selected.
+ selectedProp?.let { autofillApi.setSelected(this, it) }
+
+ // Checkable.
+ val toggleableState = toggleableStateProp
+ val selected = selectedProp
+ if (toggleableState != null) {
+ autofillApi.setCheckable(this, true)
+ autofillApi.setChecked(this, toggleableState == On)
+ } else if (selected != null && roleProp != Role.Tab) {
+ autofillApi.setCheckable(this, true)
+ autofillApi.setChecked(this, selected)
+ }
+
+ // Password.
+ val passwordHint = ContentType.Password.contentHints.first()
+ val contentTypePassword = contentTypeProp?.contentHints?.contains(passwordHint) == true
+ val isPassword = isPasswordProp || contentTypePassword
+ if (isPassword) {
+ autofillApi.setDataIsSensitive(this, true)
+ }
+
+ // Visibility.
+ // TODO(b/383198004): This only checks transparency. We should also check whether the layoutNode
+ // is within visible bounds.
+ autofillApi.setVisibility(this, if (semanticsInfo.isTransparent()) INVISIBLE else VISIBLE)
+
+ // TODO(335726351): will call the below method when b/335726351 has been fulfilled and
+ // `isOpaque` is added back.
+ // autofillApi.setOpaque(this, isOpaque)
+
+ // Text.
+ textMergedProp?.let {
+ var concatenatedText = ""
+ it.fastForEach { text -> concatenatedText += text.text + "\n" }
+ autofillApi.setText(this, concatenatedText)
+ autofillApi.setClassName(this, TextClassName)
+ }
+
+ // Role.
+ if (semanticsInfo.childrenInfo.isEmpty()) {
+ roleProp?.toLegacyClassName()?.let { autofillApi.setClassName(this, it) }
+ }
+
+ // TextField.
+ if (hasSetTextAction) {
+ autofillApi.setClassName(this, TextFieldClassName)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ maxTextLengthProp?.let { AutofillApi28Helper.setMaxTextLength(this, it) }
+ }
+
+ // Set the current value for autofill. (Used to save values during autofill commit).
+ editableTextProp?.let {
+ autofillApi.setAutofillValue(this, autofillApi.getAutofillTextValue(it.text))
+ }
+
+ // Password.
+ if (isPassword) {
+ autofillApi.setInputType(
+ this,
+ InputType.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
+ )
+ }
+ }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index fd055b2..dbb41be 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -55,6 +55,7 @@
import android.view.ViewTreeObserver
import android.view.accessibility.AccessibilityNodeInfo
import android.view.animation.AnimationUtils
+import android.view.autofill.AutofillManager as PlatformAndroidManager
import android.view.autofill.AutofillValue
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
@@ -74,7 +75,6 @@
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.ComposeUiFlags
-import androidx.compose.ui.ComposeUiFlags.isSemanticAutofillEnabled
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
@@ -85,6 +85,7 @@
import androidx.compose.ui.autofill.AutofillCallback
import androidx.compose.ui.autofill.AutofillManager
import androidx.compose.ui.autofill.AutofillTree
+import androidx.compose.ui.autofill.PlatformAutofillManagerImpl
import androidx.compose.ui.autofill.performAutofill
import androidx.compose.ui.autofill.populateViewStructure
import androidx.compose.ui.contentcapture.AndroidContentCaptureManager
@@ -107,6 +108,7 @@
import androidx.compose.ui.focus.focusRect
import androidx.compose.ui.focus.is1dFocusSearch
import androidx.compose.ui.focus.isBetterCandidate
+import androidx.compose.ui.focus.requestFocus
import androidx.compose.ui.focus.requestInteropFocus
import androidx.compose.ui.focus.toAndroidFocusDirection
import androidx.compose.ui.focus.toFocusDirection
@@ -325,90 +327,28 @@
override val windowInfo: WindowInfo
get() = _windowInfo
- /**
- * Because AndroidComposeView always accepts focus, we have to divert focus to another View if
- * there is nothing focusable within. However, if there are only nonfocusable ComposeViews, then
- * the redirection can recurse infinitely. This makes sure that if that happens, then it can
- * bail when it is detected
- */
- private var processingRequestFocusForNextNonChildView = false
-
// When move focus is triggered by a key event, and move focus does not cause any focus change,
// we return the key event to the view system if focus search finds a suitable view which is not
// a compose sub-view. However if move focus is triggered programmatically, we have to manually
// implement this behavior because the view system does not have a moveFocus API.
private fun onMoveFocusInChildren(focusDirection: FocusDirection): Boolean {
- @OptIn(ExperimentalComposeUiApi::class)
- if (!ComposeUiFlags.isViewFocusFixEnabled) {
- // The view system does not have an API corresponding to Enter/Exit.
- if (focusDirection == Enter || focusDirection == Exit) return false
- val direction =
- checkNotNull(focusDirection.toAndroidFocusDirection()) { "Invalid focus direction" }
- val focusedRect = onFetchFocusRect()?.toAndroidRect()
-
- val nextView =
- FocusFinder.getInstance().let {
- if (focusedRect == null) {
- it.findNextFocus(this, findFocus(), direction)
- } else {
- it.findNextFocusFromRect(this, focusedRect, direction)
- }
- }
- return nextView?.requestInteropFocus(direction, focusedRect) ?: false
- }
// The view system does not have an API corresponding to Enter/Exit.
- if (focusDirection == Enter || focusDirection == Exit || !hasFocus()) return false
-
- val androidViewsHandler = _androidViewsHandler ?: return false
+ if (focusDirection == Enter || focusDirection == Exit) return false
val direction =
checkNotNull(focusDirection.toAndroidFocusDirection()) { "Invalid focus direction" }
+ val focusedRect = onFetchFocusRect()?.toAndroidRect()
- val root = rootView as ViewGroup
-
- val currentFocus = root.findFocus() ?: error("view hasFocus but root can't find it")
-
- val focusFinder = FocusFinder.getInstance()
- val nextView: View?
- val focusedRect: Rect?
- if (focusDirection.is1dFocusSearch() && androidViewsHandler.hasFocus()) {
- focusedRect = null
- if (SDK_INT >= O) {
- // On newer devices, the focus is normal and we can expect forward/next to work
- nextView = focusFinder.findNextFocus(root, currentFocus, direction)
- } else {
- // On older devices, FocusFinder doesn't properly order Views, so we have to use
- // a copy of the focus finder the corrects the order
- nextView = FocusFinderCompat.instance.findNextFocus1d(root, currentFocus, direction)
+ val nextView =
+ FocusFinder.getInstance().let {
+ if (focusedRect == null) {
+ it.findNextFocus(this, findFocus(), direction)
+ } else {
+ it.findNextFocusFromRect(this, focusedRect, direction)
+ }
}
- } else {
- focusedRect = onFetchFocusRect()?.toAndroidRect()
- nextView = focusFinder.findNextFocusFromRect(root, focusedRect, direction)
- nextView?.getLocationInWindow(tmpPositionArray)
- val nextPositionX = tmpPositionArray[0]
- val nextPositionY = tmpPositionArray[1]
- getLocationInWindow(tmpPositionArray)
- focusedRect?.offset(
- tmpPositionArray[0] - nextPositionX,
- tmpPositionArray[1] - nextPositionY
- )
- }
-
- // is it part of the compose hierarchy?
- if (nextView == null || nextView === currentFocus) {
- return false
- }
-
- val focusedChild = androidViewsHandler.focusedChild
- var nextParent = nextView.parent
- while (nextParent != null && nextParent !== focusedChild) {
- nextParent = nextParent.parent
- }
- if (nextParent == null) {
- return false // not a part of the compose hierarchy
- }
- return nextView.requestInteropFocus(direction, focusedRect)
+ return nextView?.requestInteropFocus(direction, focusedRect) ?: false
}
// If this root view is focused, we can get the focus rect from focusOwner. But if a sub-view
@@ -429,16 +369,6 @@
val focusDirection = getFocusDirection(keyEvent)
if (focusDirection == null || keyEvent.type != KeyDown) return@onKeyEvent false
- val androidDirection = focusDirection.toAndroidFocusDirection()
-
- @OptIn(ExperimentalComposeUiApi::class)
- if (ComposeUiFlags.isViewFocusFixEnabled) {
- if (hasFocus() && androidDirection != null) {
- // A child AndroidView is focused. See if the view has a child that should be
- // focused next.
- if (onMoveFocusInChildren(focusDirection)) return@onKeyEvent true
- }
- }
val focusedRect = onFetchFocusRect()
// Consume the key event if we moved focus or if focus search or requestFocus is
@@ -459,22 +389,13 @@
// this view. We don't return false because we don't want to re-visit sub-views. They
// will
// instead be visited when the AndroidView around them gets a moveFocus(Enter)).
+ val androidDirection =
+ checkNotNull(focusDirection.toAndroidFocusDirection()) { "Invalid focus direction" }
+ val androidRect = checkNotNull(focusedRect?.toAndroidRect()) { "Invalid rect" }
- if (androidDirection != null) {
- val nextView = findNextNonChildView(androidDirection).takeIf { it != this }
- if (nextView != null) {
- val androidRect = checkNotNull(focusedRect?.toAndroidRect()) { "Invalid rect" }
- nextView.getLocationInWindow(tmpPositionArray)
- val nextX = tmpPositionArray[0]
- val nextY = tmpPositionArray[1]
- getLocationInWindow(tmpPositionArray)
- val currentX = tmpPositionArray[0]
- val currentY = tmpPositionArray[1]
- androidRect.offset(currentX - nextX, currentY - nextY)
- if (nextView.requestInteropFocus(androidDirection, androidRect)) {
- return@onKeyEvent true
- }
- }
+ val nextView = findNextNonChildView(androidDirection).takeIf { it != this }
+ if (nextView != null && nextView.requestInteropFocus(androidDirection, androidRect)) {
+ return@onKeyEvent true
}
// Focus finder couldn't find another view. We manually wrap around since focus remained
@@ -498,9 +419,10 @@
private fun findNextNonChildView(direction: Int): View? {
var currentView: View? = this
- val focusFinder = FocusFinder.getInstance()
while (currentView != null) {
- currentView = focusFinder.findNextFocus(rootView as ViewGroup, currentView, direction)
+ currentView =
+ FocusFinder.getInstance()
+ .findNextFocus(rootView as ViewGroup, currentView, direction)
if (currentView != null && !containsDescendant(currentView)) return currentView
}
return null
@@ -534,6 +456,8 @@
override val layoutNodes: MutableIntObjectMap<LayoutNode> = mutableIntObjectMapOf()
+ override val rectManager = RectManager(layoutNodes)
+
override val rootForTest: RootForTest = this
override val semanticsOwner: SemanticsOwner =
@@ -585,7 +509,20 @@
private val _autofill = if (autofillSupported()) AndroidAutofill(this, autofillTree) else null
- internal val _autofillManager = if (autofillSupported()) AndroidAutofillManager(this) else null
+ internal val _autofillManager =
+ if (autofillSupported()) {
+ val platformAutofill = context.getSystemService(PlatformAndroidManager::class.java)
+ checkPreconditionNotNull(platformAutofill) { "Autofill service could not be located." }
+ AndroidAutofillManager(
+ platformAutofillManager = PlatformAutofillManagerImpl(platformAutofill),
+ semanticsOwner = semanticsOwner,
+ view = this,
+ rectManager = rectManager,
+ packageName = context.packageName
+ )
+ } else {
+ null
+ }
// Used as a CompositionLocal for performing autofill.
override val autofill: Autofill?
@@ -706,6 +643,7 @@
* The legacy text input service. This is only used for new text input sessions if
* [textInputSessionMutex] is null.
*/
+ @Deprecated("Use PlatformTextInputModifierNode instead.")
override val textInputService =
TextInputService(platformTextInputServiceInterceptor(legacyTextInputServiceAndroid))
@@ -898,7 +836,6 @@
init {
addOnAttachStateChangeListener(contentCaptureManager)
- _autofillManager?.let { addOnAttachStateChangeListener(it) }
setWillNotDraw(false)
isFocusable = true
if (SDK_INT >= O) {
@@ -1014,73 +951,22 @@
}
override fun requestFocus(direction: Int, previouslyFocusedRect: Rect?): Boolean {
- @OptIn(ExperimentalComposeUiApi::class)
- if (!ComposeUiFlags.isViewFocusFixEnabled) {
- // This view is already focused.
- if (isFocused) return true
-
- // If the root has focus, it means a sub-view is focused,
- // and is trying to move focus within itself.
- if (focusOwner.rootState.hasFocus) {
- return super.requestFocus(direction, previouslyFocusedRect)
- }
-
- val focusDirection = toFocusDirection(direction) ?: Enter
- return focusOwner.focusSearch(
- focusDirection = focusDirection,
- focusedRect = previouslyFocusedRect?.toComposeRect()
- ) {
- it.requestFocus(focusDirection)
- } ?: false
- }
// This view is already focused.
if (isFocused) return true
- // There is nothing focusable and we've looped around all Views back to this one, so
- // we just return false to indicate that nothing can be focused.
- if (processingRequestFocusForNextNonChildView) return false
-
- val focusDirection = toFocusDirection(direction) ?: Enter
-
// If the root has focus, it means a sub-view is focused,
// and is trying to move focus within itself.
- if (hasFocus() && onMoveFocusInChildren(focusDirection)) return true
-
- var foundFocusable = false
- val focusSearchResult =
- focusOwner.focusSearch(
- focusDirection = focusDirection,
- focusedRect = previouslyFocusedRect?.toComposeRect()
- ) {
- foundFocusable = true
- it.requestFocus(focusDirection)
- }
- if (focusSearchResult == null) {
- return false // The focus search was canceled
- }
- if (focusSearchResult) {
- return true // We found something to focus on
- }
- if (foundFocusable) {
- return false // The requestFocus() from within the focusSearch was canceled
+ if (focusOwner.rootState.hasFocus) {
+ return super.requestFocus(direction, previouslyFocusedRect)
}
- // We advertised ourselves as focusable, but we aren't. Try to just move the focus to the
- // next item.
- val nextFocusedView = findNextNonChildView(direction)
-
- // Can crash if we return false when we've advertised ourselves as focusable and we aren't
- // b/369256395
- if (nextFocusedView == null || nextFocusedView === this) {
- // There is no next View, so just return true so we don't cause a crash
- return true
- }
-
- // Try to focus on the next focusable View
- processingRequestFocusForNextNonChildView = true
- val requestFocusResult = nextFocusedView.requestFocus(direction)
- processingRequestFocusForNextNonChildView = false
- return requestFocusResult
+ val focusDirection = toFocusDirection(direction) ?: Enter
+ return focusOwner.focusSearch(
+ focusDirection = focusDirection,
+ focusedRect = previouslyFocusedRect?.toComposeRect()
+ ) {
+ it.requestFocus(focusDirection)
+ } ?: false
}
private fun onRequestFocusForOwner(
@@ -1098,12 +984,7 @@
}
private fun onClearFocusForOwner() {
- @OptIn(ExperimentalComposeUiApi::class)
- if (isFocused || (!ComposeUiFlags.isViewFocusFixEnabled && hasFocus())) {
- super.clearFocus()
- } else if (hasFocus()) {
- // Call clearFocus() on the child that has focus
- findFocus()?.clearFocus()
+ if (isFocused || hasFocus()) {
super.clearFocus()
}
}
@@ -1195,7 +1076,10 @@
}
override fun onPostAttach(node: LayoutNode) {
- // TODO(MNUZEN): add autofill logic here
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (autofillSupported() && ComposeUiFlags.isSemanticAutofillEnabled) {
+ _autofillManager?.onPostAttach(node)
+ }
}
override fun onDetach(node: LayoutNode) {
@@ -1206,6 +1090,10 @@
if (ComposeUiFlags.isRectTrackingEnabled) {
rectManager.remove(node)
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (autofillSupported() && ComposeUiFlags.isSemanticAutofillEnabled) {
+ _autofillManager?.onDetach(node)
+ }
}
fun requestClearInvalidObservations() {
@@ -1221,6 +1109,10 @@
if (childAndroidViews != null) {
clearChildInvalidObservations(childAndroidViews)
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (autofillSupported() && ComposeUiFlags.isSemanticAutofillEnabled) {
+ _autofillManager?.onEndApplyChanges()
+ }
// Listeners can add more items to the list and we want to ensure that they
// are executed after being added, so loop until the list is empty
while (endApplyChangesListeners.isNotEmpty() && endApplyChangesListeners[0] != null) {
@@ -1759,10 +1651,6 @@
override fun onSemanticsChange() {
composeAccessibilityDelegate.onSemanticsChange()
contentCaptureManager.onSemanticsChange()
- @OptIn(ExperimentalComposeUiApi::class)
- if (SDK_INT >= 26 && isSemanticAutofillEnabled) {
- _autofillManager?.onSemanticsChange()
- }
}
override fun onLayoutChange(layoutNode: LayoutNode) {
@@ -1770,8 +1658,6 @@
contentCaptureManager.onLayoutChange(layoutNode)
}
- override val rectManager = RectManager(layoutNodes)
-
override fun onLayoutNodeDeactivated(layoutNode: LayoutNode) {
@OptIn(ExperimentalComposeUiApi::class)
if (ComposeUiFlags.isRectTrackingEnabled) {
@@ -1986,9 +1872,8 @@
viewTreeObserver.addOnScrollChangedListener(scrollChangedListener)
viewTreeObserver.addOnTouchModeChangeListener(touchModeChangeListener)
- if (SDK_INT >= S) {
- AndroidComposeViewTranslationCallbackS.setViewTranslationCallback(this)
- }
+ if (SDK_INT >= S) AndroidComposeViewTranslationCallbackS.setViewTranslationCallback(this)
+ if (autofillSupported()) _autofillManager?.let { semanticsOwner.listeners += it }
}
override fun onDetachedFromWindow() {
@@ -2013,25 +1898,26 @@
viewTreeObserver.removeOnTouchModeChangeListener(touchModeChangeListener)
if (SDK_INT >= S) AndroidComposeViewTranslationCallbackS.clearViewTranslationCallback(this)
+ if (autofillSupported()) _autofillManager?.let { semanticsOwner.listeners -= it }
}
- @OptIn(ExperimentalComposeUiApi::class)
override fun onProvideAutofillVirtualStructure(structure: ViewStructure?, flags: Int) {
if (autofillSupported() && structure != null) {
- if (isSemanticAutofillEnabled) {
+ if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isSemanticAutofillEnabled) {
_autofillManager?.populateViewStructure(structure)
} else {
+ // TODO(b/383201236): Remove _autofill and route requests through _autofillManager.
_autofill?.populateViewStructure(structure)
}
}
}
- @OptIn(ExperimentalComposeUiApi::class)
override fun autofill(values: SparseArray<AutofillValue>) {
if (autofillSupported()) {
- if (isSemanticAutofillEnabled) {
+ if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isSemanticAutofillEnabled) {
_autofillManager?.performAutofill(values)
} else {
+ // TODO(b/383201236): Remove _autofill and route requests through _autofillManager.
_autofill?.performAutofill(values)
}
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/FocusFinderCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/FocusFinderCompat.android.kt
deleted file mode 100644
index 9a42b02..0000000
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/FocusFinderCompat.android.kt
+++ /dev/null
@@ -1,462 +0,0 @@
-/*
- * Copyright 2024 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
- *
- * http://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 androidx.compose.ui.platform
-
-import android.annotation.SuppressLint
-import android.content.pm.PackageManager
-import android.graphics.Rect
-import android.os.Build
-import android.view.View
-import android.view.View.FOCUS_BACKWARD
-import android.view.View.FOCUS_FORWARD
-import android.view.ViewGroup
-import androidx.collection.MutableObjectList
-import androidx.collection.ObjectList
-import androidx.collection.mutableObjectIntMapOf
-import androidx.collection.mutableScatterMapOf
-import androidx.collection.mutableScatterSetOf
-import androidx.core.view.isVisible
-import java.util.Collections
-
-/**
- * On devices before [Build.VERSION_CODES.O], [FocusFinder] orders Views incorrectly. This
- * implementation uses the current [FocusFinder] behavior, ordering Views correctly for
- * one-dimensional focus searches.
- *
- * This is copied and simplified from FocusFinder's source. There may be some code that doesn't look
- * quite right in Kotlin as it was copy/pasted with auto-translation.
- */
-internal class FocusFinderCompat {
- companion object {
- private val FocusFinderThreadLocal =
- object : ThreadLocal<FocusFinderCompat>() {
- override fun initialValue(): FocusFinderCompat {
- return FocusFinderCompat()
- }
- }
-
- /** Get the focus finder for this thread. */
- val instance: FocusFinderCompat
- get() = FocusFinderThreadLocal.get()!!
- }
-
- private val focusedRect: Rect = Rect()
-
- private val userSpecifiedFocusComparator =
- UserSpecifiedFocusComparator({ r, v ->
- if (isValidId(v.nextFocusForwardId)) v.findUserSetNextFocus(r, FOCUS_FORWARD) else null
- })
-
- private val tmpList = MutableObjectList<View>()
-
- // enforce thread local access
- private fun FocusFinder() {}
-
- /**
- * Find the next view to take focus in root's descendants, starting from the view that currently
- * is focused.
- *
- * @param root Contains focused. Cannot be null.
- * @param focused Has focus now.
- * @param direction Direction to look.
- * @return The next focusable view, or null if none exists.
- */
- fun findNextFocus1d(root: ViewGroup, focused: View, direction: Int): View? {
- val effectiveRoot = getEffectiveRoot(root, focused)
- var next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction)
- if (next != null) {
- return next
- }
- val focusables = tmpList
- try {
- focusables.clear()
- effectiveRoot.addFocusableViews(focusables, direction)
- if (!focusables.isEmpty()) {
- next = findNextFocus(effectiveRoot, focused, direction, focusables)
- }
- } finally {
- focusables.clear()
- }
- return next
- }
-
- /**
- * Returns the "effective" root of a view. The "effective" root is the closest ancestor
- * within-which focus should cycle.
- *
- * For example: normal focus navigation would stay within a ViewGroup marked as
- * touchscreenBlocksFocus and keyboardNavigationCluster until a cluster-jump out.
- *
- * @return the "effective" root of {@param focused}
- */
- private fun getEffectiveRoot(root: ViewGroup, focused: View?): ViewGroup {
- if (focused == null || focused === root) {
- return root
- }
- var effective: ViewGroup? = null
- var nextParent = focused.parent
- while (nextParent is ViewGroup) {
- if (nextParent === root) {
- return effective ?: root
- }
- val vg = nextParent
- if (
- vg.touchscreenBlocksFocus &&
- focused.context.packageManager.hasSystemFeature(
- PackageManager.FEATURE_TOUCHSCREEN
- )
- ) {
- // Don't stop and return here because the cluster could be nested and we only
- // care about the top-most one.
- effective = vg
- }
- nextParent = nextParent.parent
- }
- return root
- }
-
- private fun findNextUserSpecifiedFocus(root: ViewGroup, focused: View, direction: Int): View? {
- // check for user specified next focus
- var userSetNextFocus: View? = focused.findUserSetNextFocus(root, direction)
- var cycleCheck = userSetNextFocus
- var cycleStep = true // we want the first toggle to yield false
- while (userSetNextFocus != null) {
- if (
- userSetNextFocus.isFocusable &&
- userSetNextFocus.visibility == View.VISIBLE &&
- (!userSetNextFocus.isInTouchMode || userSetNextFocus.isFocusableInTouchMode)
- ) {
- return userSetNextFocus
- }
- userSetNextFocus = userSetNextFocus.findUserSetNextFocus(root, direction)
- if ((!cycleStep).also { cycleStep = it }) {
- cycleCheck = cycleCheck?.findUserSetNextFocus(root, direction)
- if (cycleCheck === userSetNextFocus) {
- // found a cycle, user-specified focus forms a loop and none of the views
- // are currently focusable.
- break
- }
- }
- }
- return null
- }
-
- private fun findNextFocus(
- root: ViewGroup,
- focused: View,
- direction: Int,
- focusables: MutableObjectList<View>
- ): View? {
- val focusedRect = focusedRect
- // fill in interesting rect from focused
- focused.getFocusedRect(focusedRect)
- root.offsetDescendantRectToMyCoords(focused, focusedRect)
-
- return findNextFocusInRelativeDirection(focusables, root, focused, direction)
- }
-
- @SuppressLint("AsCollectionCall")
- private fun findNextFocusInRelativeDirection(
- focusables: MutableObjectList<View>,
- root: ViewGroup?,
- focused: View,
- direction: Int
- ): View? {
- try {
- // Note: This sort is stable.
- userSpecifiedFocusComparator.setFocusables(focusables, root!!)
- Collections.sort(focusables.asMutableList(), userSpecifiedFocusComparator)
- } finally {
- userSpecifiedFocusComparator.recycle()
- }
-
- val count = focusables.size
- if (count < 2) {
- return null
- }
- var next: View? = null
- val looped = BooleanArray(1)
- when (direction) {
- FOCUS_FORWARD -> next = getNextFocusable(focused, focusables, count, looped)
- FOCUS_BACKWARD -> next = getPreviousFocusable(focused, focusables, count, looped)
- }
- return next ?: focusables[count - 1]
- }
-
- private fun getNextFocusable(
- focused: View,
- focusables: ObjectList<View>,
- count: Int,
- outLooped: BooleanArray
- ): View? {
- if (count < 2) {
- return null
- }
- val position = focusables.lastIndexOf(focused)
- if (position >= 0 && position + 1 < count) {
- return focusables[position + 1]
- }
- outLooped[0] = true
- return focusables[0]
- }
-
- private fun getPreviousFocusable(
- focused: View?,
- focusables: ObjectList<View>,
- count: Int,
- outLooped: BooleanArray
- ): View? {
- if (count < 2) {
- return null
- }
- if (focused != null) {
- val position = focusables.indexOf(focused)
- if (position > 0) {
- return focusables[position - 1]
- }
- }
- outLooped[0] = true
- return focusables[count - 1]
- }
-
- private fun isValidId(id: Int): Boolean {
- return id != 0 && id != View.NO_ID
- }
-
- /**
- * Sorts views according to any explicitly-specified focus-chains. If there are no explicitly
- * specified focus chains (eg. no nextFocusForward attributes defined), this should be a no-op.
- */
- private class UserSpecifiedFocusComparator(private val mNextFocusGetter: NextFocusGetter) :
- Comparator<View?> {
- private val nextFoci = mutableScatterMapOf<View, View>()
- private val isConnectedTo = mutableScatterSetOf<View>()
- private val headsOfChains = mutableScatterMapOf<View, View>()
- private val originalOrdinal = mutableObjectIntMapOf<View>()
- private var root: View? = null
-
- fun interface NextFocusGetter {
- fun get(root: View, view: View): View?
- }
-
- fun recycle() {
- root = null
- headsOfChains.clear()
- isConnectedTo.clear()
- originalOrdinal.clear()
- nextFoci.clear()
- }
-
- fun setFocusables(focusables: ObjectList<View>, root: View) {
- this.root = root
- focusables.forEachIndexed { index, view -> originalOrdinal[view] = index }
-
- for (i in focusables.indices.reversed()) {
- val view = focusables[i]
- val next = mNextFocusGetter.get(root, view)
- if (next != null && originalOrdinal.containsKey(next)) {
- nextFoci[view] = next
- isConnectedTo.add(next)
- }
- }
-
- for (i in focusables.indices.reversed()) {
- val view = focusables[i]
- val next = nextFoci[view]
- if (next != null && !isConnectedTo.contains(view)) {
- setHeadOfChain(view)
- }
- }
- }
-
- fun setHeadOfChain(head: View) {
- var newHead = head
- var view: View? = newHead
- while (view != null) {
- val otherHead = headsOfChains[view]
- if (otherHead != null) {
- if (otherHead === newHead) {
- return // This view has already had its head set properly
- }
- // A hydra -- multi-headed focus chain (e.g. A->C and B->C)
- // Use the one we've already chosen instead and reset this chain.
- view = newHead
- newHead = otherHead
- }
- headsOfChains[view] = newHead
- view = nextFoci[view]
- }
- }
-
- override fun compare(first: View?, second: View?): Int {
- if (first === second) {
- return 0
- }
- if (first == null) {
- return -1
- }
- if (second == null) {
- return 1
- }
- // Order between views within a chain is immaterial -- next/previous is
- // within a chain is handled elsewhere.
- val firstHead = headsOfChains[first]
- val secondHead = headsOfChains[second]
- if (firstHead === secondHead && firstHead != null) {
- return if (first === firstHead) {
- -1 // first is the head, it should be first
- } else if (second === firstHead) {
- 1 // second is the head, it should be first
- } else if (nextFoci[first] != null) {
- -1 // first is not the end of the chain
- } else {
- 1 // first is end of chain
- }
- }
- val chainedFirst = firstHead ?: first
- val chainedSecond = secondHead ?: second
-
- return if (firstHead != null || secondHead != null) {
- // keep original order between chains
- if (originalOrdinal[chainedFirst] < originalOrdinal[chainedSecond]) -1 else 1
- } else {
- 0
- }
- }
- }
-}
-
-/**
- * If a user manually specified the next view id for a particular direction, use the root to look up
- * the view.
- *
- * @param root The root view of the hierarchy containing this view.
- * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD, or
- * FOCUS_BACKWARD.
- * @return The user specified next view, or null if there is none.
- */
-private fun View.findUserSetNextFocus(root: View, direction: Int): View? {
- when (direction) {
- FOCUS_FORWARD -> {
- val next = nextFocusForwardId
- if (next == View.NO_ID) return null
- return findViewInsideOutShouldExist(root, this, next)
- }
- FOCUS_BACKWARD -> {
- if (id == View.NO_ID) return null
- val startView: View = this
- // Since we have forward links but no backward links, we need to find the view that
- // forward links to this view. We can't just find the view with the specified ID
- // because view IDs need not be unique throughout the tree.
- return root.findViewByPredicateInsideOut(startView) { t ->
- (findViewInsideOutShouldExist(root, t, t.nextFocusForwardId) === startView)
- }
- }
- }
- return null
-}
-
-private fun findViewInsideOutShouldExist(root: View, start: View, id: Int): View? {
- return root.findViewByPredicateInsideOut(start) { it.id == id }
-}
-
-/**
- * Look for a child view that matches the specified predicate, starting with the specified view and
- * its descendants and then recursively searching the ancestors and siblings of that view until this
- * view is reached.
- *
- * This method is useful in cases where the predicate does not match a single unique view (perhaps
- * multiple views use the same id) and we are trying to find the view that is "closest" in scope to
- * the starting view.
- *
- * @param start The view to start from.
- * @param predicate The predicate to evaluate.
- * @return The first view that matches the predicate or null.
- */
-private fun View.findViewByPredicateInsideOut(start: View, predicate: (View) -> Boolean): View? {
- var next = start
- var childToSkip: View? = null
- while (true) {
- val view = next.findViewByPredicateTraversal(predicate, childToSkip)
- if (view != null || next === this) {
- return view
- }
-
- val parent = next.parent
- if (parent == null || parent !is View) {
- return null
- }
-
- childToSkip = next
- next = parent
- }
-}
-
-/**
- * @param predicate The predicate to evaluate.
- * @param childToSkip If not null, ignores this child during the recursive traversal.
- * @return The first view that matches the predicate or null.
- */
-private fun View.findViewByPredicateTraversal(
- predicate: (View) -> Boolean,
- childToSkip: View?
-): View? {
- if (predicate(this)) {
- return this
- }
- if (this is ViewGroup) {
- for (i in 0 until childCount) {
- val child = getChildAt(i)
- if (child !== childToSkip) {
- val v = child.findViewByPredicateTraversal(predicate, childToSkip)
- if (v != null) {
- return v
- }
- }
- }
- }
- return null
-}
-
-private fun View.addFocusableViews(views: MutableObjectList<View>, direction: Int) {
- addFocusableViews(views, direction, isInTouchMode)
-}
-
-/**
- * Older versions of View don't add focusable Views in order. This is a corrected version that adds
- * them in the right order.
- */
-private fun View.addFocusableViews(
- views: MutableObjectList<View>,
- direction: Int,
- inTouchMode: Boolean
-) {
- if (
- isVisible &&
- isFocusable &&
- isEnabled &&
- width > 0 &&
- height > 0 &&
- (!inTouchMode || isFocusableInTouchMode)
- ) {
- views += this
- }
- if (this is ViewGroup) {
- for (i in 0 until childCount) {
- getChildAt(i).addFocusableViews(views, direction, inTouchMode)
- }
- }
-}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/SemanticsUtils.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/SemanticsUtils.android.kt
index 27e6655..e715695 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/SemanticsUtils.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/SemanticsUtils.android.kt
@@ -49,8 +49,6 @@
currentSemanticsNodes: IntObjectMap<SemanticsNodeWithAdjustedBounds>
) {
val unmergedConfig = semanticsNode.unmergedConfig
- // Root node must always be considered visible and sent to listening services
- val isTransparent = if (semanticsNode.isRoot) false else semanticsNode.isTransparent
val children: MutableIntSet = MutableIntSet(semanticsNode.replacedChildren.size)
init {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
index 9c9835e..8b1d1dc9 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
@@ -25,8 +25,6 @@
import android.view.ViewParent
import androidx.compose.runtime.ComposeNodeLifecycleCallback
import androidx.compose.runtime.CompositionContext
-import androidx.compose.ui.ComposeUiFlags
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
@@ -420,10 +418,6 @@
if (view.parent !== this) addView(view)
}
layoutNode.onDetach = { owner ->
- @OptIn(ExperimentalComposeUiApi::class)
- if (ComposeUiFlags.isViewFocusFixEnabled && hasFocus()) {
- owner.focusOwner.clearFocus(true)
- }
(owner as? AndroidComposeView)?.removeAndroidView(this)
removeAllViewsInLayout()
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/FocusGroupNode.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/FocusGroupNode.android.kt
index 8082b9a..0d25269 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/FocusGroupNode.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/FocusGroupNode.android.kt
@@ -22,8 +22,6 @@
import android.view.ViewGroup
import android.view.ViewGroup.FOCUS_DOWN
import android.view.ViewTreeObserver
-import androidx.compose.ui.ComposeUiFlags
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection.Companion.Exit
@@ -88,12 +86,7 @@
val onExit: FocusEnterExitScope.() -> Unit = {
val embeddedView = getEmbeddedView()
- @OptIn(ExperimentalComposeUiApi::class)
- if (ComposeUiFlags.isViewFocusFixEnabled) {
- if (embeddedView.hasFocus() || embeddedView.isFocused) {
- embeddedView.clearFocus()
- }
- } else if (embeddedView.hasFocus()) {
+ if (embeddedView.hasFocus()) {
val focusOwner = requireOwner().focusOwner
val hostView = requireView()
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/spatial/ThrottledCallbacksTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/spatial/ThrottledCallbacksTest.kt
index ed03be2..fb323fe2 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/spatial/ThrottledCallbacksTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/spatial/ThrottledCallbacksTest.kt
@@ -223,7 +223,7 @@
id: Int,
throttleMs: Long,
debounceMs: Long,
- callback: (RectInfo) -> Unit
+ callback: (RelativeLayoutBounds) -> Unit
): DisposableHandle {
return registerOnRectChanged(id, throttleMs, debounceMs, fakeNode(), callback)
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt
index d518673..728a1bb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt
@@ -80,10 +80,4 @@
* the new Autofill APIs and features introduced.
*/
@Suppress("MutableBareField") @JvmField var isSemanticAutofillEnabled: Boolean = false
-
- /**
- * This enables fixes for View focus. The changes are large enough to require a flag to allow
- * disabling them.
- */
- @Suppress("MutableBareField") @JvmField var isViewFocusFixEnabled: Boolean = true
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt
index 504d86a..bbfbafe 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt
@@ -16,6 +16,8 @@
package androidx.compose.ui.autofill
+// TODO(b/383201236): This is replaced by ContentType. It was experimental in 1.7 so mark it
+// deprecated before 1.8.
/**
* Autofill type information.
*
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
index d86a6bc..8741918 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
@@ -17,8 +17,6 @@
package androidx.compose.ui.focus
import androidx.collection.MutableLongSet
-import androidx.compose.ui.ComposeUiFlags
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.CustomDestinationResult.Cancelled
import androidx.compose.ui.focus.CustomDestinationResult.None
@@ -204,11 +202,6 @@
* @return true if focus was moved successfully. false if the focused item is unchanged.
*/
override fun moveFocus(focusDirection: FocusDirection): Boolean {
- // First check to see if the focus should move within child Views
- @OptIn(ExperimentalComposeUiApi::class)
- if (ComposeUiFlags.isViewFocusFixEnabled && onMoveFocusInterop(focusDirection)) {
- return true
- }
var requestFocusSuccess: Boolean? = false
val generationBefore = focusTransactionManager.generation
val focusSearchSuccess =
@@ -244,8 +237,7 @@
// If we couldn't move focus within compose, we attempt to move focus within embedded views.
// We don't need this for 1D focus search because the wrap-around logic triggers a
// focus exit which will perform a focus search among the subviews.
- @OptIn(ExperimentalComposeUiApi::class)
- return !ComposeUiFlags.isViewFocusFixEnabled && onMoveFocusInterop(focusDirection)
+ return onMoveFocusInterop(focusDirection)
}
override fun focusSearch(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt
index fe97805..7d1b2d6 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt
@@ -16,9 +16,6 @@
package androidx.compose.ui.focus
-import androidx.compose.ui.ComposeUiFlags
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.focus.CustomDestinationResult.Cancelled
import androidx.compose.ui.focus.CustomDestinationResult.None
import androidx.compose.ui.focus.CustomDestinationResult.RedirectCancelled
@@ -32,7 +29,6 @@
import androidx.compose.ui.node.Nodes.FocusTarget
import androidx.compose.ui.node.nearestAncestor
import androidx.compose.ui.node.observeReads
-import androidx.compose.ui.node.requireLayoutNode
import androidx.compose.ui.node.requireOwner
/**
@@ -62,14 +58,7 @@
}
}
}
- if (success) {
- @OptIn(ExperimentalComposeUiApi::class, InternalComposeUiApi::class)
- if (ComposeUiFlags.isViewFocusFixEnabled && requireLayoutNode().getInteropView() == null) {
- // This isn't an AndroidView, so we should be focused on this ComposeView
- requireOwner().focusOwner.requestFocusForOwner(FocusDirection.Next, null)
- }
- dispatchFocusCallbacks()
- }
+ if (success) dispatchFocusCallbacks()
return success
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnGlobalLayoutListener.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnGlobalLayoutListener.kt
index 55a4457..e65c8c9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnGlobalLayoutListener.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnGlobalLayoutListener.kt
@@ -19,52 +19,52 @@
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.requireLayoutNode
import androidx.compose.ui.node.requireOwner
-import androidx.compose.ui.spatial.RectInfo
+import androidx.compose.ui.spatial.RelativeLayoutBounds
import kotlinx.coroutines.DisposableHandle
/**
* Registers a [callback] to be executed with the position of this modifier node relative to the
* coordinate system of the root of the composition, as well as in screen coordinates and window
- * coordinates, see [RectInfo].
+ * coordinates, see [RelativeLayoutBounds].
*
* It may also be used to calculate certain Layout relationships at the time of the callback
- * execution, such as [RectInfo.calculateOcclusions].
+ * execution, such as [RelativeLayoutBounds.calculateOcclusions].
*
* This will be called after layout pass. This API allows for throttling and debouncing parameters
* in order to moderate the frequency with which the callback gets invoked during high rates of
* change (e.g. scrolling).
*
- * Specifying [throttleMs] will prevent [callback] from being executed more than once over that time
- * period. Specifying [debounceMs] will delay the execution of [callback] until that amount of time
- * has elapsed without a new position.
+ * Specifying [throttleMillis] will prevent [callback] from being executed more than once over that
+ * time period. Specifying [debounceMillis] will delay the execution of [callback] until that amount
+ * of time has elapsed without a new position.
*
- * Specifying 0 for both [throttleMs] and [debounceMs] will result in the callback being executed
- * every time the position has changed. Specifying non-zero amounts for both will result in both
- * conditions being met.
+ * Specifying 0 for both [throttleMillis] and [debounceMillis] will result in the callback being
+ * executed every time the position has changed. Specifying non-zero amounts for both will result in
+ * both conditions being met.
*
- * @param throttleMs The duration, in milliseconds, to prevent [callback] from being executed more
- * than once over that time period.
- * @param debounceMs The duration, in milliseconds, to delay the execution of [callback] until that
- * amount of time has elapsed without a new position.
- * @param callback The callback to be executed, provides a new [RectInfo] instance associated to
- * this [DelegatableNode]. Keep in mind this callback is executed on the main thread even when
- * debounced.
+ * @param throttleMillis The duration, in milliseconds, to prevent [callback] from being executed
+ * more than once over that time period.
+ * @param debounceMillis The duration, in milliseconds, to delay the execution of [callback] until
+ * that amount of time has elapsed without a new position.
+ * @param callback The callback to be executed, provides a new [RelativeLayoutBounds] instance
+ * associated to this [DelegatableNode]. Keep in mind this callback is executed on the main thread
+ * even when debounced.
* @return an object which should be used to unregister/dispose this callback, such as when a node
* is detached
*/
@Suppress("PairedRegistration") // User expected to handle disposing
fun DelegatableNode.registerOnGlobalLayoutListener(
- throttleMs: Int,
- debounceMs: Int,
- callback: (RectInfo) -> Unit
+ throttleMillis: Long,
+ debounceMillis: Long,
+ callback: (RelativeLayoutBounds) -> Unit
): DisposableHandle {
val layoutNode = requireLayoutNode()
val id = layoutNode.semanticsId
val rectManager = layoutNode.requireOwner().rectManager
return rectManager.registerOnGlobalLayoutCallback(
id = id,
- throttleMs = throttleMs,
- debounceMs = debounceMs,
+ throttleMillis = throttleMillis,
+ debounceMillis = debounceMillis,
node = node,
callback = callback
)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnLayoutRectChangedModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnLayoutRectChangedModifier.kt
new file mode 100644
index 0000000..37a7f0f
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnLayoutRectChangedModifier.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.compose.ui.layout
+
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.DelegatableNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.requireLayoutNode
+import androidx.compose.ui.node.requireOwner
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.spatial.RelativeLayoutBounds
+import kotlinx.coroutines.DisposableHandle
+
+/**
+ * Invokes [callback] with the position of this layout node relative to the coordinate system of the
+ * root of the composition, as well as in screen coordinates and window coordinates. This will be
+ * called after layout pass. This API allows for throttling and debouncing parameters in order to
+ * moderate the frequency with which the callback gets invoked during high rates of change (e.g.
+ * scrolling).
+ *
+ * Specifying [throttleMillis] will prevent [callback] from being executed more than once over that
+ * time period. Specifying [debounceMillis] will delay the execution of [callback] until that amount
+ * of time has elapsed without a new position, scheduling the callback to be executed when that
+ * amount of time expires.
+ *
+ * Specifying 0 for both [throttleMillis] and [debounceMillis] will result in the callback being
+ * executed every time the position has changed. Specifying non-zero amounts for both will result in
+ * both conditions being met. Specifying a non-zero [throttleMillis] but a zero [debounceMillis] is
+ * equivalent to providing the same value for both [throttleMillis] and [debounceMillis].
+ *
+ * @param throttleMillis The duration, in milliseconds, to prevent [callback] from being executed
+ * more than once over that time period.
+ * @param debounceMillis The duration, in milliseconds, to delay the execution of [callback] until
+ * that amount of time has elapsed without a new position.
+ * @param callback The callback to be executed.
+ * @see RelativeLayoutBounds
+ * @see onGloballyPositioned
+ * @see registerOnLayoutRectChanged
+ */
+@Stable
+fun Modifier.onLayoutRectChanged(
+ throttleMillis: Long = 0,
+ debounceMillis: Long = 64,
+ callback: (RelativeLayoutBounds) -> Unit
+) = this then OnLayoutRectChangedElement(throttleMillis, debounceMillis, callback)
+
+private data class OnLayoutRectChangedElement(
+ val throttleMillis: Long,
+ val debounceMillis: Long,
+ val callback: (RelativeLayoutBounds) -> Unit
+) : ModifierNodeElement<OnLayoutRectChangedNode>() {
+ override fun create() = OnLayoutRectChangedNode(throttleMillis, debounceMillis, callback)
+
+ override fun update(node: OnLayoutRectChangedNode) {
+ node.throttleMillis = throttleMillis
+ node.debounceMillis = debounceMillis
+ node.callback = callback
+ node.disposeAndRegister()
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "onRectChanged"
+ properties["throttleMillis"] = throttleMillis
+ properties["debounceMillis"] = debounceMillis
+ properties["callback"] = callback
+ }
+}
+
+private class OnLayoutRectChangedNode(
+ var throttleMillis: Long,
+ var debounceMillis: Long,
+ var callback: (RelativeLayoutBounds) -> Unit,
+) : Modifier.Node() {
+ var handle: DisposableHandle? = null
+
+ fun disposeAndRegister() {
+ handle?.dispose()
+ handle = registerOnLayoutRectChanged(throttleMillis, debounceMillis, callback)
+ }
+
+ override fun onAttach() {
+ disposeAndRegister()
+ }
+
+ override fun onDetach() {
+ handle?.dispose()
+ }
+}
+
+/**
+ * Registers a [callback] to be executed with the position of this modifier node relative to the
+ * coordinate system of the root of the composition, as well as in screen coordinates and window
+ * coordinates. This will be called after layout pass. This API allows for throttling and debouncing
+ * parameters in order to moderate the frequency with which the callback gets invoked during high
+ * rates of change (e.g. scrolling).
+ *
+ * Specifying [throttleMillis] will prevent [callback] from being executed more than once over that
+ * time period. Specifying [debounceMillis] will delay the execution of [callback] until that amount
+ * of time has elapsed without a new position.
+ *
+ * Specifying 0 for both [throttleMillis] and [debounceMillis] will result in the callback being
+ * executed every time the position has changed. Specifying non-zero amounts for both will result in
+ * both conditions being met.
+ *
+ * @param throttleMillis The duration, in milliseconds, to prevent [callback] from being executed
+ * more than once over that time period.
+ * @param debounceMillis The duration, in milliseconds, to delay the execution of [callback] until
+ * that amount of time has elapsed without a new position.
+ * @param callback The callback to be executed.
+ * @return an object which should be used to unregister/dispose this callback
+ * @see onLayoutRectChanged
+ */
+fun DelegatableNode.registerOnLayoutRectChanged(
+ throttleMillis: Long,
+ debounceMillis: Long,
+ callback: (RelativeLayoutBounds) -> Unit,
+): DisposableHandle {
+ val layoutNode = requireLayoutNode()
+ val id = layoutNode.semanticsId
+ val rectManager = layoutNode.requireOwner().rectManager
+ return rectManager.registerOnRectChangedCallback(
+ id,
+ throttleMillis,
+ debounceMillis,
+ this,
+ callback,
+ )
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRectChangedModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRectChangedModifier.kt
deleted file mode 100644
index 443b6ee..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRectChangedModifier.kt
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright 2024 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
- *
- * http://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 androidx.compose.ui.layout
-
-import androidx.compose.runtime.Stable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.node.DelegatableNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.node.requireLayoutNode
-import androidx.compose.ui.node.requireOwner
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.spatial.RectInfo
-import kotlinx.coroutines.DisposableHandle
-
-/**
- * Invokes [callback] with the position of this layout node relative to the coordinate system of the
- * root of the composition, as well as in screen coordinates and window coordinates. This will be
- * called after layout pass. This API allows for throttling and debouncing parameters in order to
- * moderate the frequency with which the callback gets invoked during high rates of change (e.g.
- * scrolling).
- *
- * Specifying [throttleMs] will prevent [callback] from being executed more than once over that time
- * period. Specifying [debounceMs] will delay the execution of [callback] until that amount of time
- * has elapsed without a new position, scheduling the callback to be executed when that amount of
- * time expires.
- *
- * Specifying 0 for both [throttleMs] and [debounceMs] will result in the callback being executed
- * every time the position has changed. Specifying non-zero amounts for both will result in both
- * conditions being met. Specifying a non-zero [throttleMs] but a zero [debounceMs] is equivalent to
- * providing the same value for both [throttleMs] and [debounceMs].
- *
- * @param throttleMs The duration, in milliseconds, to prevent [callback] from being executed more
- * than once over that time period.
- * @param debounceMs The duration, in milliseconds, to delay the execution of [callback] until that
- * amount of time has elapsed without a new position.
- * @param callback The callback to be executed.
- * @see RectInfo
- * @see onGloballyPositioned
- * @see registerOnRectChanged
- */
-@Stable
-fun Modifier.onRectChanged(
- throttleMs: Int = 0,
- debounceMs: Int = 64,
- callback: (RectInfo) -> Unit
-) = this then OnRectChangedElement(throttleMs, debounceMs, callback)
-
-private data class OnRectChangedElement(
- val throttleMs: Int,
- val debounceMs: Int,
- val callback: (RectInfo) -> Unit
-) : ModifierNodeElement<OnRectChangedNode>() {
- override fun create() = OnRectChangedNode(throttleMs, debounceMs, callback)
-
- override fun update(node: OnRectChangedNode) {
- node.throttleMs = throttleMs
- node.debounceMs = debounceMs
- node.callback = callback
- node.disposeAndRegister()
- }
-
- override fun InspectorInfo.inspectableProperties() {
- name = "onRectChanged"
- properties["throttleMs"] = throttleMs
- properties["debounceMs"] = debounceMs
- properties["callback"] = callback
- }
-}
-
-private class OnRectChangedNode(
- var throttleMs: Int,
- var debounceMs: Int,
- var callback: (RectInfo) -> Unit,
-) : Modifier.Node() {
- var handle: DisposableHandle? = null
-
- fun disposeAndRegister() {
- handle?.dispose()
- handle = registerOnRectChanged(throttleMs, debounceMs, callback)
- }
-
- override fun onAttach() {
- disposeAndRegister()
- }
-
- override fun onDetach() {
- handle?.dispose()
- }
-}
-
-/**
- * Registers a [callback] to be executed with the position of this modifier node relative to the
- * coordinate system of the root of the composition, as well as in screen coordinates and window
- * coordinates. This will be called after layout pass. This API allows for throttling and debouncing
- * parameters in order to moderate the frequency with which the callback gets invoked during high
- * rates of change (e.g. scrolling).
- *
- * Specifying [throttleMs] will prevent [callback] from being executed more than once over that time
- * period. Specifying [debounceMs] will delay the execution of [callback] until that amount of time
- * has elapsed without a new position.
- *
- * Specifying 0 for both [throttleMs] and [debounceMs] will result in the callback being executed
- * every time the position has changed. Specifying non-zero amounts for both will result in both
- * conditions being met.
- *
- * @param throttleMs The duration, in milliseconds, to prevent [callback] from being executed more
- * than once over that time period.
- * @param debounceMs The duration, in milliseconds, to delay the execution of [callback] until that
- * amount of time has elapsed without a new position.
- * @param callback The callback to be executed.
- * @return an object which should be used to unregister/dispose this callback
- * @see onRectChanged
- */
-fun DelegatableNode.registerOnRectChanged(
- throttleMs: Int,
- debounceMs: Int,
- callback: (RectInfo) -> Unit,
-): DisposableHandle {
- val layoutNode = requireLayoutNode()
- val id = layoutNode.semanticsId
- val rectManager = layoutNode.requireOwner().rectManager
- return rectManager.registerOnRectChangedCallback(
- id,
- throttleMs,
- debounceMs,
- this,
- callback,
- )
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
index 4c26b36..62c8c45 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
@@ -510,8 +510,9 @@
*/
private fun Placeable.handleMotionFrameOfReferencePlacement() {
if (this is MotionReferencePlacementDelegate) {
- this.isPlacedUnderMotionFrameOfReference =
+ updatePlacedUnderMotionFrameOfReference(
[email protected]
+ )
}
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutAwareModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutAwareModifierNode.kt
index 8f32b3a..23b6f98 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutAwareModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutAwareModifierNode.kt
@@ -52,3 +52,8 @@
*/
fun onRemeasured(size: IntSize) {}
}
+
+// TODO(b/309776096): Make it public
+internal interface OnUnplacedModifierNode : DelegatableNode {
+ fun onUnplaced()
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 623b582..bc99aece 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -398,6 +398,8 @@
invalidateMeasurements()
}
+ override fun isTransparent(): Boolean = outerCoordinator.isTransparent()
+
private var isSemanticsInvalidated = false
internal fun invalidateSemantics() {
@@ -622,10 +624,6 @@
return _zSortedChildren
}
- @Suppress("UNCHECKED_CAST")
- override val childrenInfo: MutableVector<SemanticsInfo>
- get() = zSortedChildren as MutableVector<SemanticsInfo>
-
override val isValidOwnerScope: Boolean
get() = isAttached
@@ -1357,6 +1355,9 @@
override val parentInfo: SemanticsInfo?
get() = parent
+ override val childrenInfo: List<SemanticsInfo>
+ get() = children
+
override var isDeactivated = false
private set
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
index 667b10f..5b0f57d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
@@ -536,6 +536,10 @@
if (isPlaced) {
isPlaced = false
layoutNode.forEachCoordinatorIncludingInner {
+ // TODO(b/309776096): Node can be detached without calling this, so we need to
+ // find a better place to more reliable call this.
+ it.onUnplaced()
+
// nodes are not placed with a layer anymore, so the layers should be released
it.releaseLayer()
}
@@ -817,16 +821,17 @@
private var needsCoordinatesUpdate = false
override var isPlacedUnderMotionFrameOfReference: Boolean = false
- set(new) {
- // Delegated to outerCoordinator
- val old = outerCoordinator.isPlacedUnderMotionFrameOfReference
- if (new != old) {
- outerCoordinator.isPlacedUnderMotionFrameOfReference = old
- // Affects coordinates measurements
- this.needsCoordinatesUpdate = true
- }
- field = new
+
+ override fun updatePlacedUnderMotionFrameOfReference(newMFR: Boolean) {
+ // Delegated to outerCoordinator
+ val old = outerCoordinator.isPlacedUnderMotionFrameOfReference
+ if (newMFR != old) {
+ outerCoordinator.isPlacedUnderMotionFrameOfReference = newMFR
+ // Affects coordinates measurements
+ this.needsCoordinatesUpdate = true
}
+ isPlacedUnderMotionFrameOfReference = newMFR
+ }
private fun placeSelf(
position: IntOffset,
@@ -939,21 +944,53 @@
}
override fun minIntrinsicWidth(height: Int): Int {
+ // If there is an intrinsic size query coming from above the lookahead root, we will
+ // direct the query down to the lookahead pass. Note, when a regular measure call
+ // reaches a top-level lookahead root, the measure call is turned into lookahead
+ // measure followed by approach measure. This is a similar, although not exactly the
+ // same, mental model.
+ if (layoutNode.isOutMostLookaheadRoot) {
+ return lookaheadPassDelegate!!.minIntrinsicWidth(height)
+ }
onIntrinsicsQueried()
return outerCoordinator.minIntrinsicWidth(height)
}
override fun maxIntrinsicWidth(height: Int): Int {
+ // If there is an intrinsic size query coming from above the lookahead root, we will
+ // direct the query down to the lookahead pass. Note, when a regular measure call
+ // reaches a top-level lookahead root, the measure call is turned into lookahead
+ // measure followed by approach measure. This is a similar, although not exactly the
+ // same, mental model.
+ if (layoutNode.isOutMostLookaheadRoot) {
+ return lookaheadPassDelegate!!.maxIntrinsicWidth(height)
+ }
onIntrinsicsQueried()
return outerCoordinator.maxIntrinsicWidth(height)
}
override fun minIntrinsicHeight(width: Int): Int {
+ // If there is an intrinsic size query coming from above the lookahead root, we will
+ // direct the query down to the lookahead pass. Note, when a regular measure call
+ // reaches a top-level lookahead root, the measure call is turned into lookahead
+ // measure followed by approach measure. This is a similar, although not exactly the
+ // same, mental model.
+ if (layoutNode.isOutMostLookaheadRoot) {
+ return lookaheadPassDelegate!!.minIntrinsicHeight(width)
+ }
onIntrinsicsQueried()
return outerCoordinator.minIntrinsicHeight(width)
}
override fun maxIntrinsicHeight(width: Int): Int {
+ // If there is an intrinsic size query coming from above the lookahead root, we will
+ // direct the query down to the lookahead pass. Note, when a regular measure call
+ // reaches a top-level lookahead root, the measure call is turned into lookahead
+ // measure followed by approach measure. This is a similar, although not exactly the
+ // same, mental model.
+ if (layoutNode.isOutMostLookaheadRoot) {
+ return lookaheadPassDelegate!!.maxIntrinsicHeight(width)
+ }
onIntrinsicsQueried()
return outerCoordinator.maxIntrinsicHeight(width)
}
@@ -1536,14 +1573,15 @@
}
override var isPlacedUnderMotionFrameOfReference: Boolean = false
- set(new) {
- // Delegated to outerCoordinator
- val old = outerCoordinator.lookaheadDelegate?.isPlacedUnderMotionFrameOfReference
- if (new != old) {
- outerCoordinator.lookaheadDelegate?.isPlacedUnderMotionFrameOfReference = new
- }
- field = new
+
+ override fun updatePlacedUnderMotionFrameOfReference(newMFR: Boolean) {
+ // Delegated to outerCoordinator
+ val old = outerCoordinator.lookaheadDelegate?.isPlacedUnderMotionFrameOfReference
+ if (newMFR != old) {
+ outerCoordinator.lookaheadDelegate?.isPlacedUnderMotionFrameOfReference = newMFR
}
+ isPlacedUnderMotionFrameOfReference = newMFR
+ }
private fun placeSelf(
position: IntOffset,
@@ -2050,20 +2088,33 @@
* [LookaheadCapablePlaceable.isPlacedUnderMotionFrameOfReference] to the proper placeable.
*/
internal interface MotionReferencePlacementDelegate {
-
/**
* Called when a layout is about to be placed.
*
- * The corresponding [LookaheadCapablePlaceable] should have their
- * [LookaheadCapablePlaceable.isPlacedUnderMotionFrameOfReference] flag updated to the given
- * value.
+ * This updates the corresponding [LookaheadCapablePlaceable]'s
+ * [LookaheadCapablePlaceable.isPlacedUnderMotionFrameOfReference] flag IF AND ONLY IF the
+ * placement call comes from parent [LookaheadCapablePlaceable]. More specifically, for
+ * [LookaheadCapablePlaceable] that are the head of the modifier chain (e.g. outerCoordinator),
+ * the placement call doesn't always come from the parent, as the node can be independently
+ * replaced. For these [LookaheadCapablePlaceable], we maintain the old
+ * [isPlacedUnderMotionFrameOfReference] until the next placement call comes from the parent.
+ * This reason is that only placement from parent runs the placement lambda where
+ * [Placeable.PlacementScope.withMotionFrameOfReferencePlacement] is invoked. Also note, for
+ * [LookaheadCapablePlaceable] that are not the head of the modifier chain, the placement call
+ * always comes from the parent.
*
* The placeable should be tagged such that its corresponding coordinates reflect the flag in
* [androidx.compose.ui.layout.LayoutCoordinates.introducesMotionFrameOfReference]. Note that
* when it's placed on the current frame of reference, it means it doesn't introduce a new frame
* of reference.
- *
- * This also means that coordinates consumers (onPlaced readers) are expected to be updated.
*/
- var isPlacedUnderMotionFrameOfReference: Boolean
+ fun updatePlacedUnderMotionFrameOfReference(newMFR: Boolean)
+
+ /**
+ * Flag to indicate whether the [MotionReferencePlacementDelegate] is being placed under motion
+ * frame of reference. This is also reflected in
+ * [androidx.compose.ui.layout.LayoutCoordinates.introducesMotionFrameOfReference], which is
+ * used for local position calculation between coordinates.
+ */
+ val isPlacedUnderMotionFrameOfReference: Boolean
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
index 708fec5..e74274b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
@@ -61,6 +61,24 @@
*/
override var isPlacedUnderMotionFrameOfReference: Boolean = false
+ override fun updatePlacedUnderMotionFrameOfReference(newMFR: Boolean) {
+ val parentNode = parent?.layoutNode
+ if (parentNode == layoutNode) {
+ isPlacedUnderMotionFrameOfReference = newMFR
+ } else {
+ // This node is the beginning of the chain (i.e. outerCoordinator), check if this
+ // placement call comes from the parent
+ if (
+ parentNode?.layoutState == LayoutNode.LayoutState.LayingOut ||
+ parentNode?.layoutState == LayoutNode.LayoutState.LookaheadLayingOut
+ ) {
+ isPlacedUnderMotionFrameOfReference = newMFR
+ }
+ // If the node is simply being replaced without parent, we need to maintain the flag
+ // from last time when `placeChildren` lambda was run. Therefore no op.
+ }
+ }
+
val rulerScope: RulerScope
get() {
return _rulerScope
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
index f102b02..a59cb92 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
@@ -320,6 +320,12 @@
}
}
+ fun onUnplaced() {
+ if (hasNode(Nodes.Unplaced)) {
+ visitNodes(Nodes.Unplaced) { it.onUnplaced() }
+ }
+ }
+
/** Places the modified child. */
/*@CallSuper*/
override fun placeAt(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
index ec063bb..d204355 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
@@ -147,6 +147,10 @@
@JvmStatic
inline val BringIntoView
get() = NodeKind<BringIntoViewModifierNode>(0b1 shl 19)
+
+ @JvmStatic
+ inline val Unplaced
+ get() = NodeKind<OnUnplacedModifierNode>(0b1 shl 20)
// ...
}
@@ -253,6 +257,9 @@
if (node is BringIntoViewModifierNode) {
mask = mask or Nodes.BringIntoView
}
+ if (node is OnUnplacedModifierNode) {
+ mask = mask or Nodes.Unplaced
+ }
mask
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsInfo.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsInfo.kt
index d6849c4..5a471e9a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsInfo.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsInfo.kt
@@ -16,10 +16,8 @@
package androidx.compose.ui.semantics
-import androidx.compose.runtime.collection.MutableVector
+import androidx.collection.MutableObjectList
import androidx.compose.ui.layout.LayoutInfo
-import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.node.LayoutNode
/**
* This is an internal interface that can be used by [SemanticsListener]s to read semantic
@@ -31,6 +29,9 @@
/** The semantics configuration (Semantic properties and actions) associated with this node. */
val semanticsConfiguration: SemanticsConfiguration?
+ /** Whether the node is transparent. */
+ fun isTransparent(): Boolean
+
/**
* The [SemanticsInfo] of the parent.
*
@@ -39,13 +40,12 @@
override val parentInfo: SemanticsInfo?
/**
- * Returns the children list sorted by their [LayoutNode.zIndex] first (smaller first) and the
- * order they were placed via [Placeable.placeAt] by parent (smaller first). Please note that
- * this list contains not placed items as well, so you have to manually filter them.
+ * Returns the list of children.
*
- * Note that the object is reused so you shouldn't save it for later.
+ * Please note that this list contains not placed items as well, so you have to manually filter
+ * them. Note that the object is reused so you shouldn't save it for later.
*/
- val childrenInfo: MutableVector<SemanticsInfo>
+ val childrenInfo: List<SemanticsInfo>
}
/** The semantics parent (nearest ancestor which has semantic properties). */
@@ -68,18 +68,36 @@
return null
}
-internal inline fun SemanticsInfo.findSemanticsChildren(
- includeDeactivated: Boolean = false,
- block: (SemanticsInfo) -> Unit
-) {
- val unvisitedStack = MutableVector<SemanticsInfo>(childrenInfo.size)
- childrenInfo.forEachReversed { unvisitedStack += it }
- while (unvisitedStack.isNotEmpty()) {
- val child = unvisitedStack.removeAt(unvisitedStack.lastIndex)
- when {
- child.isDeactivated && !includeDeactivated -> continue
- child.semanticsConfiguration != null -> block(child)
- else -> child.childrenInfo.forEachReversed { unvisitedStack += it }
- }
+/** Merges the semantics of all the children of this node into a single SemanticsConfiguration. */
+internal fun SemanticsInfo.mergedSemanticsConfiguration(): SemanticsConfiguration? {
+ val unMergedConfig = semanticsConfiguration
+ if (
+ unMergedConfig == null ||
+ !unMergedConfig.isMergingSemanticsOfDescendants ||
+ unMergedConfig.isClearingSemantics
+ ) {
+ return unMergedConfig
}
+
+ var mergedConfig: SemanticsConfiguration = unMergedConfig.copy()
+ val needsMerging: MutableObjectList<SemanticsInfo> =
+ MutableObjectList<SemanticsInfo>(childrenInfo.size).apply { addAll(childrenInfo) }
+
+ @Suppress("Range") // isNotEmpty ensures removeAt is not called with -1.
+ while (needsMerging.isNotEmpty()) {
+ val childInfo = needsMerging.removeAt(needsMerging.lastIndex)
+ val childConfig = childInfo.semanticsConfiguration
+
+ // Don't merge children that themselves merge all their descendants (because that
+ // indicates they are independently screen-reader-focusable).
+ if (childConfig == null || childConfig.isMergingSemanticsOfDescendants) continue
+
+ // Merge child values.
+ mergedConfig.mergeChild(childConfig)
+
+ // Merge children (unless this child is clearing semantics).
+ if (!childConfig.isClearingSemantics) needsMerging.addAll(childInfo.childrenInfo)
+ }
+
+ return mergedConfig
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
index 84ae0fc..babb0be 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
@@ -216,7 +216,11 @@
get() {
if (isMergingSemanticsOfDescendants) {
val mergedConfig = unmergedConfig.copy()
- mergeConfig(mutableListOf(), mergedConfig)
+ mergeConfig(
+ // TODO(b/384549982): Pass in the unmerged children instead of an empty list.
+ mutableListOf(),
+ mergedConfig
+ )
return mergedConfig
} else {
return unmergedConfig
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectInfo.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectInfo.kt
deleted file mode 100644
index 723389a..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectInfo.kt
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- * Copyright 2024 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
- *
- * http://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 androidx.compose.ui.spatial
-
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.graphics.Matrix
-import androidx.compose.ui.node.DelegatableNode
-import androidx.compose.ui.node.requireLayoutNode
-import androidx.compose.ui.node.requireOwner
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntRect
-import androidx.compose.ui.unit.roundToIntRect
-
-/**
- * Represents an axis-aligned bounding Rectangle for an element in a compose hierarchy, in the
- * coordinates of either the Root of the compose hierarchy, the Window, or the Screen.
- *
- * @see androidx.compose.ui.layout.onRectChanged
- */
-class RectInfo
-internal constructor(
- private val topLeft: Long,
- private val bottomRight: Long,
- private val windowOffset: IntOffset,
- private val screenOffset: IntOffset,
- private val viewToWindowMatrix: Matrix?,
- private val node: DelegatableNode,
-) {
- /**
- * The top left position of the Rect in the coordinates of the root node of the compose
- * hierarchy.
- */
- val positionInRoot: IntOffset
- get() = IntOffset(topLeft)
-
- /** The top left position of the Rect in the coordinates of the Window it is contained in */
- val positionInWindow: IntOffset
- get() {
- val x = screenOffset.x - windowOffset.x
- val y = screenOffset.y - windowOffset.y
- val l = unpackX(topLeft)
- val t = unpackY(topLeft)
- return IntOffset(l + x, t + y)
- }
-
- /** The top left position of the Rect in the coordinates of the Screen it is contained in. */
- val positionInScreen: IntOffset
- get() {
- val x = screenOffset.x
- val y = screenOffset.y
- val l = unpackX(topLeft)
- val t = unpackY(topLeft)
- return IntOffset(l + x, t + y)
- }
-
- /** The width, in pixels, of the Rect */
- val width: Int
- get() {
- val l = unpackX(topLeft)
- val r = unpackX(bottomRight)
- return r - l
- }
-
- /** The height, in pixels, of the Rect */
- val height: Int
- get() {
- val t = unpackY(topLeft)
- val b = unpackY(bottomRight)
- return b - t
- }
-
- /**
- * The positioned bounding Rect in the coordinates of the root node of the compose hierarchy.
- */
- val rootRect: IntRect
- get() {
- val l = unpackX(topLeft)
- val t = unpackY(topLeft)
- val r = unpackX(bottomRight)
- val b = unpackY(bottomRight)
- return IntRect(l, t, r, b)
- }
-
- /** The positioned bounding Rect in the coordinates of the Window which it is contained in. */
- val windowRect: IntRect
- get() {
- val l = unpackX(topLeft)
- val t = unpackY(topLeft)
- val r = unpackX(bottomRight)
- val b = unpackY(bottomRight)
- if (viewToWindowMatrix != null) {
- // TODO: we could implement a `Matrix.map(l, t, r, b): IntRect` that was only a
- // single allocation if we wanted to. this would avoid the two Rect(FFFF)
- // allocations that we have here.
- return viewToWindowMatrix
- .map(Rect(l.toFloat(), t.toFloat(), r.toFloat(), b.toFloat()))
- .roundToIntRect()
- }
- val x = screenOffset.x - windowOffset.x
- val y = screenOffset.y - windowOffset.y
- return IntRect(l + x, t + y, r + x, b + y)
- }
-
- /** The positioned bounding Rect in the coordinates of the Screen which it is contained in. */
- val screenRect: IntRect
- get() {
- if (viewToWindowMatrix != null) {
- val windowRect = windowRect
- val offset = windowOffset
- return IntRect(
- windowRect.left + offset.x,
- windowRect.top + offset.y,
- windowRect.right + offset.x,
- windowRect.bottom + offset.y,
- )
- }
- val l = unpackX(topLeft)
- val t = unpackY(topLeft)
- val r = unpackX(bottomRight)
- val b = unpackY(bottomRight)
- val x = screenOffset.x
- val y = screenOffset.y
- return IntRect(l + x, t + y, r + x, b + y)
- }
-
- /**
- * At the current state of the layout, calculates which other Composable Layouts are occluding
- * the Composable associated with this [RectInfo]. **Note**: Calling this method during measure
- * or layout may result on calculations with stale (or partially stale) layout information.
- *
- * An occlusion is defined by an intersecting Composable that may draw on top of the target
- * Composable.
- *
- * There's no guarantee that something was actually drawn to occlude, so a transparent
- * Composable that could otherwise draw on top of the target is considered to be occluding.
- *
- * There's no differentiation between partial and complete occlusions, they are all included as
- * part of this calculation.
- *
- * Ancestors, child and grandchild Layouts are never considered to be occluding.
- *
- * @return A [List] of the rectangles that occlude the associated Composable Layout.
- */
- fun calculateOcclusions(): List<IntRect> {
- val rectManager = node.requireOwner().rectManager
- val id = node.requireLayoutNode().semanticsId
- val rectList = rectManager.rects
- val idIndex = rectList.indexOf(id)
- if (idIndex < 0) {
- return emptyList()
- }
- // For the given `id`, finds intersections and determines occlusions by the result of
- // the 'RectManager.isTargetDrawnFirst', if the node for 'id' is drawn first, then it's
- // being occluded and the intersecting rect is added to the list result.
- return buildList {
- rectList.forEachIntersectingRectWithValueAt(idIndex) { l, t, r, b, intersectingId ->
- if (rectManager.isTargetDrawnFirst(id, intersectingId)) {
- [email protected](IntRect(l, t, r, b))
- }
- }
- }
- }
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt
index 6e9cf7f..557e7ae 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt
@@ -138,8 +138,8 @@
dispatchToken = postDelayed(delay, dispatchLambda)
}
- fun currentRectInfo(id: Int, node: DelegatableNode): RectInfo? {
- var result: RectInfo? = null
+ fun currentRectInfo(id: Int, node: DelegatableNode): RelativeLayoutBounds? {
+ var result: RelativeLayoutBounds? = null
rects.withRect(id) { l, t, r, b ->
result =
rectInfoFor(
@@ -161,15 +161,15 @@
fun registerOnRectChangedCallback(
id: Int,
- throttleMs: Int,
- debounceMs: Int,
+ throttleMillis: Long,
+ debounceMillis: Long,
node: DelegatableNode,
- callback: (RectInfo) -> Unit
+ callback: (RelativeLayoutBounds) -> Unit
): DisposableHandle {
return throttledCallbacks.registerOnRectChanged(
id,
- throttleMs.toLong(),
- debounceMs.toLong(),
+ throttleMillis,
+ debounceMillis,
node,
callback,
)
@@ -177,15 +177,15 @@
fun registerOnGlobalLayoutCallback(
id: Int,
- throttleMs: Int,
- debounceMs: Int,
+ throttleMillis: Long,
+ debounceMillis: Long,
node: DelegatableNode,
- callback: (RectInfo) -> Unit
+ callback: (RelativeLayoutBounds) -> Unit
): DisposableHandle {
return throttledCallbacks.registerOnGlobalChange(
id = id,
- throttleMs = throttleMs.toLong(),
- debounceMs = debounceMs.toLong(),
+ throttleMillis = throttleMillis,
+ debounceMillis = debounceMillis,
node = node,
callback = callback,
)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RelativeLayoutBounds.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RelativeLayoutBounds.kt
new file mode 100644
index 0000000..27dcde1
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RelativeLayoutBounds.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.compose.ui.spatial
+
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.node.DelegatableNode
+import androidx.compose.ui.node.requireLayoutNode
+import androidx.compose.ui.node.requireOwner
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.roundToIntRect
+
+/**
+ * Represents an axis-aligned bounding Rectangle for an element in a compose hierarchy, in the
+ * coordinates of either the Root of the compose hierarchy, the Window, or the Screen.
+ *
+ * @see androidx.compose.ui.layout.onLayoutRectChanged
+ */
+class RelativeLayoutBounds
+internal constructor(
+ private val topLeft: Long,
+ private val bottomRight: Long,
+ private val windowOffset: IntOffset,
+ private val screenOffset: IntOffset,
+ private val viewToWindowMatrix: Matrix?,
+ private val node: DelegatableNode,
+) {
+ /**
+ * The top left position of the Rect in the coordinates of the root node of the compose
+ * hierarchy.
+ */
+ val positionInRoot: IntOffset
+ get() = IntOffset(topLeft)
+
+ /** The top left position of the Rect in the coordinates of the Window it is contained in */
+ val positionInWindow: IntOffset
+ get() {
+ val x = screenOffset.x - windowOffset.x
+ val y = screenOffset.y - windowOffset.y
+ val l = unpackX(topLeft)
+ val t = unpackY(topLeft)
+ return IntOffset(l + x, t + y)
+ }
+
+ /** The top left position of the Rect in the coordinates of the Screen it is contained in. */
+ val positionInScreen: IntOffset
+ get() {
+ val x = screenOffset.x
+ val y = screenOffset.y
+ val l = unpackX(topLeft)
+ val t = unpackY(topLeft)
+ return IntOffset(l + x, t + y)
+ }
+
+ /** The width, in pixels, of the Rect */
+ val width: Int
+ get() {
+ val l = unpackX(topLeft)
+ val r = unpackX(bottomRight)
+ return r - l
+ }
+
+ /** The height, in pixels, of the Rect */
+ val height: Int
+ get() {
+ val t = unpackY(topLeft)
+ val b = unpackY(bottomRight)
+ return b - t
+ }
+
+ /**
+ * The positioned bounding Rect in the coordinates of the root node of the compose hierarchy.
+ */
+ val boundsInRoot: IntRect
+ get() {
+ val l = unpackX(topLeft)
+ val t = unpackY(topLeft)
+ val r = unpackX(bottomRight)
+ val b = unpackY(bottomRight)
+ return IntRect(l, t, r, b)
+ }
+
+ /** The positioned bounding Rect in the coordinates of the Window which it is contained in. */
+ val boundsInWindow: IntRect
+ get() {
+ val l = unpackX(topLeft)
+ val t = unpackY(topLeft)
+ val r = unpackX(bottomRight)
+ val b = unpackY(bottomRight)
+ if (viewToWindowMatrix != null) {
+ // TODO: we could implement a `Matrix.map(l, t, r, b): IntRect` that was only a
+ // single allocation if we wanted to. this would avoid the two Rect(FFFF)
+ // allocations that we have here.
+ return viewToWindowMatrix
+ .map(Rect(l.toFloat(), t.toFloat(), r.toFloat(), b.toFloat()))
+ .roundToIntRect()
+ }
+ val x = screenOffset.x - windowOffset.x
+ val y = screenOffset.y - windowOffset.y
+ return IntRect(l + x, t + y, r + x, b + y)
+ }
+
+ /** The positioned bounding Rect in the coordinates of the Screen which it is contained in. */
+ val boundsInScreen: IntRect
+ get() {
+ if (viewToWindowMatrix != null) {
+ val windowRect = boundsInWindow
+ val offset = windowOffset
+ return IntRect(
+ windowRect.left + offset.x,
+ windowRect.top + offset.y,
+ windowRect.right + offset.x,
+ windowRect.bottom + offset.y,
+ )
+ }
+ val l = unpackX(topLeft)
+ val t = unpackY(topLeft)
+ val r = unpackX(bottomRight)
+ val b = unpackY(bottomRight)
+ val x = screenOffset.x
+ val y = screenOffset.y
+ return IntRect(l + x, t + y, r + x, b + y)
+ }
+
+ /**
+ * At the current state of the layout, calculates which other Composable Layouts are occluding
+ * the Composable associated with this [RelativeLayoutBounds]. **Note**: Calling this method
+ * during measure or layout may result on calculations with stale (or partially stale) layout
+ * information.
+ *
+ * An occlusion is defined by an intersecting Composable that may draw on top of the target
+ * Composable.
+ *
+ * There's no guarantee that something was actually drawn to occlude, so a transparent
+ * Composable that could otherwise draw on top of the target is considered to be occluding.
+ *
+ * There's no differentiation between partial and complete occlusions, they are all included as
+ * part of this calculation.
+ *
+ * Ancestors, child and grandchild Layouts are never considered to be occluding.
+ *
+ * @return A [List] of the rectangles that occlude the associated Composable Layout.
+ */
+ fun calculateOcclusions(): List<IntRect> {
+ val rectManager = node.requireOwner().rectManager
+ val id = node.requireLayoutNode().semanticsId
+ val rectList = rectManager.rects
+ val idIndex = rectList.indexOf(id)
+ if (idIndex < 0) {
+ return emptyList()
+ }
+ // For the given `id`, finds intersections and determines occlusions by the result of
+ // the 'RectManager.isTargetDrawnFirst', if the node for 'id' is drawn first, then it's
+ // being occluded and the intersecting rect is added to the list result.
+ return buildList {
+ rectList.forEachIntersectingRectWithValueAt(idIndex) { l, t, r, b, intersectingId ->
+ if (rectManager.isTargetDrawnFirst(id, intersectingId)) {
+ [email protected](IntRect(l, t, r, b))
+ }
+ }
+ }
+ }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/ThrottledCallbacks.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/ThrottledCallbacks.kt
index b0f577b..54e02c3 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/ThrottledCallbacks.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/ThrottledCallbacks.kt
@@ -31,7 +31,7 @@
internal class ThrottledCallbacks {
/**
- * Entry for a throttled callback for [RectInfo] associated to the given [node].
+ * Entry for a throttled callback for [RelativeLayoutBounds] associated to the given [node].
*
* Supports a linked-list structure for multiple callbacks on the same [node] through [next].
*/
@@ -40,7 +40,7 @@
val throttleMillis: Long,
val debounceMillis: Long,
val node: DelegatableNode,
- val callback: (RectInfo) -> Unit,
+ val callback: (RelativeLayoutBounds) -> Unit,
) : DisposableHandle {
var next: Entry? = null
@@ -119,21 +119,21 @@
fun registerOnRectChanged(
id: Int,
- throttleMs: Long,
- debounceMs: Long,
+ throttleMillis: Long,
+ debounceMillis: Long,
node: DelegatableNode,
- callback: (RectInfo) -> Unit,
+ callback: (RelativeLayoutBounds) -> Unit,
): DisposableHandle {
// If zero is set for debounce, we use throttle in its place. This guarantees that
// consumers will get the value where the node "settled".
- val debounceToUse = if (debounceMs == 0L) throttleMs else debounceMs
+ val debounceToUse = if (debounceMillis == 0L) throttleMillis else debounceMillis
return rectChangedMap.multiPut(
key = id,
value =
Entry(
id = id,
- throttleMillis = throttleMs,
+ throttleMillis = throttleMillis,
debounceMillis = debounceToUse,
node = node,
callback = callback,
@@ -143,19 +143,19 @@
fun registerOnGlobalChange(
id: Int,
- throttleMs: Long,
- debounceMs: Long,
+ throttleMillis: Long,
+ debounceMillis: Long,
node: DelegatableNode,
- callback: (RectInfo) -> Unit,
+ callback: (RelativeLayoutBounds) -> Unit,
): DisposableHandle {
// If zero is set for debounce, we use throttle in its place. This guarantees that
// consumers will get the value where the node "settled".
- val debounceToUse = if (debounceMs == 0L) throttleMs else debounceMs
+ val debounceToUse = if (debounceMillis == 0L) throttleMillis else debounceMillis
val entry =
Entry(
id = id,
- throttleMillis = throttleMs,
+ throttleMillis = throttleMillis,
debounceMillis = debounceToUse,
node = node,
callback = callback,
@@ -455,7 +455,7 @@
windowOffset: IntOffset,
screenOffset: IntOffset,
viewToWindowMatrix: Matrix?,
-): RectInfo? {
+): RelativeLayoutBounds? {
val coordinator = node.requireCoordinator(Nodes.Layout)
val layoutNode = node.requireLayoutNode()
if (!layoutNode.isPlaced) return null
@@ -467,7 +467,7 @@
val needsTransform = layoutNode.outerCoordinator !== coordinator
return if (needsTransform) {
val transformed = layoutNode.outerCoordinator.coordinates.localBoundingBoxOf(coordinator)
- RectInfo(
+ RelativeLayoutBounds(
transformed.topLeft.round().packedValue,
transformed.bottomRight.round().packedValue,
windowOffset,
@@ -476,7 +476,7 @@
node,
)
} else
- RectInfo(
+ RelativeLayoutBounds(
topLeft,
bottomRight,
windowOffset,
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/compose-benchmark/build.gradle b/constraintlayout/constraintlayout-compose/integration-tests/compose-benchmark/build.gradle
index d821fc0..9e1b39c 100644
--- a/constraintlayout/constraintlayout-compose/integration-tests/compose-benchmark/build.gradle
+++ b/constraintlayout/constraintlayout-compose/integration-tests/compose-benchmark/build.gradle
@@ -23,11 +23,11 @@
}
dependencies {
- androidTestImplementation project(":constraintlayout:constraintlayout-compose")
- androidTestImplementation project(":constraintlayout:constraintlayout-core")
- androidTestImplementation project(":benchmark:benchmark-junit4")
- androidTestImplementation project(":compose:runtime:runtime")
- androidTestImplementation project(":compose:benchmark-utils")
+ androidTestImplementation(project(":constraintlayout:constraintlayout-compose"))
+ androidTestImplementation(project(":constraintlayout:constraintlayout-core"))
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:benchmark-utils"))
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
index 75e2599..94ccc7b 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
@@ -2775,7 +2775,6 @@
*
* @param params the Layout Params to be copied
*/
- @SuppressLint("ClassVerificationFailure")
public LayoutParams(ViewGroup.LayoutParams params) {
super(params);
diff --git a/coordinatorlayout/coordinatorlayout/build.gradle b/coordinatorlayout/coordinatorlayout/build.gradle
index d5677b9..352cb89 100644
--- a/coordinatorlayout/coordinatorlayout/build.gradle
+++ b/coordinatorlayout/coordinatorlayout/build.gradle
@@ -36,12 +36,6 @@
}
android {
- sourceSets {
- main.res.srcDirs = [
- "src/main/res",
- "src/main/res-public"
- ]
- }
buildTypes.configureEach {
consumerProguardFiles "proguard-rules.pro"
}
diff --git a/coordinatorlayout/coordinatorlayout/src/androidTest/AndroidManifest.xml b/coordinatorlayout/coordinatorlayout/src/androidTest/AndroidManifest.xml
index 0dedce5..967e398 100644
--- a/coordinatorlayout/coordinatorlayout/src/androidTest/AndroidManifest.xml
+++ b/coordinatorlayout/coordinatorlayout/src/androidTest/AndroidManifest.xml
@@ -29,6 +29,12 @@
android:name="androidx.coordinatorlayout.widget.CoordinatorWithNestedScrollViewsActivity"
/>
+ <activity
+ android:exported="true"
+ android:theme="@style/Theme.AppCompat.Light"
+ android:name="androidx.coordinatorlayout.widget.CoordinatorWithRecyclerViewActivity"
+ />
+
<activity android:name="androidx.coordinatorlayout.widget.DynamicCoordinatorLayoutActivity"/>
</application>
diff --git a/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/widget/CoordinatorLayoutWithRecyclerViewKeyEventTest.java b/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/widget/CoordinatorLayoutWithRecyclerViewKeyEventTest.java
new file mode 100644
index 0000000..eed663e
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/widget/CoordinatorLayoutWithRecyclerViewKeyEventTest.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.coordinatorlayout.widget;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.pressKey;
+import static androidx.test.espresso.action.ViewActions.swipeUp;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+
+import static org.junit.Assert.assertEquals;
+
+import android.graphics.Rect;
+import android.view.KeyEvent;
+import android.view.View;
+
+import androidx.coordinatorlayout.test.R;
+import androidx.coordinatorlayout.testutils.AppBarStateChangedListener;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.testutils.PollingCheck;
+
+import com.google.android.material.appbar.AppBarLayout;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+@SuppressWarnings({"unchecked", "rawtypes"})
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class CoordinatorLayoutWithRecyclerViewKeyEventTest {
+
+ @Rule
+ public ActivityScenarioRule<CoordinatorWithRecyclerViewActivity> mActivityScenarioRule =
+ new ActivityScenarioRule(CoordinatorWithRecyclerViewActivity.class);
+
+ private AppBarLayout mAppBarLayout;
+ private RecyclerView mRecyclerView;
+ // Used to verify that the RecyclerView's item location is zero when using the UP Key.
+ private LinearLayoutManager mLinearLayoutManager;
+
+ private AppBarStateChangedListener.State mAppBarState =
+ AppBarStateChangedListener.State.UNKNOWN;
+
+ public static Matcher<View> isAtLeastHalfVisible() {
+ return new TypeSafeMatcher<View>() {
+ @Override
+ protected boolean matchesSafely(View view) {
+ Rect rect = new Rect();
+ return view.getGlobalVisibleRect(rect)
+ && rect.width() * rect.height() >= (view.getWidth() * view.getHeight()) / 2;
+ }
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("is at least half visible on screen");
+ }
+ };
+ }
+
+ @Before
+ public void setup() {
+ mActivityScenarioRule.getScenario().onActivity(activity -> {
+ mAppBarLayout = activity.mAppBarLayout;
+ mRecyclerView = activity.mRecyclerView;
+ mLinearLayoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
+
+ mAppBarLayout.addOnOffsetChangedListener(new AppBarStateChangedListener() {
+ @Override
+ public void onStateChanged(AppBarLayout appBarLayout, State state) {
+ mAppBarState = state;
+ }
+ });
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ }
+
+ /*** Tests ***/
+ @Test
+ @LargeTest
+ public void isCollapsingToolbarExpanded_swipeDownMultipleKeysUp_isExpanded() {
+ onView(withId(R.id.recycler_view)).check(matches(isAtLeastHalfVisible()));
+
+ // Scrolls down content and collapses the CollapsingToolbarLayout in the AppBarLayout.
+ onView(withId(R.id.coordinator)).perform(swipeUp());
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ // Espresso doesn't properly support swipeUp() with a CoordinatorLayout,
+ // AppBarLayout/CollapsingToolbarLayout, and RecyclerView. From testing, it only
+ // handles waiting until the AppBarLayout/CollapsingToolbarLayout is finished with its
+ // transition, NOT waiting until the RecyclerView is finished with its scrolling.
+ // This PollingCheck waits until the scroll is finished in the RecyclerView.
+ AtomicInteger previousScroll = new AtomicInteger();
+ PollingCheck.waitFor(() -> {
+ AtomicInteger currentScroll = new AtomicInteger();
+
+ mActivityScenarioRule.getScenario().onActivity(activity -> {
+ currentScroll.set(activity.mRecyclerView.getScrollY());
+ });
+
+ boolean isDone = currentScroll.get() == previousScroll.get();
+ previousScroll.set(currentScroll.get());
+
+ return isDone;
+ });
+
+ // Verifies the CollapsingToolbarLayout in the AppBarLayout is collapsed.
+ assertEquals(AppBarStateChangedListener.State.COLLAPSED, mAppBarState);
+ onView(withId(R.id.recycler_view)).check(matches(isCompletelyDisplayed()));
+
+ // First up keystroke gains focus (doesn't move any content).
+ onView(withId(R.id.recycler_view)).perform(pressKey(KeyEvent.KEYCODE_DPAD_UP));
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ // Retrieve top visible item in the RecyclerView.
+ int currentTopVisibleItem = mLinearLayoutManager.findFirstCompletelyVisibleItemPosition();
+
+ // Scroll up to the 0 position in the RecyclerView via UP Keystroke.
+ while (currentTopVisibleItem > 0) {
+ onView(withId(R.id.recycler_view)).perform(pressKey(KeyEvent.KEYCODE_DPAD_UP));
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ currentTopVisibleItem = mLinearLayoutManager.findFirstCompletelyVisibleItemPosition();
+ }
+
+ // This is a fail-safe in case the DPAD UP isn't making any changes, we break out of the
+ // loop.
+ float previousAppBarLayoutY = 0.0f;
+
+ // Performs a key press until the app bar is either expanded completely or no changes are
+ // made in the app bar between the previous call and the current call (failure case).
+ while (mAppBarState != AppBarStateChangedListener.State.EXPANDED
+ && (mAppBarLayout.getY() != previousAppBarLayoutY)
+ ) {
+ previousAppBarLayoutY = mAppBarLayout.getY();
+
+ // Partially expands the CollapsingToolbarLayout.
+ onView(withId(R.id.recycler_view)).perform(pressKey(KeyEvent.KEYCODE_DPAD_UP));
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ }
+
+ // Checks CollapsingToolbarLayout (in the AppBarLayout) is fully expanded.
+ assertEquals(AppBarStateChangedListener.State.EXPANDED, mAppBarState);
+ }
+
+ @Test
+ @LargeTest
+ public void doesAppBarCollapse_pressKeyboardDownMultipleTimes() {
+ onView(withId(R.id.recycler_view)).check(matches(isAtLeastHalfVisible()));
+
+ // Scrolls down content (key) and collapses the CollapsingToolbarLayout in the AppBarLayout.
+ // Gains focus
+ onView(withId(R.id.recycler_view)).perform(pressKey(KeyEvent.KEYCODE_DPAD_DOWN));
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ // This is a fail-safe in case the DPAD UP isn't making any changes, we break out of the
+ // loop.
+ float previousAppBarLayoutY = 0.0f;
+
+ // Performs a key press until the app bar is either completely collapsed or no changes are
+ // made in the app bar between the previous call and the current call (failure case).
+ while (mAppBarState != AppBarStateChangedListener.State.COLLAPSED
+ && (mAppBarLayout.getY() != previousAppBarLayoutY)
+ ) {
+ previousAppBarLayoutY = mAppBarLayout.getY();
+
+ // Partial collapse of the CollapsingToolbarLayout.
+ onView(withId(R.id.recycler_view)).perform(pressKey(KeyEvent.KEYCODE_DPAD_DOWN));
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ }
+
+ // Espresso doesn't properly support down with a CoordinatorLayout,
+ // AppBarLayout/CollapsingToolbarLayout, and RecyclerView. From testing, it only
+ // handles waiting until the AppBarLayout/CollapsingToolbarLayout is finished with its
+ // transition, NOT waiting until the RecyclerView is finished with its scrolling.
+ // This PollingCheck waits until the scroll is finished in the RecyclerView.
+ AtomicInteger previousScroll = new AtomicInteger();
+ PollingCheck.waitFor(() -> {
+ AtomicInteger currentScroll = new AtomicInteger();
+
+ mActivityScenarioRule.getScenario().onActivity(activity -> {
+ currentScroll.set(activity.mRecyclerView.getScrollY());
+ });
+
+ boolean isDone = currentScroll.get() == previousScroll.get();
+ previousScroll.set(currentScroll.get());
+
+ return isDone;
+ });
+
+ // Verifies the CollapsingToolbarLayout in the AppBarLayout is collapsed.
+ assertEquals(AppBarStateChangedListener.State.COLLAPSED, mAppBarState);
+ onView(withId(R.id.recycler_view)).check(matches(isCompletelyDisplayed()));
+ }
+}
diff --git a/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/widget/CoordinatorWithRecyclerViewActivity.java b/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/widget/CoordinatorWithRecyclerViewActivity.java
new file mode 100644
index 0000000..52c3a7a
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/widget/CoordinatorWithRecyclerViewActivity.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.coordinatorlayout.widget;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.coordinatorlayout.BaseTestActivity;
+import androidx.coordinatorlayout.test.R;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.appbar.AppBarLayout;
+import com.google.android.material.appbar.CollapsingToolbarLayout;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CoordinatorWithRecyclerViewActivity extends BaseTestActivity {
+ AppBarLayout mAppBarLayout;
+ RecyclerView mRecyclerView;
+
+ @Override
+ protected int getContentViewLayoutResId() {
+ return R.layout.activity_coordinator_with_recycler_view;
+ }
+
+ @Override
+ protected void onContentViewSet() {
+ mAppBarLayout = findViewById(R.id.app_bar_layout);
+ mRecyclerView = findViewById(R.id.recycler_view);
+
+ CollapsingToolbarLayout collapsingToolbarLayout =
+ findViewById(R.id.collapsing_toolbar_layout);
+
+ collapsingToolbarLayout.setTitle("Collapsing Bar Test");
+
+ mRecyclerView = findViewById(R.id.recycler_view);
+ mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
+
+ List<String> data = new ArrayList<String>();
+ for (int index = 0; index < 14; index++) {
+ data.add(String.valueOf(index));
+ }
+
+ MyAdapter adapter = new MyAdapter(data);
+ mRecyclerView.setAdapter(adapter);
+ }
+
+ public static class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
+ private final List<String> mDataForItems;
+
+ public MyAdapter(@NonNull List<String> items) {
+ this.mDataForItems = items;
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ @NonNull public TextView textViewHeader;
+ @NonNull public TextView textViewSubHeader;
+
+ public ViewHolder(@NonNull View itemView) {
+ super(itemView);
+ textViewHeader = itemView.findViewById(R.id.textViewHeader);
+ textViewSubHeader = itemView.findViewById(R.id.textViewSubHeader);
+ }
+ }
+
+ @NonNull
+ @Override
+ public MyAdapter.ViewHolder onCreateViewHolder(
+ @NonNull ViewGroup parent,
+ int viewType
+ ) {
+ View itemView = LayoutInflater.from(parent.getContext()).inflate(
+ R.layout.recycler_view_with_collapsing_toolbar_list_item,
+ parent,
+ false
+ );
+ return new ViewHolder(itemView);
+ }
+
+ @Override
+ public void onBindViewHolder(
+ MyAdapter.ViewHolder holder,
+ int position
+ ) {
+ String number = mDataForItems.get(position);
+
+ holder.textViewHeader.setText(number);
+ holder.textViewSubHeader.setText("Sub Header for " + number);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mDataForItems.size();
+ }
+ }
+}
diff --git a/coordinatorlayout/coordinatorlayout/src/androidTest/res/layout/activity_coordinator_with_recycler_view.xml b/coordinatorlayout/coordinatorlayout/src/androidTest/res/layout/activity_coordinator_with_recycler_view.xml
new file mode 100644
index 0000000..91bd00f
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/src/androidTest/res/layout/activity_coordinator_with_recycler_view.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2024 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
+
+ http://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.
+-->
+<!-- Uses a recyclerview with collapsing app bar AND sets scroll flag to include snap. -->
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/coordinator"
+ android:fitsSystemWindows="true"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!-- App Bar -->
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/app_bar_layout"
+ android:layout_width="match_parent"
+ android:layout_height="200dp">
+
+ <com.google.android.material.appbar.CollapsingToolbarLayout
+ android:id="@+id/collapsing_toolbar_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:expandedTitleMarginStart="48dp"
+ app:expandedTitleMarginEnd="64dp"
+ app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
+
+ <View android:layout_width="match_parent"
+ android:layout_height="200dp"
+ android:background="#FF0000"/>
+
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ app:layout_collapseMode="pin" />
+ </com.google.android.material.appbar.CollapsingToolbarLayout>
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <!-- Content -->
+ <FrameLayout
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:orientation="vertical"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ </androidx.recyclerview.widget.RecyclerView>
+ </FrameLayout>
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/coordinatorlayout/coordinatorlayout/src/androidTest/res/layout/recycler_view_with_collapsing_toolbar_list_item.xml b/coordinatorlayout/coordinatorlayout/src/androidTest/res/layout/recycler_view_with_collapsing_toolbar_list_item.xml
new file mode 100644
index 0000000..d44cdf1
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/src/androidTest/res/layout/recycler_view_with_collapsing_toolbar_list_item.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 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
+ http://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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="16dp"
+ android:focusable="true">
+ <TextView
+ android:id="@+id/textViewHeader"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="24sp" />
+ <TextView
+ android:id="@+id/textViewSubHeader"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="14sp" />
+</LinearLayout>
diff --git a/coordinatorlayout/coordinatorlayout/src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java b/coordinatorlayout/coordinatorlayout/src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java
index 1e76df6..0b20ce4 100644
--- a/coordinatorlayout/coordinatorlayout/src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java
+++ b/coordinatorlayout/coordinatorlayout/src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java
@@ -116,8 +116,8 @@
NestedScrollingParent3 {
static final String TAG = "CoordinatorLayout";
static final String WIDGET_PACKAGE_NAME;
- // For the UP/DOWN keys, we scroll 1/10th of the screen.
- private static final float KEY_SCROLL_FRACTION_AMOUNT = 0.1f;
+ // For the UP/DOWN keys, we scroll 20% of the screen.
+ private static final float KEY_SCROLL_FRACTION_AMOUNT = 0.2f;
static {
final Package pkg = CoordinatorLayout.class.getPackage();
@@ -2083,6 +2083,16 @@
ViewCompat.TYPE_NON_TOUCH
);
+ onNestedPreScroll(
+ focusedView,
+ 0,
+ yScrollDelta,
+ mKeyTriggeredScrollConsumed,
+ ViewCompat.TYPE_NON_TOUCH
+ );
+
+ int yScrollDeltaConsumed = mKeyTriggeredScrollConsumed[1];
+
// Reset consumed values to zero.
mKeyTriggeredScrollConsumed[0] = 0;
mKeyTriggeredScrollConsumed[1] = 0;
@@ -2090,7 +2100,7 @@
onNestedScroll(
focusedView,
0,
- 0,
+ yScrollDeltaConsumed,
0,
yScrollDelta,
ViewCompat.TYPE_NON_TOUCH,
diff --git a/coordinatorlayout/coordinatorlayout/src/main/res-public/values/public_attrs.xml b/coordinatorlayout/coordinatorlayout/src/main/res/values/public_attrs.xml
similarity index 100%
rename from coordinatorlayout/coordinatorlayout/src/main/res-public/values/public_attrs.xml
rename to coordinatorlayout/coordinatorlayout/src/main/res/values/public_attrs.xml
diff --git a/coordinatorlayout/coordinatorlayout/src/main/res-public/values/public_styles.xml b/coordinatorlayout/coordinatorlayout/src/main/res/values/public_styles.xml
similarity index 100%
rename from coordinatorlayout/coordinatorlayout/src/main/res-public/values/public_styles.xml
rename to coordinatorlayout/coordinatorlayout/src/main/res/values/public_styles.xml
diff --git a/core/core-animation-integration-tests/testapp/build.gradle b/core/core-animation-integration-tests/testapp/build.gradle
index 389b42d..d4bb678 100644
--- a/core/core-animation-integration-tests/testapp/build.gradle
+++ b/core/core-animation-integration-tests/testapp/build.gradle
@@ -20,6 +20,7 @@
}
dependencies {
+ api(libs.jspecify)
api("androidx.annotation:annotation:1.8.1")
implementation("androidx.core:core:1.1.0")
implementation(project(":core:core-animation"))
diff --git a/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/AnimatorSetTest.java b/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/AnimatorSetTest.java
index c15630f..6e60761f 100644
--- a/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/AnimatorSetTest.java
+++ b/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/AnimatorSetTest.java
@@ -26,11 +26,11 @@
import android.util.Property;
-import androidx.annotation.NonNull;
import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import org.jspecify.annotations.NonNull;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
diff --git a/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/AnimatorTestRuleIsolationTest.java b/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/AnimatorTestRuleIsolationTest.java
index ecd2390..d999723 100644
--- a/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/AnimatorTestRuleIsolationTest.java
+++ b/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/AnimatorTestRuleIsolationTest.java
@@ -20,11 +20,11 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
-import androidx.annotation.NonNull;
import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import org.jspecify.annotations.NonNull;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/ObjectAnimatorTest.java b/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/ObjectAnimatorTest.java
index aed16bd..8217931 100644
--- a/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/ObjectAnimatorTest.java
+++ b/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/ObjectAnimatorTest.java
@@ -25,11 +25,11 @@
import android.graphics.PointF;
import android.util.Property;
-import androidx.annotation.NonNull;
import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import org.jspecify.annotations.NonNull;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
@@ -693,9 +693,8 @@
};
TypeConverter<PointF, Float> converter = new TypeConverter<PointF, Float>(
PointF.class, Float.class) {
- @NonNull
@Override
- public Float convert(@NonNull PointF value) {
+ public @NonNull Float convert(@NonNull PointF value) {
return (float) Math.sqrt(value.x * value.x + value.y * value.y);
}
};
diff --git a/core/core-animation-testing/build.gradle b/core/core-animation-testing/build.gradle
index 890dcf3..85f0418 100644
--- a/core/core-animation-testing/build.gradle
+++ b/core/core-animation-testing/build.gradle
@@ -29,6 +29,7 @@
}
dependencies {
+ api(libs.jspecify)
api("androidx.annotation:annotation:1.8.1")
implementation("androidx.core:core:1.3.1")
implementation(project(":core:core-animation"))
@@ -41,8 +42,6 @@
mavenVersion = LibraryVersions.CORE_ANIMATION_TESTING
inceptionYear = "2018"
description = "This library provides functionalities for testing animations for API 14 and above."
- // TODO: b/326456246
- optOutJSpecify = true
}
android {
diff --git a/core/core-animation-testing/src/main/java/androidx/core/animation/AnimatorTestRule.java b/core/core-animation-testing/src/main/java/androidx/core/animation/AnimatorTestRule.java
index d25ce57..44098f2 100644
--- a/core/core-animation-testing/src/main/java/androidx/core/animation/AnimatorTestRule.java
+++ b/core/core-animation-testing/src/main/java/androidx/core/animation/AnimatorTestRule.java
@@ -20,8 +20,7 @@
import android.os.SystemClock;
import android.util.AndroidRuntimeException;
-import androidx.annotation.NonNull;
-
+import org.jspecify.annotations.NonNull;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
@@ -68,9 +67,9 @@
mTestHandler = new AnimationHandler(new TestProvider());
}
- @NonNull
@Override
- public Statement apply(@NonNull final Statement base, @NonNull Description description) {
+ public @NonNull Statement apply(final @NonNull Statement base,
+ @NonNull Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
diff --git a/core/core-animation/build.gradle b/core/core-animation/build.gradle
index d7d4494..39f5909 100644
--- a/core/core-animation/build.gradle
+++ b/core/core-animation/build.gradle
@@ -29,6 +29,7 @@
}
dependencies {
+ api(libs.jspecify)
api("androidx.annotation:annotation:1.8.1")
implementation("androidx.core:core:1.13.0")
implementation("androidx.collection:collection:1.4.2")
@@ -44,8 +45,6 @@
mavenVersion = LibraryVersions.CORE_ANIMATION
inceptionYear = "2018"
description = "This library provides functionalities for creating and manipulating animations for API 14 and above."
- // TODO: b/326456246
- optOutJSpecify = true
}
android {
diff --git a/core/core-animation/src/main/java/androidx/core/animation/AccelerateInterpolator.java b/core/core-animation/src/main/java/androidx/core/animation/AccelerateInterpolator.java
index de01686..9db911a 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/AccelerateInterpolator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/AccelerateInterpolator.java
@@ -23,7 +23,8 @@
import android.util.AttributeSet;
import androidx.annotation.FloatRange;
-import androidx.annotation.NonNull;
+
+import org.jspecify.annotations.NonNull;
/**
* An interpolator where the rate of change starts out slowly and
diff --git a/core/core-animation/src/main/java/androidx/core/animation/Animator.java b/core/core-animation/src/main/java/androidx/core/animation/Animator.java
index b4c7171..a66821d 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/Animator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/Animator.java
@@ -19,8 +19,9 @@
import android.annotation.SuppressLint;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
@@ -183,8 +184,7 @@
*
* @param duration The length of the animation, in milliseconds.
*/
- @NonNull
- public abstract Animator setDuration(@IntRange(from = 0) long duration);
+ public abstract @NonNull Animator setDuration(@IntRange(from = 0) long duration);
/**
* Gets the duration of the animation.
@@ -232,8 +232,7 @@
*
* @return The timing interpolator for this animation.
*/
- @Nullable
- public Interpolator getInterpolator() {
+ public @Nullable Interpolator getInterpolator() {
return null;
}
@@ -299,8 +298,7 @@
*
* @return ArrayList<AnimatorListener> The set of listeners.
*/
- @Nullable
- ArrayList<AnimatorListener> getListeners() {
+ @Nullable ArrayList<AnimatorListener> getListeners() {
return mListeners;
}
@@ -391,9 +389,8 @@
}
@SuppressLint("NoClone") /* Platform API */
- @NonNull
@Override
- public Animator clone() {
+ public @NonNull Animator clone() {
try {
final Animator anim = (Animator) super.clone();
if (mListeners != null) {
diff --git a/core/core-animation/src/main/java/androidx/core/animation/AnimatorInflater.java b/core/core-animation/src/main/java/androidx/core/animation/AnimatorInflater.java
index fa5e929..f2e805d 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/AnimatorInflater.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/AnimatorInflater.java
@@ -29,10 +29,10 @@
import androidx.annotation.AnimatorRes;
import androidx.annotation.InterpolatorRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.graphics.PathParser;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -76,8 +76,7 @@
* @return The animator object reference by the specified id
* @throws NotFoundException when the animation cannot be loaded
*/
- @NonNull
- public static Animator loadAnimator(@NonNull Context context, @AnimatorRes int id)
+ public static @NonNull Animator loadAnimator(@NonNull Context context, @AnimatorRes int id)
throws NotFoundException {
return loadAnimator(context.getResources(), context.getTheme(), id);
}
@@ -91,9 +90,8 @@
* @return The animator object reference by the specified id
* @throws NotFoundException when the animation cannot be loaded
*/
- @NonNull
- public static Animator loadAnimator(@NonNull Resources resources, @Nullable Theme theme,
- @AnimatorRes int id) throws NotFoundException {
+ public static @NonNull Animator loadAnimator(@NonNull Resources resources,
+ @Nullable Theme theme, @AnimatorRes int id) throws NotFoundException {
return loadAnimator(resources, theme, id, 1);
}
@@ -134,11 +132,10 @@
static class PathDataEvaluator implements TypeEvaluator<PathParser.PathDataNode[]> {
private PathParser.PathDataNode[] mPathData;
- @NonNull
@Override
- public PathParser.PathDataNode[] evaluate(
- float fraction, @NonNull PathParser.PathDataNode[] startPathData,
- @NonNull PathParser.PathDataNode[] endPathData) {
+ public PathParser.PathDataNode @NonNull [] evaluate(
+ float fraction, PathParser.PathDataNode @NonNull [] startPathData,
+ PathParser.PathDataNode @NonNull [] endPathData) {
if (mPathData == null) {
// This path buffer has to have the same size and structure as the morphing path.
mPathData = PathParser.deepCopyNodes(endPathData);
@@ -845,8 +842,7 @@
* @return The animation object reference by the specified id
* @throws NotFoundException when interpolator resources cannot be loaded
*/
- @NonNull
- public static Interpolator loadInterpolator(@NonNull Context context,
+ public static @NonNull Interpolator loadInterpolator(@NonNull Context context,
@AnimatorRes @InterpolatorRes int id) throws NotFoundException {
XmlResourceParser parser = null;
try {
diff --git a/core/core-animation/src/main/java/androidx/core/animation/AnimatorListenerAdapter.java b/core/core-animation/src/main/java/androidx/core/animation/AnimatorListenerAdapter.java
index 1d660d7..4c1d5e4 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/AnimatorListenerAdapter.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/AnimatorListenerAdapter.java
@@ -16,7 +16,7 @@
package androidx.core.animation;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* This adapter class provides empty implementations of the methods from
diff --git a/core/core-animation/src/main/java/androidx/core/animation/AnimatorSet.java b/core/core-animation/src/main/java/androidx/core/animation/AnimatorSet.java
index fc71818..bb837e5 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/AnimatorSet.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/AnimatorSet.java
@@ -22,11 +22,12 @@
import android.util.Log;
import android.view.animation.Animation;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.collection.SimpleArrayMap;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -194,7 +195,7 @@
*
* @param items The animations that will be started simultaneously.
*/
- public void playTogether(@NonNull Animator... items) {
+ public void playTogether(Animator @NonNull ... items) {
if (items != null) {
Builder builder = play(items[0]);
for (int i = 1; i < items.length; ++i) {
@@ -227,7 +228,7 @@
*
* @param items The animations that will be started one after another.
*/
- public void playSequentially(@NonNull Animator... items) {
+ public void playSequentially(Animator @NonNull ... items) {
if (items != null) {
if (items.length == 1) {
play(items[0]);
@@ -266,8 +267,7 @@
* @return ArrayList<Animator> The list of child animations of this AnimatorSet.
*/
@SuppressLint("ConcreteCollection") /* Platform API */
- @NonNull
- public ArrayList<Animator> getChildAnimations() {
+ public @NonNull ArrayList<Animator> getChildAnimations() {
ArrayList<Animator> childList = new ArrayList<Animator>();
int size = mNodes.size();
@@ -316,8 +316,7 @@
}
@Override
- @Nullable
- public Interpolator getInterpolator() {
+ public @Nullable Interpolator getInterpolator() {
return mInterpolator;
}
@@ -349,8 +348,7 @@
* outlined in the calls to <code>play</code> and the other methods in the
* <code>Builder</code object.
*/
- @NonNull
- public Builder play(@NonNull Animator anim) {
+ public @NonNull Builder play(@NonNull Animator anim) {
return new Builder(anim);
}
@@ -538,8 +536,7 @@
* animations of this AnimatorSet.
*/
@Override
- @NonNull
- public AnimatorSet setDuration(long duration) {
+ public @NonNull AnimatorSet setDuration(long duration) {
if (duration < 0) {
throw new IllegalArgumentException("duration must be a value of zero or greater");
}
@@ -1257,9 +1254,8 @@
}
@SuppressLint("NoClone") /* Platform API */
- @NonNull
@Override
- public AnimatorSet clone() {
+ public @NonNull AnimatorSet clone() {
final AnimatorSet anim = (AnimatorSet) super.clone();
/*
* The basic clone() operation copies all items. This doesn't work very well for
@@ -1716,9 +1712,8 @@
this.mAnimation = animation;
}
- @NonNull
@Override
- public Node clone() {
+ public @NonNull Node clone() {
try {
Node node = (Node) super.clone();
node.mAnimation = mAnimation.clone();
@@ -1811,9 +1806,8 @@
}
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
String eventStr = mEvent == ANIMATION_START ? "start" : (
mEvent == ANIMATION_DELAY_ENDED ? "delay ended" : "end");
return eventStr + " " + mNode.mAnimation.toString();
@@ -1951,8 +1945,7 @@
* @param anim The animation that will play when the animation supplied to the
* {@link AnimatorSet#play(Animator)} method starts.
*/
- @NonNull
- public Builder with(@NonNull Animator anim) {
+ public @NonNull Builder with(@NonNull Animator anim) {
Node node = getNodeForAnimation(anim);
mCurrentNode.addSibling(node);
return this;
@@ -1966,8 +1959,7 @@
* @param anim The animation that will play when the animation supplied to the
* {@link AnimatorSet#play(Animator)} method ends.
*/
- @NonNull
- public Builder before(@NonNull Animator anim) {
+ public @NonNull Builder before(@NonNull Animator anim) {
Node node = getNodeForAnimation(anim);
mCurrentNode.addChild(node);
return this;
@@ -1981,8 +1973,7 @@
* @param anim The animation whose end will cause the animation supplied to the
* {@link AnimatorSet#play(Animator)} method to play.
*/
- @NonNull
- public Builder after(@NonNull Animator anim) {
+ public @NonNull Builder after(@NonNull Animator anim) {
Node node = getNodeForAnimation(anim);
mCurrentNode.addParent(node);
return this;
@@ -1996,8 +1987,7 @@
* @param delay The number of milliseconds that should elapse before the
* animation starts.
*/
- @NonNull
- public Builder after(long delay) {
+ public @NonNull Builder after(long delay) {
// setup no-op ValueAnimator just to run the clock
ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
anim.setDuration(delay);
diff --git a/core/core-animation/src/main/java/androidx/core/animation/AnticipateInterpolator.java b/core/core-animation/src/main/java/androidx/core/animation/AnticipateInterpolator.java
index 21e29d6..388c089 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/AnticipateInterpolator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/AnticipateInterpolator.java
@@ -23,8 +23,9 @@
import android.util.AttributeSet;
import androidx.annotation.FloatRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* An interpolator where the change starts backward then flings forward.
diff --git a/core/core-animation/src/main/java/androidx/core/animation/AnticipateOvershootInterpolator.java b/core/core-animation/src/main/java/androidx/core/animation/AnticipateOvershootInterpolator.java
index ba8a687..e3081ab 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/AnticipateOvershootInterpolator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/AnticipateOvershootInterpolator.java
@@ -23,8 +23,9 @@
import android.util.AttributeSet;
import androidx.annotation.FloatRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* An interpolator where the change starts backward then flings forward and overshoots
diff --git a/core/core-animation/src/main/java/androidx/core/animation/ArgbEvaluator.java b/core/core-animation/src/main/java/androidx/core/animation/ArgbEvaluator.java
index 0d87507..ce3c37a 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/ArgbEvaluator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/ArgbEvaluator.java
@@ -18,7 +18,7 @@
import android.annotation.SuppressLint;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* This evaluator can be used to perform type interpolation between integer
@@ -33,8 +33,7 @@
* be used in multiple <code>Animator</code>s because it holds no state.
* @return An instance of <code>ArgbEvaluator</code>.
*/
- @NonNull
- public static ArgbEvaluator getInstance() {
+ public static @NonNull ArgbEvaluator getInstance() {
return sInstance;
}
@@ -59,8 +58,7 @@
*/
@SuppressLint("AutoBoxing") /* Generics */
@Override
- @NonNull
- public Integer evaluate(
+ public @NonNull Integer evaluate(
float fraction,
@SuppressLint("AutoBoxing") @NonNull Integer startValue,
@SuppressLint("AutoBoxing") @NonNull Integer endValue
diff --git a/core/core-animation/src/main/java/androidx/core/animation/BidirectionalTypeConverter.java b/core/core-animation/src/main/java/androidx/core/animation/BidirectionalTypeConverter.java
index c8f7a73..8ce5a26 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/BidirectionalTypeConverter.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/BidirectionalTypeConverter.java
@@ -15,7 +15,7 @@
*/
package androidx.core.animation;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Abstract base class used convert type T to another type V and back again. This
@@ -48,8 +48,7 @@
* @param value The Object to convert.
* @return A value of type T, converted from <code>value</code>.
*/
- @NonNull
- public abstract T convertBack(@NonNull V value);
+ public abstract @NonNull T convertBack(@NonNull V value);
/**
* Returns the inverse of this converter, where the from and to classes are reversed.
@@ -58,8 +57,7 @@
* {@link #convertBack(Object)} calls.
* @return The inverse of this converter, where the from and to classes are reversed.
*/
- @NonNull
- public BidirectionalTypeConverter<V, T> invert() {
+ public @NonNull BidirectionalTypeConverter<V, T> invert() {
if (mInvertedConverter == null) {
mInvertedConverter = new InvertedConverter<>(this);
}
@@ -74,15 +72,13 @@
mConverter = converter;
}
- @NonNull
@Override
- public From convertBack(@NonNull To value) {
+ public @NonNull From convertBack(@NonNull To value) {
return mConverter.convert(value);
}
- @NonNull
@Override
- public To convert(@NonNull From value) {
+ public @NonNull To convert(@NonNull From value) {
return mConverter.convertBack(value);
}
}
diff --git a/core/core-animation/src/main/java/androidx/core/animation/CycleInterpolator.java b/core/core-animation/src/main/java/androidx/core/animation/CycleInterpolator.java
index 437b465..2ccc9c0 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/CycleInterpolator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/CycleInterpolator.java
@@ -23,8 +23,9 @@
import android.util.AttributeSet;
import androidx.annotation.FloatRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* Repeats the animation for a specified number of cycles. The
diff --git a/core/core-animation/src/main/java/androidx/core/animation/DecelerateInterpolator.java b/core/core-animation/src/main/java/androidx/core/animation/DecelerateInterpolator.java
index 3483c0b..958b33e 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/DecelerateInterpolator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/DecelerateInterpolator.java
@@ -23,8 +23,9 @@
import android.util.AttributeSet;
import androidx.annotation.FloatRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* An interpolator where the rate of change starts out quickly and
diff --git a/core/core-animation/src/main/java/androidx/core/animation/FloatArrayEvaluator.java b/core/core-animation/src/main/java/androidx/core/animation/FloatArrayEvaluator.java
index b89e5d0..bb4769f 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/FloatArrayEvaluator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/FloatArrayEvaluator.java
@@ -17,8 +17,8 @@
package androidx.core.animation;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* This evaluator can be used to perform type interpolation between <code>float[]</code> values.
@@ -48,7 +48,7 @@
*
* @param reuseArray The array to modify and return from <code>evaluate</code>.
*/
- public FloatArrayEvaluator(@Nullable float[] reuseArray) {
+ public FloatArrayEvaluator(float @Nullable [] reuseArray) {
mArray = reuseArray;
}
@@ -65,9 +65,8 @@
* the same index in startValue and endValue.
*/
@Override
- @NonNull
- public float[] evaluate(float fraction, @NonNull float[] startValue,
- @NonNull float[] endValue) {
+ public float @NonNull [] evaluate(float fraction, float @NonNull [] startValue,
+ float @NonNull [] endValue) {
float[] array = mArray;
if (array == null) {
array = new float[startValue.length];
diff --git a/core/core-animation/src/main/java/androidx/core/animation/FloatEvaluator.java b/core/core-animation/src/main/java/androidx/core/animation/FloatEvaluator.java
index 3797d5c..816ea0b 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/FloatEvaluator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/FloatEvaluator.java
@@ -18,7 +18,7 @@
import android.annotation.SuppressLint;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* This evaluator can be used to perform type interpolation between <code>float</code> values.
@@ -33,8 +33,7 @@
* be used in multiple <code>Animator</code>s because it holds no state.
* @return An instance of <code>FloatEvaluator</code>.
*/
- @NonNull
- public static FloatEvaluator getInstance() {
+ public static @NonNull FloatEvaluator getInstance() {
return sInstance;
}
@@ -57,8 +56,7 @@
*/
@SuppressLint("AutoBoxing") /* Generics */
@Override
- @NonNull
- public Float evaluate(
+ public @NonNull Float evaluate(
float fraction,
@SuppressLint("AutoBoxing") @NonNull Float startValue,
@SuppressLint("AutoBoxing") @NonNull Float endValue
diff --git a/core/core-animation/src/main/java/androidx/core/animation/FloatKeyframeSet.java b/core/core-animation/src/main/java/androidx/core/animation/FloatKeyframeSet.java
index 3eeb3b6..45af7aa 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/FloatKeyframeSet.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/FloatKeyframeSet.java
@@ -16,9 +16,10 @@
package androidx.core.animation;
-import androidx.annotation.NonNull;
import androidx.core.animation.Keyframe.FloatKeyframe;
+import org.jspecify.annotations.NonNull;
+
import java.util.List;
/**
@@ -41,9 +42,8 @@
return getFloatValue(fraction);
}
- @NonNull
@Override
- public FloatKeyframeSet clone() {
+ public @NonNull FloatKeyframeSet clone() {
final List<Keyframe<Float>> keyframes = mKeyframes;
final int numKeyframes = mKeyframes.size();
FloatKeyframe[] newKeyframes = new FloatKeyframe[numKeyframes];
diff --git a/core/core-animation/src/main/java/androidx/core/animation/FloatProperty.java b/core/core-animation/src/main/java/androidx/core/animation/FloatProperty.java
index d967a5e3..13ccacdf 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/FloatProperty.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/FloatProperty.java
@@ -18,7 +18,7 @@
import android.annotation.SuppressLint;
import android.util.Property;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* An implementation of {@link android.util.Property} to be used specifically with fields of type
diff --git a/core/core-animation/src/main/java/androidx/core/animation/IntArrayEvaluator.java b/core/core-animation/src/main/java/androidx/core/animation/IntArrayEvaluator.java
index d3d4edb..55969e4 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/IntArrayEvaluator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/IntArrayEvaluator.java
@@ -16,8 +16,8 @@
package androidx.core.animation;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* This evaluator can be used to perform type interpolation between <code>int[]</code> values.
@@ -47,7 +47,7 @@
*
* @param reuseArray The array to modify and return from <code>evaluate</code>.
*/
- public IntArrayEvaluator(@Nullable int[] reuseArray) {
+ public IntArrayEvaluator(int @Nullable [] reuseArray) {
mArray = reuseArray;
}
@@ -63,8 +63,8 @@
* the same index in startValue and endValue.
*/
@Override
- @NonNull
- public int[] evaluate(float fraction, @NonNull int[] startValue, @NonNull int[] endValue) {
+ public int @NonNull [] evaluate(float fraction, int @NonNull [] startValue,
+ int @NonNull [] endValue) {
int[] array = mArray;
if (array == null) {
array = new int[startValue.length];
diff --git a/core/core-animation/src/main/java/androidx/core/animation/IntEvaluator.java b/core/core-animation/src/main/java/androidx/core/animation/IntEvaluator.java
index f09fa14..fb2919c 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/IntEvaluator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/IntEvaluator.java
@@ -18,7 +18,7 @@
import android.annotation.SuppressLint;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* This evaluator can be used to perform type interpolation between <code>int</code> values.
@@ -33,8 +33,7 @@
* be used in multiple <code>Animator</code>s because it holds no state.
* @return An instance of <code>IntEvaluator</code>.
*/
- @NonNull
- public static IntEvaluator getInstance() {
+ public static @NonNull IntEvaluator getInstance() {
return sInstance;
}
@@ -57,8 +56,7 @@
*/
@SuppressLint("AutoBoxing") /* Generics */
@Override
- @NonNull
- public Integer evaluate(
+ public @NonNull Integer evaluate(
float fraction,
@SuppressLint("AutoBoxing") @NonNull Integer startValue,
@SuppressLint("AutoBoxing") @NonNull Integer endValue
diff --git a/core/core-animation/src/main/java/androidx/core/animation/IntKeyframeSet.java b/core/core-animation/src/main/java/androidx/core/animation/IntKeyframeSet.java
index a43a19b7..71ff949 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/IntKeyframeSet.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/IntKeyframeSet.java
@@ -16,9 +16,10 @@
package androidx.core.animation;
-import androidx.annotation.NonNull;
import androidx.core.animation.Keyframe.IntKeyframe;
+import org.jspecify.annotations.NonNull;
+
import java.util.List;
/**
@@ -41,9 +42,8 @@
return getIntValue(fraction);
}
- @NonNull
@Override
- public IntKeyframeSet clone() {
+ public @NonNull IntKeyframeSet clone() {
List<Keyframe<Integer>> keyframes = mKeyframes;
int numKeyframes = mKeyframes.size();
IntKeyframe[] newKeyframes = new IntKeyframe[numKeyframes];
diff --git a/core/core-animation/src/main/java/androidx/core/animation/IntProperty.java b/core/core-animation/src/main/java/androidx/core/animation/IntProperty.java
index 78c0d3a..02bf783 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/IntProperty.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/IntProperty.java
@@ -18,7 +18,7 @@
import android.annotation.SuppressLint;
import android.util.Property;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* An implementation of {@link android.util.Property} to be used specifically with fields of type
diff --git a/core/core-animation/src/main/java/androidx/core/animation/Keyframe.java b/core/core-animation/src/main/java/androidx/core/animation/Keyframe.java
index ca2a121..a283634c 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/Keyframe.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/Keyframe.java
@@ -19,8 +19,9 @@
import android.annotation.SuppressLint;
import androidx.annotation.FloatRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* This class holds a time/value pair for an animation. The Keyframe class is used
@@ -85,8 +86,8 @@
* the time in this keyframe, and the the value animated from as the time passes the time in
* this keyframe.
*/
- @NonNull
- public static Keyframe<Integer> ofInt(@FloatRange(from = 0, to = 1) float fraction, int value) {
+ public static @NonNull Keyframe<Integer> ofInt(@FloatRange(from = 0, to = 1) float fraction,
+ int value) {
return new IntKeyframe(fraction, value);
}
@@ -102,8 +103,7 @@
* @param fraction The time, expressed as a value between 0 and 1, representing the fraction
* of time elapsed of the overall animation duration.
*/
- @NonNull
- public static Keyframe<Integer> ofInt(@FloatRange(from = 0, to = 1) float fraction) {
+ public static @NonNull Keyframe<Integer> ofInt(@FloatRange(from = 0, to = 1) float fraction) {
return new IntKeyframe(fraction);
}
@@ -119,8 +119,7 @@
* the time in this keyframe, and the the value animated from as the time passes the time in
* this keyframe.
*/
- @NonNull
- public static Keyframe<Float> ofFloat(@FloatRange(from = 0, to = 1) float fraction,
+ public static @NonNull Keyframe<Float> ofFloat(@FloatRange(from = 0, to = 1) float fraction,
float value) {
return new FloatKeyframe(fraction, value);
}
@@ -137,8 +136,7 @@
* @param fraction The time, expressed as a value between 0 and 1, representing the fraction
* of time elapsed of the overall animation duration.
*/
- @NonNull
- public static Keyframe<Float> ofFloat(@FloatRange(from = 0, to = 1) float fraction) {
+ public static @NonNull Keyframe<Float> ofFloat(@FloatRange(from = 0, to = 1) float fraction) {
return new FloatKeyframe(fraction);
}
@@ -154,8 +152,7 @@
* the time in this keyframe, and the the value animated from as the time passes the time in
* this keyframe.
*/
- @NonNull
- public static <T> Keyframe<T> ofObject(@FloatRange(from = 0, to = 1) float fraction,
+ public static <T> @NonNull Keyframe<T> ofObject(@FloatRange(from = 0, to = 1) float fraction,
@Nullable T value) {
return new ObjectKeyframe<T>(fraction, value);
}
@@ -172,8 +169,7 @@
* @param fraction The time, expressed as a value between 0 and 1, representing the fraction
* of time elapsed of the overall animation duration.
*/
- @NonNull
- public static <T> Keyframe<T> ofObject(@FloatRange(from = 0, to = 1) float fraction) {
+ public static <T> @NonNull Keyframe<T> ofObject(@FloatRange(from = 0, to = 1) float fraction) {
return new ObjectKeyframe<>(fraction, null);
}
@@ -210,8 +206,7 @@
//TODO: Consider removing hasValue() and making keyframe always contain nonNull value, and
// using a different signal for when the animation should read the property value as its start
// value.
- @Nullable
- public abstract T getValue();
+ public abstract @Nullable T getValue();
/**
* Sets the value for this Keyframe.
@@ -246,8 +241,7 @@
*
* @return The optional interpolator for this Keyframe.
*/
- @Nullable
- public Interpolator getInterpolator() {
+ public @Nullable Interpolator getInterpolator() {
return mInterpolator;
}
@@ -266,15 +260,13 @@
*
* @return The type of the value stored in the Keyframe.
*/
- @NonNull
- public Class<?> getType() {
+ public @NonNull Class<?> getType() {
return mValueType;
}
@SuppressLint("NoClone") /* Platform API */
- @NonNull
@Override
- public abstract Keyframe<T> clone();
+ public abstract @NonNull Keyframe<T> clone();
/**
* This internal subclass is used for all types which are not int or float.
@@ -304,9 +296,8 @@
mHasValue = (value != null);
}
- @NonNull
@Override
- public ObjectKeyframe<T> clone() {
+ public @NonNull ObjectKeyframe<T> clone() {
ObjectKeyframe<T> kfClone = new ObjectKeyframe<>(getFraction(),
hasValue() ? mValue : null);
kfClone.mValueWasSetOnStart = mValueWasSetOnStart;
@@ -354,9 +345,8 @@
}
}
- @NonNull
@Override
- public IntKeyframe clone() {
+ public @NonNull IntKeyframe clone() {
IntKeyframe kfClone = mHasValue ? new IntKeyframe(getFraction(), mValue) :
new IntKeyframe(getFraction());
kfClone.setInterpolator(getInterpolator());
@@ -403,9 +393,8 @@
}
}
- @NonNull
@Override
- public FloatKeyframe clone() {
+ public @NonNull FloatKeyframe clone() {
FloatKeyframe kfClone = mHasValue ? new FloatKeyframe(getFraction(), mValue) :
new FloatKeyframe(getFraction());
kfClone.setInterpolator(getInterpolator());
diff --git a/core/core-animation/src/main/java/androidx/core/animation/KeyframeSet.java b/core/core-animation/src/main/java/androidx/core/animation/KeyframeSet.java
index 518c889..640b55a 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/KeyframeSet.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/KeyframeSet.java
@@ -19,10 +19,11 @@
import android.graphics.Path;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.core.animation.Keyframe.FloatKeyframe;
import androidx.core.animation.Keyframe.IntKeyframe;
+import org.jspecify.annotations.NonNull;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -184,9 +185,8 @@
return mFirstKeyframe.getType();
}
- @NonNull
@Override
- public KeyframeSet<T> clone() {
+ public @NonNull KeyframeSet<T> clone() {
List<Keyframe<T>> keyframes = mKeyframes;
int numKeyframes = mKeyframes.size();
final ArrayList<Keyframe<T>> newKeyframes = new ArrayList<>(numKeyframes);
@@ -265,9 +265,8 @@
return mLastKeyframe.getValue();
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
String returnVal = " ";
for (int i = 0; i < mNumKeyframes; ++i) {
returnVal += mKeyframes.get(i).getValue() + " ";
diff --git a/core/core-animation/src/main/java/androidx/core/animation/Keyframes.java b/core/core-animation/src/main/java/androidx/core/animation/Keyframes.java
index a552de5..a680f3f 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/Keyframes.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/Keyframes.java
@@ -15,7 +15,7 @@
*/
package androidx.core.animation;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
import java.util.List;
@@ -60,8 +60,7 @@
*/
List<Keyframe<T>> getKeyframes();
- @NonNull
- Keyframes clone();
+ @NonNull Keyframes clone();
/**
* A specialization of Keyframes that has integer primitive value calculation.
diff --git a/core/core-animation/src/main/java/androidx/core/animation/LinearInterpolator.java b/core/core-animation/src/main/java/androidx/core/animation/LinearInterpolator.java
index 8ae8373..da20421 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/LinearInterpolator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/LinearInterpolator.java
@@ -20,8 +20,9 @@
import android.util.AttributeSet;
import androidx.annotation.FloatRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* An interpolator where the rate of change is constant
diff --git a/core/core-animation/src/main/java/androidx/core/animation/ObjectAnimator.java b/core/core-animation/src/main/java/androidx/core/animation/ObjectAnimator.java
index e6b9d8c..c0ed8b9 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/ObjectAnimator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/ObjectAnimator.java
@@ -23,8 +23,9 @@
import android.util.Property;
import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import java.lang.ref.WeakReference;
@@ -160,8 +161,7 @@
* object (if there was just one) or a comma-separated list of all of the
* names (if there are more than one).</p>
*/
- @NonNull
- public String getPropertyName() {
+ public @NonNull String getPropertyName() {
String propertyName = null;
if (mPropertyName != null) {
propertyName = mPropertyName;
@@ -181,8 +181,7 @@
}
@Override
- @NonNull
- public String getNameForTrace() {
+ public @NonNull String getNameForTrace() {
return mAnimTraceName == null ? "animator:" + getPropertyName() : mAnimTraceName;
}
@@ -234,9 +233,8 @@
* @param values A set of values that the animation will animate between over time.
* @return An ObjectAnimator object that is set up to animate between the given values.
*/
- @NonNull
- public static ObjectAnimator ofInt(@NonNull Object target, @NonNull String propertyName,
- @NonNull int... values) {
+ public static @NonNull ObjectAnimator ofInt(@NonNull Object target,
+ @NonNull String propertyName, int @NonNull ... values) {
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
anim.setIntValues(values);
return anim;
@@ -259,9 +257,8 @@
* @param path The <code>Path</code> to animate values along.
* @return An ObjectAnimator object that is set up to animate along <code>path</code>.
*/
- @NonNull
- public static ObjectAnimator ofInt(@NonNull Object target, @NonNull String xPropertyName,
- @NonNull String yPropertyName, @NonNull Path path) {
+ public static @NonNull ObjectAnimator ofInt(@NonNull Object target,
+ @NonNull String xPropertyName, @NonNull String yPropertyName, @NonNull Path path) {
PathKeyframes keyframes = KeyframeSet.ofPath(path);
PropertyValuesHolder x = PropertyValuesHolder.ofKeyframes(xPropertyName,
keyframes.createXIntKeyframes());
@@ -283,9 +280,8 @@
* @param values A set of values that the animation will animate between over time.
* @return An ObjectAnimator object that is set up to animate between the given values.
*/
- @NonNull
- public static <T> ObjectAnimator ofInt(@NonNull T target,
- @NonNull Property<T, Integer> property, @NonNull int... values) {
+ public static <T> @NonNull ObjectAnimator ofInt(@NonNull T target,
+ @NonNull Property<T, Integer> property, int @NonNull ... values) {
ObjectAnimator anim = new ObjectAnimator(target, property);
anim.setIntValues(values);
return anim;
@@ -304,8 +300,7 @@
* @param path The <code>Path</code> to animate values along.
* @return An ObjectAnimator object that is set up to animate along <code>path</code>.
*/
- @NonNull
- public static <T> ObjectAnimator ofInt(@NonNull T target,
+ public static <T> @NonNull ObjectAnimator ofInt(@NonNull T target,
@Nullable Property<T, Integer> xProperty, @Nullable Property<T, Integer> yProperty,
@NonNull Path path) {
PathKeyframes keyframes = KeyframeSet.ofPath(path);
@@ -332,11 +327,10 @@
* @param values A set of values that the animation will animate between over time.
* @return An ObjectAnimator object that is set up to animate between the given values.
*/
- @NonNull
- public static ObjectAnimator ofMultiInt(
+ public static @NonNull ObjectAnimator ofMultiInt(
@NonNull Object target,
@NonNull String propertyName,
- @SuppressLint("ArrayReturn") /* Platform API */ @NonNull int[][] values
+ @SuppressLint("ArrayReturn") /* Platform API */ int @NonNull [][] values
) {
PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiInt(propertyName, values);
return ofPropertyValuesHolder(target, pvh);
@@ -357,9 +351,8 @@
* @param path The <code>Path</code> to animate values along.
* @return An ObjectAnimator object that is set up to animate along <code>path</code>.
*/
- @NonNull
- public static ObjectAnimator ofMultiInt(@NonNull Object target, @NonNull String propertyName,
- @NonNull Path path) {
+ public static @NonNull ObjectAnimator ofMultiInt(@NonNull Object target,
+ @NonNull String propertyName, @NonNull Path path) {
PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiInt(propertyName, path);
return ofPropertyValuesHolder(target, pvh);
}
@@ -384,10 +377,9 @@
* @return An ObjectAnimator object that is set up to animate between the given values.
*/
@SafeVarargs
- @NonNull
- public static <T> ObjectAnimator ofMultiInt(@NonNull Object target,
+ public static <T> @NonNull ObjectAnimator ofMultiInt(@NonNull Object target,
@NonNull String propertyName, @NonNull TypeConverter<T, int[]> converter,
- @NonNull TypeEvaluator<T> evaluator, @NonNull T... values) {
+ @NonNull TypeEvaluator<T> evaluator, T @NonNull ... values) {
PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiInt(propertyName, converter,
evaluator, values);
return ObjectAnimator.ofPropertyValuesHolder(target, pvh);
@@ -408,9 +400,8 @@
* @param values A set of values that the animation will animate between over time.
* @return An ObjectAnimator object that is set up to animate between the given values.
*/
- @NonNull
- public static ObjectAnimator ofArgb(@NonNull Object target, @NonNull String propertyName,
- @NonNull int... values) {
+ public static @NonNull ObjectAnimator ofArgb(@NonNull Object target,
+ @NonNull String propertyName, int @NonNull ... values) {
ObjectAnimator animator = ofInt(target, propertyName, values);
animator.setEvaluator(ArgbEvaluator.getInstance());
return animator;
@@ -429,9 +420,8 @@
* @param values A set of values that the animation will animate between over time.
* @return An ObjectAnimator object that is set up to animate between the given values.
*/
- @NonNull
- public static <T> ObjectAnimator ofArgb(@NonNull T target,
- @NonNull Property<T, Integer> property, @NonNull int... values) {
+ public static <T> @NonNull ObjectAnimator ofArgb(@NonNull T target,
+ @NonNull Property<T, Integer> property, int @NonNull ... values) {
ObjectAnimator animator = ofInt(target, property, values);
animator.setEvaluator(ArgbEvaluator.getInstance());
return animator;
@@ -452,9 +442,8 @@
* @param values A set of values that the animation will animate between over time.
* @return An ObjectAnimator object that is set up to animate between the given values.
*/
- @NonNull
- public static ObjectAnimator ofFloat(@NonNull Object target, @NonNull String propertyName,
- @NonNull float... values) {
+ public static @NonNull ObjectAnimator ofFloat(@NonNull Object target,
+ @NonNull String propertyName, float @NonNull ... values) {
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
anim.setFloatValues(values);
return anim;
@@ -477,9 +466,8 @@
* @param path The <code>Path</code> to animate values along.
* @return An ObjectAnimator object that is set up to animate along <code>path</code>.
*/
- @NonNull
- public static ObjectAnimator ofFloat(@NonNull Object target, @Nullable String xPropertyName,
- @Nullable String yPropertyName, @NonNull Path path) {
+ public static @NonNull ObjectAnimator ofFloat(@NonNull Object target,
+ @Nullable String xPropertyName, @Nullable String yPropertyName, @NonNull Path path) {
PathKeyframes keyframes = KeyframeSet.ofPath(path);
PropertyValuesHolder x = PropertyValuesHolder.ofKeyframes(xPropertyName,
keyframes.createXFloatKeyframes());
@@ -501,9 +489,8 @@
* @param values A set of values that the animation will animate between over time.
* @return An ObjectAnimator object that is set up to animate between the given values.
*/
- @NonNull
- public static <T> ObjectAnimator ofFloat(@NonNull T target,
- @NonNull Property<T, Float> property, @NonNull float... values) {
+ public static <T> @NonNull ObjectAnimator ofFloat(@NonNull T target,
+ @NonNull Property<T, Float> property, float @NonNull ... values) {
ObjectAnimator anim = new ObjectAnimator(target, property);
anim.setFloatValues(values);
return anim;
@@ -522,8 +509,7 @@
* @param path The <code>Path</code> to animate values along.
* @return An ObjectAnimator object that is set up to animate along <code>path</code>.
*/
- @NonNull
- public static <T> ObjectAnimator ofFloat(@NonNull T target,
+ public static <T> @NonNull ObjectAnimator ofFloat(@NonNull T target,
@Nullable Property<T, Float> xProperty, @Nullable Property<T, Float> yProperty,
@NonNull Path path) {
PathKeyframes keyframes = KeyframeSet.ofPath(path);
@@ -550,11 +536,10 @@
* @param values A set of values that the animation will animate between over time.
* @return An ObjectAnimator object that is set up to animate between the given values.
*/
- @NonNull
- public static ObjectAnimator ofMultiFloat(
+ public static @NonNull ObjectAnimator ofMultiFloat(
@NonNull Object target,
@NonNull String propertyName,
- @SuppressLint("ArrayReturn") /* Platform API */ @NonNull float[][] values
+ @SuppressLint("ArrayReturn") /* Platform API */ float @NonNull [][] values
) {
PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiFloat(propertyName, values);
return ofPropertyValuesHolder(target, pvh);
@@ -575,9 +560,8 @@
* @param path The <code>Path</code> to animate values along.
* @return An ObjectAnimator object that is set up to animate along <code>path</code>.
*/
- @NonNull
- public static ObjectAnimator ofMultiFloat(@NonNull Object target, @NonNull String propertyName,
- @NonNull Path path) {
+ public static @NonNull ObjectAnimator ofMultiFloat(@NonNull Object target,
+ @NonNull String propertyName, @NonNull Path path) {
PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiFloat(propertyName, path);
return ofPropertyValuesHolder(target, pvh);
}
@@ -602,10 +586,9 @@
* @return An ObjectAnimator object that is set up to animate between the given values.
*/
@SafeVarargs
- @NonNull
- public static <T> ObjectAnimator ofMultiFloat(@NonNull Object target,
+ public static <T> @NonNull ObjectAnimator ofMultiFloat(@NonNull Object target,
@NonNull String propertyName, @NonNull TypeConverter<T, float[]> converter,
- @NonNull TypeEvaluator<T> evaluator, @NonNull T... values) {
+ @NonNull TypeEvaluator<T> evaluator, T @NonNull ... values) {
PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiFloat(propertyName, converter,
evaluator, values);
return ObjectAnimator.ofPropertyValuesHolder(target, pvh);
@@ -634,9 +617,9 @@
* @param values A set of values that the animation will animate between over time.
* @return An ObjectAnimator object that is set up to animate between the given values.
*/
- @NonNull
- public static ObjectAnimator ofObject(@NonNull Object target, @NonNull String propertyName,
- @NonNull TypeEvaluator evaluator, @NonNull Object... values) {
+ public static @NonNull ObjectAnimator ofObject(@NonNull Object target,
+ @NonNull String propertyName, @NonNull TypeEvaluator evaluator,
+ Object @NonNull ... values) {
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
anim.setObjectValues(values);
anim.setEvaluator(evaluator);
@@ -661,9 +644,9 @@
* @param path The <code>Path</code> to animate values along.
* @return An ObjectAnimator object that is set up to animate along <code>path</code>.
*/
- @NonNull
- public static ObjectAnimator ofObject(@NonNull Object target, @NonNull String propertyName,
- @Nullable TypeConverter<PointF, ?> converter, @NonNull Path path) {
+ public static @NonNull ObjectAnimator ofObject(@NonNull Object target,
+ @NonNull String propertyName, @Nullable TypeConverter<PointF, ?> converter,
+ @NonNull Path path) {
PropertyValuesHolder pvh = PropertyValuesHolder.ofObject(propertyName, converter, path);
return ofPropertyValuesHolder(target, pvh);
}
@@ -689,11 +672,10 @@
* @param values A set of values that the animation will animate between over time.
* @return An ObjectAnimator object that is set up to animate between the given values.
*/
- @NonNull
@SafeVarargs
- public static <T, V> ObjectAnimator ofObject(@NonNull T target,
+ public static <T, V> @NonNull ObjectAnimator ofObject(@NonNull T target,
@NonNull Property<T, V> property, @NonNull TypeEvaluator<V> evaluator,
- @NonNull V... values) {
+ V @NonNull ... values) {
ObjectAnimator anim = new ObjectAnimator(target, property);
anim.setObjectValues(values);
anim.setEvaluator(evaluator);
@@ -725,11 +707,10 @@
* @param values A set of values that the animation will animate between over time.
* @return An ObjectAnimator object that is set up to animate between the given values.
*/
- @NonNull
@SafeVarargs
- public static <T, V, P> ObjectAnimator ofObject(@NonNull T target,
+ public static <T, V, P> @NonNull ObjectAnimator ofObject(@NonNull T target,
@NonNull Property<T, P> property, @NonNull TypeConverter<V, P> converter,
- @NonNull TypeEvaluator<V> evaluator, @NonNull V... values) {
+ @NonNull TypeEvaluator<V> evaluator, V @NonNull ... values) {
PropertyValuesHolder pvh = PropertyValuesHolder.ofObject(property, converter, evaluator,
values);
return ofPropertyValuesHolder(target, pvh);
@@ -754,8 +735,7 @@
* @param path The <code>Path</code> to animate values along.
* @return An ObjectAnimator object that is set up to animate along <code>path</code>.
*/
- @NonNull
- public static <T, V> ObjectAnimator ofObject(@NonNull T target,
+ public static <T, V> @NonNull ObjectAnimator ofObject(@NonNull T target,
@NonNull Property<T, V> property, @Nullable TypeConverter<PointF, V> converter,
@NonNull Path path) {
PropertyValuesHolder pvh = PropertyValuesHolder.ofObject(property, converter, path);
@@ -779,9 +759,8 @@
* over time.
* @return An ObjectAnimator object that is set up to animate between the given values.
*/
- @NonNull
- public static ObjectAnimator ofPropertyValuesHolder(@NonNull Object target,
- @NonNull PropertyValuesHolder... values) {
+ public static @NonNull ObjectAnimator ofPropertyValuesHolder(@NonNull Object target,
+ PropertyValuesHolder @NonNull ... values) {
ObjectAnimator anim = new ObjectAnimator();
anim.setTarget(target);
anim.setValues(values);
@@ -789,7 +768,7 @@
}
@Override
- public void setIntValues(@NonNull int... values) {
+ public void setIntValues(int @NonNull ... values) {
if (mValues == null || mValues.length == 0) {
// No values yet - this animator is being constructed piecemeal. Init the values with
// whatever the current propertyName is
@@ -804,7 +783,7 @@
}
@Override
- public void setFloatValues(@NonNull float... values) {
+ public void setFloatValues(float @NonNull ... values) {
if (mValues == null || mValues.length == 0) {
// No values yet - this animator is being constructed piecemeal. Init the values with
// whatever the current propertyName is
@@ -819,7 +798,7 @@
}
@Override
- public void setObjectValues(@NonNull Object... values) {
+ public void setObjectValues(Object @NonNull ... values) {
if (mValues == null || mValues.length == 0) {
// No values yet - this animator is being constructed piecemeal. Init the values with
// whatever the current propertyName is
@@ -936,8 +915,7 @@
* <code>ObjectAnimator.ofInt(target, propertyName, 0, 10).setDuration(500).start()</code>.
*/
@Override
- @NonNull
- public ObjectAnimator setDuration(long duration) {
+ public @NonNull ObjectAnimator setDuration(long duration) {
super.setDuration(duration);
return this;
}
@@ -948,8 +926,7 @@
*
* @return The object being animated
*/
- @Nullable
- public Object getTarget() {
+ public @Nullable Object getTarget() {
return mTarget == null ? null : mTarget.get();
}
@@ -1028,16 +1005,14 @@
}
@SuppressLint("NoClone") /* Platform API */
- @NonNull
@Override
- public ObjectAnimator clone() {
+ public @NonNull ObjectAnimator clone() {
final ObjectAnimator anim = (ObjectAnimator) super.clone();
return anim;
}
@Override
- @NonNull
- public String toString() {
+ public @NonNull String toString() {
String returnVal = "ObjectAnimator@" + Integer.toHexString(hashCode()) + ", target "
+ getTarget();
if (mValues != null) {
diff --git a/core/core-animation/src/main/java/androidx/core/animation/OvershootInterpolator.java b/core/core-animation/src/main/java/androidx/core/animation/OvershootInterpolator.java
index 66c5fc8..bda7c92 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/OvershootInterpolator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/OvershootInterpolator.java
@@ -23,8 +23,9 @@
import android.util.AttributeSet;
import androidx.annotation.FloatRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* An interpolator where the change flings forward and overshoots the last value
diff --git a/core/core-animation/src/main/java/androidx/core/animation/PathInterpolator.java b/core/core-animation/src/main/java/androidx/core/animation/PathInterpolator.java
index b3f4195..b0bc350b 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/PathInterpolator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/PathInterpolator.java
@@ -24,14 +24,13 @@
import android.view.InflateException;
import androidx.annotation.FloatRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.content.res.TypedArrayUtils;
import androidx.core.graphics.PathParser;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.xmlpull.v1.XmlPullParser;
-
/**
* An interpolator that can traverse a Path that extends from <code>Point</code>
* <code>(0, 0)</code> to <code>(1, 1)</code>. The x coordinate along the <code>Path</code>
diff --git a/core/core-animation/src/main/java/androidx/core/animation/PathKeyframes.java b/core/core-animation/src/main/java/androidx/core/animation/PathKeyframes.java
index fe6cb0e..c6215d1 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/PathKeyframes.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/PathKeyframes.java
@@ -18,7 +18,7 @@
import android.graphics.Path;
import android.graphics.PointF;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
import java.util.ArrayList;
import java.util.List;
@@ -128,9 +128,8 @@
return PointF.class;
}
- @NonNull
@Override
- public Keyframes clone() {
+ public @NonNull Keyframes clone() {
Keyframes clone = null;
try {
clone = (Keyframes) super.clone();
@@ -218,10 +217,9 @@
return mEmptyFrames;
}
- @NonNull
@Override
@SuppressWarnings({"unchecked", "CatchAndPrintStackTrace"})
- public Keyframes<T> clone() {
+ public @NonNull Keyframes<T> clone() {
Keyframes<T> clone = null;
try {
clone = (Keyframes<T>) super.clone();
diff --git a/core/core-animation/src/main/java/androidx/core/animation/PointFEvaluator.java b/core/core-animation/src/main/java/androidx/core/animation/PointFEvaluator.java
index 201cde0..8f55951 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/PointFEvaluator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/PointFEvaluator.java
@@ -17,7 +17,7 @@
import android.graphics.PointF;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* This evaluator can be used to perform type interpolation between <code>PointF</code> values.
@@ -71,8 +71,8 @@
* <code>fraction</code> parameter.
*/
@Override
- @NonNull
- public PointF evaluate(float fraction, @NonNull PointF startValue, @NonNull PointF endValue) {
+ public @NonNull PointF evaluate(float fraction, @NonNull PointF startValue,
+ @NonNull PointF endValue) {
float x = startValue.x + (fraction * (endValue.x - startValue.x));
float y = startValue.y + (fraction * (endValue.y - startValue.y));
diff --git a/core/core-animation/src/main/java/androidx/core/animation/PropertyValuesHolder.java b/core/core-animation/src/main/java/androidx/core/animation/PropertyValuesHolder.java
index 27e973a..e88c06e 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/PropertyValuesHolder.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/PropertyValuesHolder.java
@@ -22,8 +22,8 @@
import android.util.Log;
import android.util.Property;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -147,8 +147,8 @@
* @param values The values that the named property will animate between.
* @return PropertyValuesHolder The constructed PropertyValuesHolder object.
*/
- @NonNull
- public static PropertyValuesHolder ofInt(@NonNull String propertyName, @NonNull int... values) {
+ public static @NonNull PropertyValuesHolder ofInt(@NonNull String propertyName,
+ int @NonNull ... values) {
return new IntPropertyValuesHolder(propertyName, values);
}
@@ -159,9 +159,8 @@
* @param values The values that the property will animate between.
* @return PropertyValuesHolder The constructed PropertyValuesHolder object.
*/
- @NonNull
- public static PropertyValuesHolder ofInt(@NonNull Property<?, Integer> property,
- @NonNull int... values) {
+ public static @NonNull PropertyValuesHolder ofInt(@NonNull Property<?, Integer> property,
+ int @NonNull ... values) {
return new IntPropertyValuesHolder(property, values);
}
@@ -179,10 +178,9 @@
* @see IntArrayEvaluator#IntArrayEvaluator(int[])
* @see ObjectAnimator#ofMultiInt(Object, String, TypeConverter, TypeEvaluator, Object[])
*/
- @NonNull
- public static PropertyValuesHolder ofMultiInt(
+ public static @NonNull PropertyValuesHolder ofMultiInt(
@NonNull String propertyName,
- @SuppressLint("ArrayReturn") /* Platform API */ @NonNull int[][] values
+ @SuppressLint("ArrayReturn") /* Platform API */ int @NonNull [][] values
) {
if (values.length < 2) {
throw new IllegalArgumentException("At least 2 values must be supplied");
@@ -215,8 +213,7 @@
* @return PropertyValuesHolder The constructed PropertyValuesHolder object.
* @see ObjectAnimator#ofPropertyValuesHolder(Object, PropertyValuesHolder...)
*/
- @NonNull
- public static PropertyValuesHolder ofMultiInt(@NonNull String propertyName,
+ public static @NonNull PropertyValuesHolder ofMultiInt(@NonNull String propertyName,
@NonNull Path path) {
Keyframes keyframes = KeyframeSet.ofPath(path);
PointFToIntArray converter = new PointFToIntArray();
@@ -240,10 +237,9 @@
* @see ObjectAnimator#ofPropertyValuesHolder(Object, PropertyValuesHolder...)
*/
@SafeVarargs
- @NonNull
- public static <V> PropertyValuesHolder ofMultiInt(@NonNull String propertyName,
+ public static <V> @NonNull PropertyValuesHolder ofMultiInt(@NonNull String propertyName,
@NonNull TypeConverter<V, int[]> converter,
- @NonNull TypeEvaluator<V> evaluator, @NonNull V... values) {
+ @NonNull TypeEvaluator<V> evaluator, V @NonNull ... values) {
return new MultiIntValuesHolder(propertyName, converter, evaluator, values);
}
@@ -265,10 +261,9 @@
* @return A PropertyValuesHolder for a multi-int parameter setter.
*/
@SafeVarargs
- @NonNull
- public static <T> PropertyValuesHolder ofMultiInt(@NonNull String propertyName,
+ public static <T> @NonNull PropertyValuesHolder ofMultiInt(@NonNull String propertyName,
@Nullable TypeConverter<T, int[]> converter, @NonNull TypeEvaluator<T> evaluator,
- @NonNull Keyframe... values) {
+ Keyframe @NonNull ... values) {
KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values);
return new MultiIntValuesHolder(propertyName, converter, evaluator, keyframeSet);
}
@@ -280,9 +275,8 @@
* @param values The values that the named property will animate between.
* @return PropertyValuesHolder The constructed PropertyValuesHolder object.
*/
- @NonNull
- public static PropertyValuesHolder ofFloat(@NonNull String propertyName,
- @NonNull float... values) {
+ public static @NonNull PropertyValuesHolder ofFloat(@NonNull String propertyName,
+ float @NonNull ... values) {
return new FloatPropertyValuesHolder(propertyName, values);
}
@@ -293,9 +287,8 @@
* @param values The values that the property will animate between.
* @return PropertyValuesHolder The constructed PropertyValuesHolder object.
*/
- @NonNull
- public static PropertyValuesHolder ofFloat(@NonNull Property<?, Float> property,
- @NonNull float... values) {
+ public static @NonNull PropertyValuesHolder ofFloat(@NonNull Property<?, Float> property,
+ float @NonNull ... values) {
return new FloatPropertyValuesHolder(property, values);
}
@@ -313,10 +306,9 @@
* @see FloatArrayEvaluator#FloatArrayEvaluator(float[])
* @see ObjectAnimator#ofMultiFloat(Object, String, TypeConverter, TypeEvaluator, Object[])
*/
- @NonNull
- public static PropertyValuesHolder ofMultiFloat(
+ public static @NonNull PropertyValuesHolder ofMultiFloat(
@NonNull String propertyName,
- @SuppressLint("ArrayReturn") /* Platform API */ @NonNull float[][] values
+ @SuppressLint("ArrayReturn") /* Platform API */ float @NonNull [][] values
) {
if (values.length < 2) {
throw new IllegalArgumentException("At least 2 values must be supplied");
@@ -349,8 +341,7 @@
* @return PropertyValuesHolder The constructed PropertyValuesHolder object.
* @see ObjectAnimator#ofPropertyValuesHolder(Object, PropertyValuesHolder...)
*/
- @NonNull
- public static PropertyValuesHolder ofMultiFloat(@NonNull String propertyName,
+ public static @NonNull PropertyValuesHolder ofMultiFloat(@NonNull String propertyName,
@NonNull Path path) {
Keyframes keyframes = KeyframeSet.ofPath(path);
PointFToFloatArray converter = new PointFToFloatArray();
@@ -373,10 +364,9 @@
* @see ObjectAnimator#ofMultiFloat(Object, String, TypeConverter, TypeEvaluator, Object[])
*/
@SafeVarargs
- @NonNull
- public static <V> PropertyValuesHolder ofMultiFloat(@NonNull String propertyName,
+ public static <V> @NonNull PropertyValuesHolder ofMultiFloat(@NonNull String propertyName,
@NonNull TypeConverter<V, float[]> converter,
- @NonNull TypeEvaluator<V> evaluator, @NonNull V... values) {
+ @NonNull TypeEvaluator<V> evaluator, V @NonNull ... values) {
return new MultiFloatValuesHolder(propertyName, converter, evaluator, values);
}
@@ -398,11 +388,10 @@
* @return A PropertyValuesHolder for a multi-float parameter setter.
*/
@SafeVarargs
- @NonNull
- public static <T> PropertyValuesHolder ofMultiFloat(@NonNull String propertyName,
+ public static <T> @NonNull PropertyValuesHolder ofMultiFloat(@NonNull String propertyName,
@Nullable TypeConverter<T, float[]> converter,
@NonNull TypeEvaluator<T> evaluator,
- @NonNull Keyframe... values) {
+ Keyframe @NonNull ... values) {
KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values);
return new MultiFloatValuesHolder(propertyName, converter, evaluator, keyframeSet);
}
@@ -424,9 +413,8 @@
* @param values The values that the named property will animate between.
* @return PropertyValuesHolder The constructed PropertyValuesHolder object.
*/
- @NonNull
- public static PropertyValuesHolder ofObject(@NonNull String propertyName,
- @NonNull TypeEvaluator evaluator, @NonNull Object... values) {
+ public static @NonNull PropertyValuesHolder ofObject(@NonNull String propertyName,
+ @NonNull TypeEvaluator evaluator, Object @NonNull ... values) {
PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName);
pvh.setObjectValues(values);
pvh.setEvaluator(evaluator);
@@ -449,8 +437,7 @@
* @param path The Path along which the values should be animated.
* @return PropertyValuesHolder The constructed PropertyValuesHolder object.
*/
- @NonNull
- public static PropertyValuesHolder ofObject(@NonNull String propertyName,
+ public static @NonNull PropertyValuesHolder ofObject(@NonNull String propertyName,
@Nullable TypeConverter<PointF, ?> converter, @NonNull Path path) {
PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName);
pvh.mKeyframes = KeyframeSet.ofPath(path);
@@ -477,9 +464,8 @@
* @return PropertyValuesHolder The constructed PropertyValuesHolder object.
*/
@SafeVarargs
- @NonNull
- public static <V> PropertyValuesHolder ofObject(@NonNull Property property,
- @NonNull TypeEvaluator<V> evaluator, @NonNull V... values) {
+ public static <V> @NonNull PropertyValuesHolder ofObject(@NonNull Property property,
+ @NonNull TypeEvaluator<V> evaluator, V @NonNull ... values) {
PropertyValuesHolder pvh = new PropertyValuesHolder(property);
pvh.setObjectValues(values);
pvh.setEvaluator(evaluator);
@@ -511,10 +497,9 @@
* @see TypeConverter
*/
@SafeVarargs
- @NonNull
- public static <T, V> PropertyValuesHolder ofObject(@NonNull Property<?, V> property,
+ public static <T, V> @NonNull PropertyValuesHolder ofObject(@NonNull Property<?, V> property,
@NonNull TypeConverter<T, V> converter, @NonNull TypeEvaluator<T> evaluator,
- @NonNull T... values) {
+ T @NonNull ... values) {
PropertyValuesHolder pvh = new PropertyValuesHolder(property);
pvh.setConverter(converter);
pvh.setObjectValues(values);
@@ -538,8 +523,7 @@
* @param path The Path along which the values should be animated.
* @return PropertyValuesHolder The constructed PropertyValuesHolder object.
*/
- @NonNull
- public static <V> PropertyValuesHolder ofObject(@NonNull Property<?, V> property,
+ public static <V> @NonNull PropertyValuesHolder ofObject(@NonNull Property<?, V> property,
@Nullable TypeConverter<PointF, V> converter, @NonNull Path path) {
PropertyValuesHolder pvh = new PropertyValuesHolder(property);
pvh.mKeyframes = KeyframeSet.ofPath(path);
@@ -567,9 +551,8 @@
* @param values The set of values to animate between.
*/
@SafeVarargs
- @NonNull
- public static PropertyValuesHolder ofKeyframe(@NonNull String propertyName,
- @NonNull Keyframe... values) {
+ public static @NonNull PropertyValuesHolder ofKeyframe(@NonNull String propertyName,
+ Keyframe @NonNull ... values) {
KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values);
return ofKeyframes(propertyName, keyframeSet);
}
@@ -591,9 +574,8 @@
* @param values The set of values to animate between.
*/
@SafeVarargs
- @NonNull
- public static PropertyValuesHolder ofKeyframe(@NonNull Property property,
- @NonNull Keyframe... values) {
+ public static @NonNull PropertyValuesHolder ofKeyframe(@NonNull Property property,
+ Keyframe @NonNull ... values) {
KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values);
return ofKeyframes(property, keyframeSet);
}
@@ -637,7 +619,7 @@
*
* @param values One or more values that the animation will animate between.
*/
- public void setIntValues(@NonNull int... values) {
+ public void setIntValues(int @NonNull ... values) {
mValueType = int.class;
mKeyframes = KeyframeSet.ofInt(values);
}
@@ -654,7 +636,7 @@
*
* @param values One or more values that the animation will animate between.
*/
- public void setFloatValues(@NonNull float... values) {
+ public void setFloatValues(float @NonNull ... values) {
mValueType = float.class;
mKeyframes = KeyframeSet.ofFloat(values);
}
@@ -664,7 +646,7 @@
*
* @param values One or more values that the animation will animate between.
*/
- public void setKeyframes(@NonNull Keyframe... values) {
+ public void setKeyframes(Keyframe @NonNull ... values) {
int numKeyframes = values.length;
Keyframe[] keyframes = new Keyframe[Math.max(numKeyframes, 2)];
mValueType = values[0].getType();
@@ -691,7 +673,7 @@
*
* @param values One or more values that the animation will animate between.
*/
- public void setObjectValues(@NonNull Object... values) {
+ public void setObjectValues(Object @NonNull ... values) {
mValueType = values[0].getClass();
mKeyframes = KeyframeSet.ofObject(values);
if (mEvaluator != null) {
@@ -979,9 +961,8 @@
}
@SuppressLint("NoClone") /* Platform API */
- @NonNull
@Override
- public PropertyValuesHolder clone() {
+ public @NonNull PropertyValuesHolder clone() {
try {
PropertyValuesHolder newPVH = (PropertyValuesHolder) super.clone();
newPVH.mPropertyName = mPropertyName;
@@ -1102,8 +1083,7 @@
* <code>valueFrom</code> or <code>valueTo</code> is null, then a getter function will
* also be derived and called.
*/
- @NonNull
- public String getPropertyName() {
+ public @NonNull String getPropertyName() {
return mPropertyName;
}
@@ -1120,9 +1100,8 @@
return mValueType;
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
return mPropertyName + ": " + mKeyframes.toString();
}
@@ -1196,7 +1175,7 @@
}
@Override
- public void setIntValues(@NonNull int... values) {
+ public void setIntValues(int @NonNull ... values) {
super.setIntValues(values);
mIntKeyframes = (Keyframes.IntKeyframes) mKeyframes;
}
@@ -1211,9 +1190,8 @@
return mIntAnimatedValue;
}
- @NonNull
@Override
- public IntPropertyValuesHolder clone() {
+ public @NonNull IntPropertyValuesHolder clone() {
IntPropertyValuesHolder newPVH = (IntPropertyValuesHolder) super.clone();
newPVH.mIntKeyframes = (Keyframes.IntKeyframes) newPVH.mKeyframes;
return newPVH;
@@ -1295,7 +1273,7 @@
}
@Override
- public void setFloatValues(@NonNull float... values) {
+ public void setFloatValues(float @NonNull ... values) {
super.setFloatValues(values);
mFloatKeyframes = (Keyframes.FloatKeyframes) mKeyframes;
}
@@ -1310,9 +1288,8 @@
return mFloatAnimatedValue;
}
- @NonNull
@Override
- public FloatPropertyValuesHolder clone() {
+ public @NonNull FloatPropertyValuesHolder clone() {
FloatPropertyValuesHolder newPVH = (FloatPropertyValuesHolder) super.clone();
newPVH.mFloatKeyframes = (Keyframes.FloatKeyframes) newPVH.mKeyframes;
return newPVH;
@@ -1559,9 +1536,8 @@
super(PointF.class, float[].class);
}
- @NonNull
@Override
- public float[] convert(@NonNull PointF value) {
+ public float @NonNull [] convert(@NonNull PointF value) {
mCoordinates[0] = value.x;
mCoordinates[1] = value.y;
return mCoordinates;
@@ -1578,9 +1554,8 @@
super(PointF.class, int[].class);
}
- @NonNull
@Override
- public int[] convert(@NonNull PointF value) {
+ public int @NonNull [] convert(@NonNull PointF value) {
mCoordinates[0] = Math.round(value.x);
mCoordinates[1] = Math.round(value.y);
return mCoordinates;
diff --git a/core/core-animation/src/main/java/androidx/core/animation/RectEvaluator.java b/core/core-animation/src/main/java/androidx/core/animation/RectEvaluator.java
index 9f5e54f..8da9e3d 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/RectEvaluator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/RectEvaluator.java
@@ -17,7 +17,7 @@
import android.graphics.Rect;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* This evaluator can be used to perform type interpolation between <code>Rect</code> values.
@@ -71,8 +71,8 @@
* <code>fraction</code> parameter.
*/
@Override
- @NonNull
- public Rect evaluate(float fraction, @NonNull Rect startValue, @NonNull Rect endValue) {
+ public @NonNull Rect evaluate(float fraction, @NonNull Rect startValue,
+ @NonNull Rect endValue) {
int left = startValue.left + (int) ((endValue.left - startValue.left) * fraction);
int top = startValue.top + (int) ((endValue.top - startValue.top) * fraction);
int right = startValue.right + (int) ((endValue.right - startValue.right) * fraction);
diff --git a/core/core-animation/src/main/java/androidx/core/animation/TimeAnimator.java b/core/core-animation/src/main/java/androidx/core/animation/TimeAnimator.java
index f484716..a949c9c 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/TimeAnimator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/TimeAnimator.java
@@ -18,8 +18,8 @@
import android.view.animation.AnimationUtils;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* This class provides a simple callback mechanism to listeners that is synchronized with all
diff --git a/core/core-animation/src/main/java/androidx/core/animation/TypeConverter.java b/core/core-animation/src/main/java/androidx/core/animation/TypeConverter.java
index 7cf4e3f..76dd7e6 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/TypeConverter.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/TypeConverter.java
@@ -16,7 +16,7 @@
package androidx.core.animation;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Abstract base class used convert type T to another type V. This
diff --git a/core/core-animation/src/main/java/androidx/core/animation/TypeEvaluator.java b/core/core-animation/src/main/java/androidx/core/animation/TypeEvaluator.java
index 75b0c6e..9ae1737 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/TypeEvaluator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/TypeEvaluator.java
@@ -16,7 +16,7 @@
package androidx.core.animation;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Interface for use with the {@link ValueAnimator#setEvaluator(TypeEvaluator)} function. Evaluators
@@ -42,7 +42,6 @@
* @return A linear interpolation between the start and end values, given the
* <code>fraction</code> parameter.
*/
- @NonNull
- T evaluate(float fraction, @NonNull T startValue, @NonNull T endValue);
+ @NonNull T evaluate(float fraction, @NonNull T startValue, @NonNull T endValue);
}
diff --git a/core/core-animation/src/main/java/androidx/core/animation/ValueAnimator.java b/core/core-animation/src/main/java/androidx/core/animation/ValueAnimator.java
index 20c2325..26f6eb6 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/ValueAnimator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/ValueAnimator.java
@@ -24,10 +24,11 @@
import androidx.annotation.CallSuper;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
@@ -298,8 +299,7 @@
* @param values A set of values that the animation will animate between over time.
* @return A ValueAnimator object that is set up to animate between the given values.
*/
- @NonNull
- public static ValueAnimator ofInt(@NonNull int... values) {
+ public static @NonNull ValueAnimator ofInt(int @NonNull ... values) {
ValueAnimator anim = new ValueAnimator();
anim.setIntValues(values);
return anim;
@@ -316,8 +316,7 @@
* @param values A set of values that the animation will animate between over time.
* @return A ValueAnimator object that is set up to animate between the given values.
*/
- @NonNull
- public static ValueAnimator ofArgb(@NonNull int... values) {
+ public static @NonNull ValueAnimator ofArgb(int @NonNull ... values) {
ValueAnimator anim = new ValueAnimator();
anim.setIntValues(values);
anim.setEvaluator(ArgbEvaluator.getInstance());
@@ -335,8 +334,7 @@
* @param values A set of values that the animation will animate between over time.
* @return A ValueAnimator object that is set up to animate between the given values.
*/
- @NonNull
- public static ValueAnimator ofFloat(@NonNull float... values) {
+ public static @NonNull ValueAnimator ofFloat(float @NonNull ... values) {
ValueAnimator anim = new ValueAnimator();
anim.setFloatValues(values);
return anim;
@@ -350,8 +348,8 @@
* between over time.
* @return A ValueAnimator object that is set up to animate between the given values.
*/
- @NonNull
- public static ValueAnimator ofPropertyValuesHolder(@NonNull PropertyValuesHolder... values) {
+ public static @NonNull ValueAnimator ofPropertyValuesHolder(
+ PropertyValuesHolder @NonNull ... values) {
ValueAnimator anim = new ValueAnimator();
anim.setValues(values);
return anim;
@@ -379,9 +377,8 @@
* @param values A set of values that the animation will animate between over time.
* @return A ValueAnimator object that is set up to animate between the given values.
*/
- @NonNull
- public static ValueAnimator ofObject(@NonNull TypeEvaluator evaluator,
- @NonNull Object... values) {
+ public static @NonNull ValueAnimator ofObject(@NonNull TypeEvaluator evaluator,
+ Object @NonNull ... values) {
ValueAnimator anim = new ValueAnimator();
anim.setObjectValues(values);
anim.setEvaluator(evaluator);
@@ -402,7 +399,7 @@
*
* @param values A set of values that the animation will animate between over time.
*/
- public void setIntValues(@NonNull int... values) {
+ public void setIntValues(int @NonNull ... values) {
if (values == null || values.length == 0) {
return;
}
@@ -430,7 +427,7 @@
*
* @param values A set of values that the animation will animate between over time.
*/
- public void setFloatValues(@NonNull float... values) {
+ public void setFloatValues(float @NonNull ... values) {
if (values == null || values.length == 0) {
return;
}
@@ -467,7 +464,7 @@
*
* @param values The set of values to animate between.
*/
- public void setObjectValues(@NonNull Object... values) {
+ public void setObjectValues(Object @NonNull ... values) {
if (values == null || values.length == 0) {
return;
}
@@ -489,7 +486,7 @@
*
* @param values The set of values, per property, being animated between.
*/
- public void setValues(@NonNull PropertyValuesHolder... values) {
+ public void setValues(PropertyValuesHolder @NonNull ... values) {
int numValues = values.length;
mValues = values;
mValuesMap = new HashMap<String, PropertyValuesHolder>(numValues);
@@ -510,8 +507,7 @@
* values, per property, that define the animation.
*/
@SuppressLint("ArrayReturn") /* Platform API */
- @NonNull
- public PropertyValuesHolder[] getValues() {
+ public PropertyValuesHolder @NonNull [] getValues() {
return mValues;
}
@@ -546,8 +542,7 @@
* duration, as in <code>ValueAnimator.ofInt(0, 10).setDuration(500).start()</code>.
*/
@Override
- @NonNull
- public ValueAnimator setDuration(long duration) {
+ public @NonNull ValueAnimator setDuration(long duration) {
if (duration < 0) {
throw new IllegalArgumentException("Animators cannot have negative duration: "
+ duration);
@@ -813,8 +808,7 @@
* (specified by several PropertyValuesHolder objects in the constructor), this function
* returns the animated value for the first of those objects.
*/
- @NonNull
- public Object getAnimatedValue() {
+ public @NonNull Object getAnimatedValue() {
if (mValues != null && mValues.length > 0) {
return mValues[0].getAnimatedValue();
}
@@ -832,8 +826,7 @@
* @return animatedValue The value most recently calculated for the named property
* by this <code>ValueAnimator</code>.
*/
- @Nullable
- public Object getAnimatedValue(@NonNull String propertyName) {
+ public @Nullable Object getAnimatedValue(@NonNull String propertyName) {
PropertyValuesHolder valuesHolder = mValuesMap.get(propertyName);
if (valuesHolder != null) {
return valuesHolder.getAnimatedValue();
@@ -909,8 +902,7 @@
* @return The timing interpolator for this ValueAnimator.
*/
@Override
- @Nullable
- public Interpolator getInterpolator() {
+ public @Nullable Interpolator getInterpolator() {
return mInterpolator;
}
@@ -1195,8 +1187,7 @@
/**
* Returns the name of this animator for debugging purposes.
*/
- @NonNull
- public String getNameForTrace() {
+ public @NonNull String getNameForTrace() {
return mAnimTraceName == null ? "animator" : mAnimTraceName;
}
@@ -1458,8 +1449,7 @@
@SuppressLint("NoClone") /* Platform API */
@Override
- @NonNull
- public ValueAnimator clone() {
+ public @NonNull ValueAnimator clone() {
final ValueAnimator anim = (ValueAnimator) super.clone();
if (mUpdateListeners != null) {
anim.mUpdateListeners = new ArrayList<AnimatorUpdateListener>(mUpdateListeners);
@@ -1507,9 +1497,8 @@
return AnimationHandler.getAnimationCount();
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
String returnVal = "ValueAnimator@" + Integer.toHexString(hashCode());
if (mValues != null) {
for (int i = 0; i < mValues.length; ++i) {
diff --git a/core/core-appdigest/build.gradle b/core/core-appdigest/build.gradle
index efab216..4f26e39 100644
--- a/core/core-appdigest/build.gradle
+++ b/core/core-appdigest/build.gradle
@@ -29,6 +29,7 @@
}
dependencies {
+ api(libs.jspecify)
api("androidx.annotation:annotation:1.8.1")
implementation("androidx.core:core:1.7.0")
implementation("androidx.concurrent:concurrent-futures:1.0.0")
@@ -46,8 +47,6 @@
mavenVersion = LibraryVersions.CORE_APPDIGEST
inceptionYear = "2020"
description = "AndroidX AppDigest Library"
- // TODO: b/326456246
- optOutJSpecify = true
}
android {
diff --git a/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java b/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java
index 82448fb..b0f208f 100644
--- a/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java
+++ b/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java
@@ -49,8 +49,6 @@
import android.os.Build;
import android.os.ParcelFileDescriptor;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.concurrent.futures.ResolvableFuture;
import androidx.test.core.app.ApplicationProvider;
@@ -60,6 +58,8 @@
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.After;
import org.junit.Assert;
import org.junit.Assume;
@@ -225,8 +225,7 @@
executeShellCommand("pm uninstall " + packageName);
}
- @NonNull
- private static String bytesToHexString(byte[] array) {
+ private static @NonNull String bytesToHexString(byte[] array) {
int offset = 0;
int length = array.length;
@@ -243,8 +242,7 @@
return new String(buf);
}
- @NonNull
- private static byte[] hexStringToBytes(String s) {
+ private static byte @NonNull [] hexStringToBytes(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
@@ -1159,7 +1157,7 @@
new android.content.pm.Checksum(android.content.pm.Checksum.TYPE_WHOLE_MD5,
hexStringToBytes(TEST_FIXED_APK_MD5))};
- static @NonNull android.content.pm.Checksum readFromStream(@NonNull DataInputStream dis)
+ static android.content.pm.@NonNull Checksum readFromStream(@NonNull DataInputStream dis)
throws IOException {
final int type = dis.readInt();
@@ -1169,7 +1167,7 @@
}
private static void writeToStream(@NonNull DataOutputStream dos,
- @NonNull android.content.pm.Checksum checksum) throws IOException {
+ android.content.pm.@NonNull Checksum checksum) throws IOException {
dos.writeInt(checksum.getType());
final byte[] valueBytes = checksum.getValue();
diff --git a/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksum.java b/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksum.java
index 47fa0ca..84674c9 100644
--- a/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksum.java
+++ b/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksum.java
@@ -19,11 +19,12 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.annotation.Retention;
@@ -139,7 +140,7 @@
/**
* Checksum value.
*/
- private final @NonNull byte[] mValue;
+ private final byte @NonNull [] mValue;
/**
* For Installer-provided checksums, package name of the Installer.
*/
@@ -147,20 +148,20 @@
/**
* For Installer-provided checksums, certificate of the Installer.
*/
- private final @Nullable byte[] mInstallerCertificate;
+ private final byte @Nullable [] mInstallerCertificate;
/**
* Constructor, internal use only.
*/
Checksum(@Nullable String splitName, @Checksum.Type int type,
- @NonNull byte[] value) {
+ byte @NonNull [] value) {
this(splitName, type, value, (String) null, (byte[]) null);
}
/**
* Constructor, internal use only.
*/
- Checksum(@Nullable String splitName, @Checksum.Type int type, @NonNull byte[] value,
+ Checksum(@Nullable String splitName, @Checksum.Type int type, byte @NonNull [] value,
@Nullable String installerPackageName, @Nullable Certificate installerCertificate)
throws CertificateEncodingException {
this(splitName, type, value, installerPackageName,
@@ -184,9 +185,9 @@
Checksum(
@Nullable String splitName,
@Type int type,
- @NonNull byte[] value,
+ byte @NonNull [] value,
@Nullable String installerPackageName,
- @Nullable byte[] installerCertificate) {
+ byte @Nullable [] installerCertificate) {
Preconditions.checkNotNull(value);
this.mSplitName = splitName;
this.mType = type;
@@ -219,7 +220,7 @@
/**
* Checksum value.
*/
- public @NonNull byte[] getValue() {
+ public byte @NonNull [] getValue() {
return mValue;
}
diff --git a/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java b/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java
index dbb0098..720bf2c 100644
--- a/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java
+++ b/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java
@@ -31,13 +31,14 @@
import android.util.Pair;
import android.util.SparseArray;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.concurrent.futures.ResolvableFuture;
import androidx.core.util.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
diff --git a/core/core-appdigest/src/main/java/androidx/core/appdigest/ChecksumsApiSImpl.java b/core/core-appdigest/src/main/java/androidx/core/appdigest/ChecksumsApiSImpl.java
index 6106c1d..f2f5fb0 100644
--- a/core/core-appdigest/src/main/java/androidx/core/appdigest/ChecksumsApiSImpl.java
+++ b/core/core-appdigest/src/main/java/androidx/core/appdigest/ChecksumsApiSImpl.java
@@ -29,8 +29,6 @@
import android.util.Log;
import android.util.SparseArray;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.concurrent.futures.ResolvableFuture;
@@ -44,6 +42,8 @@
import org.bouncycastle.cms.SignerInformationStore;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
import org.bouncycastle.operator.OperatorCreationException;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -282,7 +282,7 @@
}
}
- private static @NonNull android.content.pm.Checksum readFromStream(@NonNull DataInputStream dis)
+ private static android.content.pm.@NonNull Checksum readFromStream(@NonNull DataInputStream dis)
throws IOException {
final int type = dis.readInt();
@@ -301,7 +301,7 @@
}
private static void writeToStream(@NonNull DataOutputStream dos,
- @NonNull android.content.pm.Checksum checksum) throws IOException {
+ android.content.pm.@NonNull Checksum checksum) throws IOException {
dos.writeInt(checksum.getType());
final byte[] valueBytes = checksum.getValue();
@@ -339,7 +339,7 @@
* @param signature detached PKCS7 signature in DER format
* @return all certificates that passed verification
*/
- private static @NonNull Certificate[] verifySignature(android.content.pm.Checksum[] checksums,
+ private static Certificate @NonNull [] verifySignature(android.content.pm.Checksum[] checksums,
byte[] signature) throws NoSuchAlgorithmException, IOException, SignatureException {
final byte[] blob;
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
diff --git a/core/core-appdigest/src/main/java/androidx/core/appdigest/VerityTreeBuilder.java b/core/core-appdigest/src/main/java/androidx/core/appdigest/VerityTreeBuilder.java
index 37870ca..6c8a379 100644
--- a/core/core-appdigest/src/main/java/androidx/core/appdigest/VerityTreeBuilder.java
+++ b/core/core-appdigest/src/main/java/androidx/core/appdigest/VerityTreeBuilder.java
@@ -16,7 +16,7 @@
package androidx.core.appdigest;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
import java.io.IOException;
import java.io.RandomAccessFile;
diff --git a/core/core-backported-fixes/integration-tests/testapp/build.gradle b/core/core-backported-fixes/integration-tests/testapp/build.gradle
index 55fe60c..e336fb3 100644
--- a/core/core-backported-fixes/integration-tests/testapp/build.gradle
+++ b/core/core-backported-fixes/integration-tests/testapp/build.gradle
@@ -25,8 +25,8 @@
api(libs.kotlinStdlib)
implementation(project(":core:core-backported-fixes"))
- implementation project(":activity:activity-compose")
- implementation project(":activity:activity")
+ implementation(project(":activity:activity-compose"))
+ implementation(project(":activity:activity"))
implementation(project(":core:core-ktx"))
implementation(project(":compose:material:material"))
implementation(project(":compose:material3:material3"))
diff --git a/core/core-backported-fixes/src/main/java/androidx/core/backported/fixes/KnownIssue.kt b/core/core-backported-fixes/src/main/java/androidx/core/backported/fixes/KnownIssue.kt
index 1de0d63..fa82720 100644
--- a/core/core-backported-fixes/src/main/java/androidx/core/backported/fixes/KnownIssue.kt
+++ b/core/core-backported-fixes/src/main/java/androidx/core/backported/fixes/KnownIssue.kt
@@ -17,9 +17,31 @@
package androidx.core.backported.fixes
/** List of all known issue reportable by [BackportedFixManager] */
-internal class KnownIssue private constructor(val id: Long, val alias: Int) {
+internal class KnownIssue
+private constructor(
+
+ /**
+ * The public id of this issue in the [Google Issue Tracker](https://issuetracker.google.com)
+ */
+ val id: Long,
+ /**
+ * The alias for this issue, if one exists.
+ *
+ * Known issues can have at most one alias.
+ *
+ * The value 0 indicates there is no alias for this issue. Non-zero alias values are unique
+ * across all known issues.
+ */
+ val alias: Int
+) {
// TODO b/381266031 - Make public
// TODO b/381267367 - Add link to public list issues
+
+ /**
+ * The url to the [Google Issue Tracker](https://issuetracker.google.com) for this known issue.
+ */
+ internal val url = "https://issuetracker.google.com/issues/$id"
+
override fun equals(other: Any?) = other is KnownIssue && id == other.id
override fun hashCode() = id.hashCode()
diff --git a/core/core-i18n/src/androidTest/java/androidx/core/i18n/DateTimeFormatterTest.kt b/core/core-i18n/src/androidTest/java/androidx/core/i18n/DateTimeFormatterTest.kt
index 26dc597..da8a883 100644
--- a/core/core-i18n/src/androidTest/java/androidx/core/i18n/DateTimeFormatterTest.kt
+++ b/core/core-i18n/src/androidTest/java/androidx/core/i18n/DateTimeFormatterTest.kt
@@ -16,7 +16,6 @@
package androidx.core.i18n
-import android.annotation.SuppressLint
import android.os.Build
import android.util.Log
import androidx.core.i18n.DateTimeFormatterSkeletonOptions as SkeletonOptions
@@ -40,7 +39,6 @@
/** Must execute on an Android device. */
@RunWith(AndroidJUnit4::class)
-@SuppressLint("ClassVerificationFailure")
class DateTimeFormatterTest {
companion object {
// Lollipop introduced Locale.toLanguageTag and Locale.forLanguageTag
diff --git a/core/core-ktx/src/main/java/androidx/core/graphics/Bitmap.kt b/core/core-ktx/src/main/java/androidx/core/graphics/Bitmap.kt
index 1a34565..2bd6c43 100644
--- a/core/core-ktx/src/main/java/androidx/core/graphics/Bitmap.kt
+++ b/core/core-ktx/src/main/java/androidx/core/graphics/Bitmap.kt
@@ -18,7 +18,6 @@
package androidx.core.graphics
-import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.ColorSpace
@@ -101,7 +100,6 @@
* @param colorSpace The new bitmap's color space
* @return A new bitmap with the specified dimensions and config
*/
-@SuppressLint("ClassVerificationFailure") // Inline fun
@RequiresApi(26)
public inline fun createBitmap(
width: Int,
diff --git a/core/core-ktx/src/main/java/androidx/core/graphics/Color.kt b/core/core-ktx/src/main/java/androidx/core/graphics/Color.kt
index 83375f2b..b7336c2 100644
--- a/core/core-ktx/src/main/java/androidx/core/graphics/Color.kt
+++ b/core/core-ktx/src/main/java/androidx/core/graphics/Color.kt
@@ -18,7 +18,6 @@
package androidx.core.graphics
-import android.annotation.SuppressLint
import android.graphics.Color
import android.graphics.ColorSpace
import androidx.annotation.ColorInt
@@ -34,9 +33,7 @@
* val (red, green, blue) = myColor
* ```
*/
-@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
-public inline operator fun Color.component1(): Float = getComponent(0)
+@RequiresApi(26) public inline operator fun Color.component1(): Float = getComponent(0)
/**
* Returns the second component of the color. For instance, when the color model of the color is
@@ -47,9 +44,7 @@
* val (red, green, blue) = myColor
* ```
*/
-@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
-public inline operator fun Color.component2(): Float = getComponent(1)
+@RequiresApi(26) public inline operator fun Color.component2(): Float = getComponent(1)
/**
* Returns the third component of the color. For instance, when the color model of the color is
@@ -59,9 +54,7 @@
* val (red, green, blue) = myColor
* ```
*/
-@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
-public inline operator fun Color.component3(): Float = getComponent(2)
+@RequiresApi(26) public inline operator fun Color.component3(): Float = getComponent(2)
/**
* Returns the fourth component of the color. For instance, when the color model of the color is
@@ -72,9 +65,7 @@
* val (red, green, blue, alpha) = myColor
* ```
*/
-@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
-public inline operator fun Color.component4(): Float = getComponent(3)
+@RequiresApi(26) public inline operator fun Color.component4(): Float = getComponent(3)
/**
* Composites two translucent colors together. More specifically, adds two colors using the
@@ -93,7 +84,6 @@
* colors do not match
*/
@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
public operator fun Color.plus(c: Color): Color = ColorUtils.compositeColors(c, this)
/**
@@ -189,7 +179,6 @@
* relative luminance defined in WCAG 2.0, W3C Recommendation 11 December 2008.
*/
@get:RequiresApi(26)
-@get:SuppressLint("ClassVerificationFailure")
public inline val @receiver:ColorInt Int.luminance: Float
get() = Color.luminance(this)
@@ -197,16 +186,13 @@
* Creates a new [Color] instance from a color int. The resulting color is in the
* [sRGB][android.graphics.ColorSpace.Named.SRGB] color space.
*/
-@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
-public inline fun @receiver:ColorInt Int.toColor(): Color = Color.valueOf(this)
+@RequiresApi(26) public inline fun @receiver:ColorInt Int.toColor(): Color = Color.valueOf(this)
/**
* Converts the specified ARGB [color int][Color] to an RGBA [color long][Color] in the
* [sRGB][android.graphics.ColorSpace.Named.SRGB] color space.
*/
@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
@ColorLong
public inline fun @receiver:ColorInt Int.toColorLong(): Long = Color.pack(this)
@@ -220,7 +206,6 @@
* ```
*/
@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
public inline operator fun @receiver:ColorLong Long.component1(): Float = Color.red(this)
/**
@@ -233,7 +218,6 @@
* ```
*/
@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
public inline operator fun @receiver:ColorLong Long.component2(): Float = Color.green(this)
/**
@@ -246,7 +230,6 @@
* ```
*/
@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
public inline operator fun @receiver:ColorLong Long.component3(): Float = Color.blue(this)
/**
@@ -259,7 +242,6 @@
* ```
*/
@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
public inline operator fun @receiver:ColorLong Long.component4(): Float = Color.alpha(this)
/**
@@ -269,7 +251,6 @@
* ```
*/
@get:RequiresApi(26)
-@get:SuppressLint("ClassVerificationFailure")
public inline val @receiver:ColorLong Long.alpha: Float
get() = Color.alpha(this)
@@ -280,7 +261,6 @@
* ```
*/
@get:RequiresApi(26)
-@get:SuppressLint("ClassVerificationFailure")
public inline val @receiver:ColorLong Long.red: Float
get() = Color.red(this)
@@ -291,7 +271,6 @@
* ```
*/
@get:RequiresApi(26)
-@get:SuppressLint("ClassVerificationFailure")
public inline val @receiver:ColorLong Long.green: Float
get() = Color.green(this)
@@ -302,7 +281,6 @@
* ```
*/
@get:RequiresApi(26)
-@get:SuppressLint("ClassVerificationFailure")
public inline val @receiver:ColorLong Long.blue: Float
get() = Color.blue(this)
@@ -311,18 +289,14 @@
* WCAG 2.0, W3C Recommendation 11 December 2008.
*/
@get:RequiresApi(26)
-@get:SuppressLint("ClassVerificationFailure")
public inline val @receiver:ColorLong Long.luminance: Float
get() = Color.luminance(this)
/** Creates a new [Color] instance from a [color long][Color]. */
-@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
-public inline fun @receiver:ColorLong Long.toColor(): Color = Color.valueOf(this)
+@RequiresApi(26) public inline fun @receiver:ColorLong Long.toColor(): Color = Color.valueOf(this)
/** Converts the specified [color long][Color] to an ARGB [color int][Color]. */
@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
@ColorInt
public inline fun @receiver:ColorLong Long.toColorInt(): Int = Color.toArgb(this)
@@ -330,19 +304,16 @@
* Indicates whether the color is in the [sRGB][android.graphics.ColorSpace.Named.SRGB] color space.
*/
@get:RequiresApi(26)
-@get:SuppressLint("ClassVerificationFailure")
public inline val @receiver:ColorLong Long.isSrgb: Boolean
get() = Color.isSrgb(this)
/** Indicates whether the color is in a [wide-gamut][android.graphics.ColorSpace] color space. */
@get:RequiresApi(26)
-@get:SuppressLint("ClassVerificationFailure")
public inline val @receiver:ColorLong Long.isWideGamut: Boolean
get() = Color.isWideGamut(this)
/** Returns the color space encoded in the specified color long. */
@get:RequiresApi(26)
-@get:SuppressLint("ClassVerificationFailure")
public inline val @receiver:ColorLong Long.colorSpace: ColorSpace
get() = Color.colorSpace(this)
@@ -354,7 +325,6 @@
* ```
*/
@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
@ColorLong
public inline infix fun @receiver:ColorInt Int.convertTo(colorSpace: ColorSpace.Named): Long =
Color.convert(this, ColorSpace.get(colorSpace))
@@ -367,7 +337,6 @@
* ```
*/
@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
@ColorLong
public inline infix fun @receiver:ColorInt Int.convertTo(colorSpace: ColorSpace): Long =
Color.convert(this, colorSpace)
@@ -380,7 +349,6 @@
* ```
*/
@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
@ColorLong
public inline infix fun @receiver:ColorLong Long.convertTo(colorSpace: ColorSpace.Named): Long =
Color.convert(this, ColorSpace.get(colorSpace))
@@ -393,7 +361,6 @@
* ```
*/
@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
@ColorLong
public inline infix fun @receiver:ColorLong Long.convertTo(colorSpace: ColorSpace): Long =
Color.convert(this, colorSpace)
@@ -406,7 +373,6 @@
* ```
*/
@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
public inline infix fun Color.convertTo(colorSpace: ColorSpace.Named): Color =
convert(ColorSpace.get(colorSpace))
@@ -418,7 +384,6 @@
* ```
*/
@RequiresApi(26)
-@SuppressLint("ClassVerificationFailure")
public inline infix fun Color.convertTo(colorSpace: ColorSpace): Color = convert(colorSpace)
/**
diff --git a/core/core-ktx/src/main/java/androidx/core/graphics/Path.kt b/core/core-ktx/src/main/java/androidx/core/graphics/Path.kt
index cc7cce0..099008f 100644
--- a/core/core-ktx/src/main/java/androidx/core/graphics/Path.kt
+++ b/core/core-ktx/src/main/java/androidx/core/graphics/Path.kt
@@ -14,12 +14,10 @@
* limitations under the License.
*/
-@file:SuppressLint("ClassVerificationFailure") // Entire file is RequiresApi(19)
@file:Suppress("NOTHING_TO_INLINE")
package androidx.core.graphics
-import android.annotation.SuppressLint
import android.graphics.Path
import androidx.annotation.RequiresApi
diff --git a/core/core-ktx/src/main/java/androidx/core/graphics/drawable/ColorDrawable.kt b/core/core-ktx/src/main/java/androidx/core/graphics/drawable/ColorDrawable.kt
index bfdfd55..9d0afc0 100644
--- a/core/core-ktx/src/main/java/androidx/core/graphics/drawable/ColorDrawable.kt
+++ b/core/core-ktx/src/main/java/androidx/core/graphics/drawable/ColorDrawable.kt
@@ -18,7 +18,6 @@
package androidx.core.graphics.drawable
-import android.annotation.SuppressLint
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import androidx.annotation.ColorInt
@@ -28,6 +27,4 @@
public inline fun @receiver:ColorInt Int.toDrawable(): ColorDrawable = ColorDrawable(this)
/** Create a [ColorDrawable] from this [Color] (via [Color.toArgb]). */
-@SuppressLint("ClassVerificationFailure") // Inline fun
-@RequiresApi(26)
-public inline fun Color.toDrawable(): ColorDrawable = ColorDrawable(toArgb())
+@RequiresApi(26) public inline fun Color.toDrawable(): ColorDrawable = ColorDrawable(toArgb())
diff --git a/core/core-ktx/src/main/java/androidx/core/graphics/drawable/Icon.kt b/core/core-ktx/src/main/java/androidx/core/graphics/drawable/Icon.kt
index 3f48a14..ca78fda 100644
--- a/core/core-ktx/src/main/java/androidx/core/graphics/drawable/Icon.kt
+++ b/core/core-ktx/src/main/java/androidx/core/graphics/drawable/Icon.kt
@@ -14,12 +14,10 @@
* limitations under the License.
*/
-@file:SuppressLint("ClassVerificationFailure") // Entire file is RequiresApi(26)
@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
package androidx.core.graphics.drawable
-import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.drawable.Icon
import android.net.Uri
diff --git a/core/core-ktx/src/main/java/androidx/core/util/Half.kt b/core/core-ktx/src/main/java/androidx/core/util/Half.kt
index 637b36e..2cd1972 100644
--- a/core/core-ktx/src/main/java/androidx/core/util/Half.kt
+++ b/core/core-ktx/src/main/java/androidx/core/util/Half.kt
@@ -14,12 +14,10 @@
* limitations under the License.
*/
-@file:SuppressLint("ClassVerificationFailure") // Entire file is RequiresApi(26)
@file:Suppress("NOTHING_TO_INLINE") // Aliases to other public API.
package androidx.core.util
-import android.annotation.SuppressLint
import android.util.Half
import androidx.annotation.HalfFloat
import androidx.annotation.RequiresApi
diff --git a/core/core-ktx/src/main/java/androidx/core/util/Range.kt b/core/core-ktx/src/main/java/androidx/core/util/Range.kt
index 3b19652..b09bd62 100644
--- a/core/core-ktx/src/main/java/androidx/core/util/Range.kt
+++ b/core/core-ktx/src/main/java/androidx/core/util/Range.kt
@@ -14,12 +14,10 @@
* limitations under the License.
*/
-@file:SuppressLint("ClassVerificationFailure") // Entire file is RequiresApi(21)
@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
package androidx.core.util
-import android.annotation.SuppressLint
import android.util.Range
import androidx.annotation.RequiresApi
diff --git a/core/core-ktx/src/main/java/androidx/core/util/Size.kt b/core/core-ktx/src/main/java/androidx/core/util/Size.kt
index 69d2da2..017c840 100644
--- a/core/core-ktx/src/main/java/androidx/core/util/Size.kt
+++ b/core/core-ktx/src/main/java/androidx/core/util/Size.kt
@@ -14,12 +14,10 @@
* limitations under the License.
*/
-@file:SuppressLint("ClassVerificationFailure") // Entire file is RequiresApi(21)
@file:Suppress("NOTHING_TO_INLINE")
package androidx.core.util
-import android.annotation.SuppressLint
import android.util.Size
import android.util.SizeF
import androidx.annotation.RequiresApi
diff --git a/core/core-location-altitude/build.gradle b/core/core-location-altitude/build.gradle
index da153cb..681776b 100644
--- a/core/core-location-altitude/build.gradle
+++ b/core/core-location-altitude/build.gradle
@@ -40,16 +40,17 @@
}
dependencies {
+ api(libs.jspecify)
api(libs.kotlinStdlib)
api("androidx.annotation:annotation:1.8.1")
implementation(project(":core:core-location-altitude-proto"))
implementation(libs.autoValueAnnotations)
implementation("androidx.core:core:1.13.0")
- implementation("androidx.room:room-runtime:2.4.3")
+ implementation(project(":room:room-runtime"))
annotationProcessor(libs.autoValue)
- annotationProcessor("androidx.room:room-compiler:2.4.3")
+ annotationProcessor(project(":room:room-compiler"))
androidTestImplementation(libs.junit)
androidTestImplementation(libs.testCore)
@@ -64,6 +65,4 @@
mavenVersion = LibraryVersions.CORE_LOCATION_ALTITUDE
inceptionYear = "2022"
description = "Provides compatibility APIs concerning location altitudes."
- // TODO: b/326456246
- optOutJSpecify = true
}
diff --git a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/AltitudeConverterCompat.java b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/AltitudeConverterCompat.java
index c172d59..01713e6 100644
--- a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/AltitudeConverterCompat.java
+++ b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/AltitudeConverterCompat.java
@@ -21,12 +21,13 @@
import android.os.Build;
import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;
import androidx.core.location.altitude.impl.AltitudeConverter;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.IOException;
/**
@@ -46,8 +47,7 @@
private static final Object sLock = new Object();
@GuardedBy("sLock")
- @Nullable
- private static AltitudeConverter sAltitudeConverter;
+ private static @Nullable AltitudeConverter sAltitudeConverter;
/** Prevents instantiation. */
private AltitudeConverterCompat() {
@@ -91,8 +91,7 @@
private static final Object sLock = new Object();
@GuardedBy("sLock")
- @Nullable
- private static Object sAltitudeConverter;
+ private static @Nullable Object sAltitudeConverter;
/** Prevents instantiation. */
private Api34Impl() {
diff --git a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/AltitudeConverter.java b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/AltitudeConverter.java
index 3fd1272..42d805b 100644
--- a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/AltitudeConverter.java
+++ b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/AltitudeConverter.java
@@ -19,11 +19,12 @@
import android.content.Context;
import android.location.Location;
-import androidx.annotation.NonNull;
import androidx.core.location.LocationCompat;
import androidx.core.location.altitude.impl.proto.MapParamsProto;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+
import java.io.IOException;
/** Implements {@link androidx.core.location.altitude.AltitudeConverterCompat}. */
@@ -80,8 +81,7 @@
* 2023 IEEE/ION Position, Location and Navigation Symposium (PLANS).
* </pre>
*/
- @NonNull
- private static long[] findMapSquare(@NonNull MapParamsProto params,
+ private static long @NonNull [] findMapSquare(@NonNull MapParamsProto params,
@NonNull Location location) {
long s2CellId =
S2CellIdUtils.fromLatLngDegrees(location.getLatitude(), location.getLongitude());
@@ -143,7 +143,7 @@
* such a common neighbor does not exist, returns z11.
*/
private static long findCommonNeighbor(
- @NonNull long[] edgeNeighbors, @NonNull long[] otherEdgeNeighbors, long z11) {
+ long @NonNull [] edgeNeighbors, long @NonNull [] otherEdgeNeighbors, long z11) {
for (long edgeNeighbor : edgeNeighbors) {
if (edgeNeighbor == z11) {
continue;
@@ -163,7 +163,7 @@
* accuracy; otherwise, does not add a corresponding accuracy.
*/
private static void addMslAltitude(@NonNull MapParamsProto params,
- @NonNull double[] geoidHeightsMeters, @NonNull Location location) {
+ double @NonNull [] geoidHeightsMeters, @NonNull Location location) {
double h0 = geoidHeightsMeters[0];
double h1 = geoidHeightsMeters[1];
double h2 = geoidHeightsMeters[2];
diff --git a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/GeoidHeightMap.java b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/GeoidHeightMap.java
index 811a19f..cdaf821 100644
--- a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/GeoidHeightMap.java
+++ b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/GeoidHeightMap.java
@@ -22,8 +22,6 @@
import android.util.LruCache;
import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.location.altitude.impl.db.AltitudeConverterDatabase;
import androidx.core.location.altitude.impl.db.MapParamsEntity;
import androidx.core.location.altitude.impl.db.TilesEntity;
@@ -33,6 +31,9 @@
import androidx.core.util.Preconditions;
import androidx.room.Room;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
@@ -58,20 +59,17 @@
private static final Object sLock = new Object();
@GuardedBy("sLock")
- @Nullable
- private static MapParamsProto sParams;
+ private static @Nullable MapParamsProto sParams;
/** Defines the resource database for {@link AltitudeConverter}. */
@GuardedBy("sLock")
- @Nullable
- private static AltitudeConverterDatabase sDatabase;
+ private static @Nullable AltitudeConverterDatabase sDatabase;
/** Defines a cache large enough to hold all cache tiles needed for interpolation. */
private final LruCache<Long, S2TileProto> mCacheTiles = new LruCache<>(4);
- @NonNull
- public static AltitudeConverterDatabase getDatabase(@NonNull Context context) {
+ public static @NonNull AltitudeConverterDatabase getDatabase(@NonNull Context context) {
synchronized (sLock) {
if (sDatabase == null) {
sDatabase = Room.databaseBuilder(context.getApplicationContext(),
@@ -86,8 +84,7 @@
* Returns the singleton parameter instance for a spherically projected geoid height map and its
* corresponding tile management.
*/
- @NonNull
- public static MapParamsProto getParams(@NonNull Context context) throws IOException {
+ public static @NonNull MapParamsProto getParams(@NonNull Context context) throws IOException {
synchronized (sLock) {
if (sParams == null) {
MapParamsEntity current = getDatabase(context).mapParamsDao().getCurrent();
@@ -104,8 +101,7 @@
return S2CellIdUtils.getParent(s2CellId, params.getCacheTileS2Level());
}
- @NonNull
- private static String getDiskToken(@NonNull MapParamsProto params, long s2CellId) {
+ private static @NonNull String getDiskToken(@NonNull MapParamsProto params, long s2CellId) {
return S2CellIdUtils.getToken(
S2CellIdUtils.getParent(s2CellId, params.getDiskTileS2Level()));
}
@@ -117,7 +113,7 @@
*/
private static boolean getUnitIntervalValues(@NonNull MapParamsProto params,
@NonNull TileFunction tileFunction,
- @NonNull long[] s2CellIds, @NonNull double[] values) throws IOException {
+ long @NonNull [] s2CellIds, double @NonNull [] values) throws IOException {
int len = s2CellIds.length;
S2TileProto[] tiles = new S2TileProto[len];
@@ -150,9 +146,9 @@
@SuppressWarnings("ReferenceEquality")
private static void mergeByteBufferValues(@NonNull MapParamsProto params,
- @NonNull long[] s2CellIds,
- @NonNull S2TileProto[] tiles,
- int tileIndex, @NonNull double[] values) {
+ long @NonNull [] s2CellIds,
+ S2TileProto @NonNull [] tiles,
+ int tileIndex, double @NonNull [] values) {
ByteString byteString = tiles[tileIndex].getByteBuffer();
if (byteString.isEmpty()) {
return;
@@ -177,17 +173,17 @@
}
private static void mergeByteJpegValues(@NonNull MapParamsProto params,
- @NonNull long[] s2CellIds,
- @NonNull S2TileProto[] tiles,
- int tileIndex, @NonNull double[] values) throws IOException {
+ long @NonNull [] s2CellIds,
+ S2TileProto @NonNull [] tiles,
+ int tileIndex, double @NonNull [] values) throws IOException {
mergeByteImageValues(params, tiles[tileIndex].getByteJpeg(), s2CellIds, tiles, tileIndex,
values);
}
private static void mergeBytePngValues(@NonNull MapParamsProto params,
- @NonNull long[] s2CellIds,
- @NonNull S2TileProto[] tiles,
- int tileIndex, @NonNull double[] values) throws IOException {
+ long @NonNull [] s2CellIds,
+ S2TileProto @NonNull [] tiles,
+ int tileIndex, double @NonNull [] values) throws IOException {
mergeByteImageValues(params, tiles[tileIndex].getBytePng(), s2CellIds, tiles, tileIndex,
values);
}
@@ -195,8 +191,8 @@
@SuppressWarnings("ReferenceEquality")
private static void mergeByteImageValues(@NonNull MapParamsProto params,
@NonNull ByteString byteString,
- @NonNull long[] s2CellIds,
- @NonNull S2TileProto[] tiles, int tileIndex, @NonNull double[] values)
+ long @NonNull [] s2CellIds,
+ S2TileProto @NonNull [] tiles, int tileIndex, double @NonNull [] values)
throws IOException {
if (byteString.isEmpty()) {
return;
@@ -238,7 +234,7 @@
* Throws an {@link IllegalArgumentException} if the {@code s2CellIds} has an invalid length or
* ID.
*/
- private static void validate(@NonNull MapParamsProto params, @NonNull long[] s2CellIds) {
+ private static void validate(@NonNull MapParamsProto params, long @NonNull [] s2CellIds) {
Preconditions.checkArgument(s2CellIds.length == 4);
for (long s2CellId : s2CellIds) {
Preconditions.checkArgument(S2CellIdUtils.getLevel(s2CellId) == params.getMapS2Level());
@@ -250,9 +246,8 @@
* {@code s2CellIds}. Throws an {@link IOException} if a geoid height cannot be calculated for
* an ID.
*/
- @NonNull
- public double[] readGeoidHeights(@NonNull MapParamsProto params, @NonNull Context context,
- @NonNull long[] s2CellIds) throws IOException {
+ public double @NonNull [] readGeoidHeights(@NonNull MapParamsProto params,
+ @NonNull Context context, long @NonNull [] s2CellIds) throws IOException {
validate(params, s2CellIds);
double[] heightsMeters = new double[s2CellIds.length];
if (getGeoidHeights(params, mCacheTiles::get, s2CellIds, heightsMeters)) {
@@ -271,9 +266,8 @@
* be loaded from raw assets. Returns the heights if present for all IDs; otherwise, returns
* null.
*/
- @Nullable
- public double[] readGeoidHeights(@NonNull MapParamsProto params, @NonNull long[] s2CellIds)
- throws IOException {
+ public double @Nullable [] readGeoidHeights(@NonNull MapParamsProto params,
+ long @NonNull [] s2CellIds) throws IOException {
validate(params, s2CellIds);
double[] heightsMeters = new double[s2CellIds.length];
if (getGeoidHeights(params, mCacheTiles::get, s2CellIds, heightsMeters)) {
@@ -288,8 +282,8 @@
* returns false and adds NaNs for absent heights.
*/
private boolean getGeoidHeights(@NonNull MapParamsProto params,
- @NonNull TileFunction tileFunction, @NonNull long[] s2CellIds,
- @NonNull double[] heightsMeters) throws IOException {
+ @NonNull TileFunction tileFunction, long @NonNull [] s2CellIds,
+ double @NonNull [] heightsMeters) throws IOException {
boolean allFound = getUnitIntervalValues(params, tileFunction, s2CellIds, heightsMeters);
for (int i = 0; i < heightsMeters.length; i++) {
// NaNs are properly preserved.
@@ -299,9 +293,8 @@
return allFound;
}
- @NonNull
- private TileFunction loadFromCacheAndDisk(@NonNull MapParamsProto params,
- @NonNull Context context, @NonNull long[] s2CellIds) throws IOException {
+ private @NonNull TileFunction loadFromCacheAndDisk(@NonNull MapParamsProto params,
+ @NonNull Context context, long @NonNull [] s2CellIds) throws IOException {
int len = s2CellIds.length;
// Enable batch loading by finding all cache keys upfront.
@@ -353,8 +346,8 @@
}
private void mergeFromDiskTile(@NonNull MapParamsProto params, @NonNull S2TileProto diskTile,
- @NonNull long[] cacheKeys, @NonNull String[] diskTokens, int diskTokenIndex,
- @NonNull S2TileProto[] loadedTiles) throws IOException {
+ long @NonNull [] cacheKeys, String @NonNull [] diskTokens, int diskTokenIndex,
+ S2TileProto @NonNull [] loadedTiles) throws IOException {
int len = cacheKeys.length;
int numMapCellsPerCacheTile =
1 << (2 * (params.getMapS2Level() - params.getCacheTileS2Level()));
@@ -404,7 +397,6 @@
/** Defines a function-like object to retrieve tiles for cache keys. */
private interface TileFunction {
- @Nullable
- S2TileProto getTile(long cacheKey);
+ @Nullable S2TileProto getTile(long cacheKey);
}
}
diff --git a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/S2CellIdUtils.java b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/S2CellIdUtils.java
index 1c09c33..d4d6233 100644
--- a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/S2CellIdUtils.java
+++ b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/S2CellIdUtils.java
@@ -16,7 +16,7 @@
package androidx.core.location.altitude.impl;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
import java.util.Arrays;
import java.util.Locale;
@@ -117,7 +117,7 @@
* <p>Inserts in the order of down, right, up, and left directions, in that order. All
* neighbors are guaranteed to be distinct.
*/
- static void getEdgeNeighbors(long s2CellId, @NonNull long[] neighbors) {
+ static void getEdgeNeighbors(long s2CellId, long @NonNull [] neighbors) {
int level = getLevel(s2CellId);
int size = levelToSizeIj(level);
int face = getFace(s2CellId);
@@ -210,8 +210,7 @@
* Encodes the S2 cell id to compact text strings suitable for display or indexing. Cells at
* lower levels (i.e., larger cells) are encoded into fewer characters.
*/
- @NonNull
- static String getToken(long s2CellId) {
+ static @NonNull String getToken(long s2CellId) {
if (s2CellId == 0) {
return "X";
}
@@ -425,8 +424,7 @@
return 1.0 + machEps;
}
- @NonNull
- private static UvTransform[] createUvTransforms() {
+ private static UvTransform @NonNull [] createUvTransforms() {
UvTransform[] uvTransforms = new UvTransform[NUM_FACES];
uvTransforms[0] =
new UvTransform() {
@@ -509,8 +507,7 @@
return uvTransforms;
}
- @NonNull
- private static XyzTransform[] createXyzTransforms() {
+ private static XyzTransform @NonNull [] createXyzTransforms() {
XyzTransform[] xyzTransforms = new XyzTransform[NUM_FACES];
xyzTransforms[0] =
new XyzTransform() {
diff --git a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/AltitudeConverterDatabase.java b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/AltitudeConverterDatabase.java
index b84b467..eb8b383 100644
--- a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/AltitudeConverterDatabase.java
+++ b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/AltitudeConverterDatabase.java
@@ -16,22 +16,21 @@
package androidx.core.location.altitude.impl.db;
-import androidx.annotation.NonNull;
import androidx.core.location.altitude.impl.AltitudeConverter;
import androidx.room.Database;
import androidx.room.RoomDatabase;
import androidx.room.TypeConverters;
+import org.jspecify.annotations.NonNull;
+
/** Defines the resource database for {@link AltitudeConverter}. */
@Database(entities = {MapParamsEntity.class, TilesEntity.class}, version = 1, exportSchema = false)
@TypeConverters({MapParamsEntity.class, TilesEntity.class})
public abstract class AltitudeConverterDatabase extends RoomDatabase {
/** Returns the data access object for the MapParams table. */
- @NonNull
- public abstract MapParamsDao mapParamsDao();
+ public abstract @NonNull MapParamsDao mapParamsDao();
/** Returns the data access object for the Tiles table. */
- @NonNull
- public abstract TilesDao tilesDao();
+ public abstract @NonNull TilesDao tilesDao();
}
diff --git a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/MapParamsDao.java b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/MapParamsDao.java
index c5e18b8..927695d 100644
--- a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/MapParamsDao.java
+++ b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/MapParamsDao.java
@@ -16,16 +16,16 @@
package androidx.core.location.altitude.impl.db;
-import androidx.annotation.Nullable;
import androidx.room.Dao;
import androidx.room.Query;
+import org.jspecify.annotations.Nullable;
+
/** Provides data access for entities within the MapParams table. */
@Dao
public interface MapParamsDao {
/** Returns the most current map parameters. */
- @Nullable
@Query("SELECT * FROM MapParams ORDER BY id DESC LIMIT 1")
- MapParamsEntity getCurrent();
+ @Nullable MapParamsEntity getCurrent();
}
diff --git a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/MapParamsEntity.java b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/MapParamsEntity.java
index 7b27678..7abe9f7 100644
--- a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/MapParamsEntity.java
+++ b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/MapParamsEntity.java
@@ -18,8 +18,6 @@
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.location.altitude.impl.proto.InvalidProtocolBufferException;
import androidx.core.location.altitude.impl.proto.MapParamsProto;
import androidx.room.ColumnInfo;
@@ -31,6 +29,8 @@
import com.google.auto.value.AutoValue.CopyAnnotations;
import org.jetbrains.annotations.Contract;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/** Defines the entity type and its converters within the MapParams table. */
@AutoValue
@@ -39,23 +39,20 @@
private static final String TAG = "MapParamsEntity";
- @NonNull
@Contract("_, _ -> new")
- static MapParamsEntity create(int id, MapParamsProto value) {
+ static @NonNull MapParamsEntity create(int id, MapParamsProto value) {
return new AutoValue_MapParamsEntity(id, value);
}
/** Encodes a {@link MapParamsProto} */
- @NonNull
@TypeConverter
- public static byte[] fromValue(@NonNull MapParamsProto value) {
+ public static byte @NonNull [] fromValue(@NonNull MapParamsProto value) {
return value.toByteArray();
}
/** Decodes a {@link MapParamsProto} */
- @Nullable
@TypeConverter
- public static MapParamsProto toValue(@NonNull byte[] byteArray) {
+ public static @Nullable MapParamsProto toValue(byte @NonNull [] byteArray) {
try {
return MapParamsProto.parseFrom(byteArray);
} catch (InvalidProtocolBufferException e) {
@@ -74,6 +71,5 @@
*/
@CopyAnnotations
@ColumnInfo(name = "value")
- @NonNull
- public abstract MapParamsProto value();
+ public abstract @NonNull MapParamsProto value();
}
diff --git a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/TilesDao.java b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/TilesDao.java
index 719d171..fee5b3f 100644
--- a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/TilesDao.java
+++ b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/TilesDao.java
@@ -16,17 +16,17 @@
package androidx.core.location.altitude.impl.db;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.room.Dao;
import androidx.room.Query;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/** Provides data access for entities within the Tiles table. */
@Dao
public interface TilesDao {
/** Returns the tile associated with the provided token. */
- @Nullable
@Query("SELECT * FROM Tiles WHERE token = :token LIMIT 1")
- TilesEntity get(@NonNull String token);
+ @Nullable TilesEntity get(@NonNull String token);
}
diff --git a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/TilesEntity.java b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/TilesEntity.java
index 6b3f29f..c2edb37 100644
--- a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/TilesEntity.java
+++ b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/impl/db/TilesEntity.java
@@ -18,8 +18,6 @@
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.location.altitude.impl.proto.InvalidProtocolBufferException;
import androidx.core.location.altitude.impl.proto.S2TileProto;
import androidx.room.ColumnInfo;
@@ -30,6 +28,9 @@
import com.google.auto.value.AutoValue;
import com.google.auto.value.AutoValue.CopyAnnotations;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/** Defines the entity type and its converters within the Tiles table. */
@AutoValue
@Entity(tableName = "Tiles")
@@ -42,16 +43,14 @@
}
/** Encodes a {@link S2TileProto}. */
- @NonNull
@TypeConverter
- public static byte[] fromTile(@NonNull S2TileProto tile) {
+ public static byte @NonNull [] fromTile(@NonNull S2TileProto tile) {
return tile.toByteArray();
}
/** Decodes a {@link S2TileProto}. */
- @Nullable
@TypeConverter
- public static S2TileProto toTile(@NonNull byte[] byteArray) {
+ public static @Nullable S2TileProto toTile(byte @NonNull [] byteArray) {
try {
return S2TileProto.parseFrom(byteArray);
} catch (InvalidProtocolBufferException e) {
@@ -64,12 +63,10 @@
@CopyAnnotations
@PrimaryKey
@ColumnInfo(name = "token")
- @NonNull
- public abstract String token();
+ public abstract @NonNull String token();
/** Returns a tile within an S2 cell ID to unit interval map. */
@CopyAnnotations
@ColumnInfo(name = "tile")
- @NonNull
- public abstract S2TileProto tile();
+ public abstract @NonNull S2TileProto tile();
}
diff --git a/core/core-telecom/integration-tests/testapp/src/main/AndroidManifest.xml b/core/core-telecom/integration-tests/testapp/src/main/AndroidManifest.xml
index 378517a..2f54068 100644
--- a/core/core-telecom/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/core/core-telecom/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -20,6 +20,9 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- BLUETOOTH_CONNECT is needed in order to expose the BT device name -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+ <!-- required permissions in order to post call-style notifications -->
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+ <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<application
android:icon="@drawable/ic_launcher"
@@ -28,6 +31,7 @@
<activity
android:name=".CallingMainActivity"
android:exported="true"
+ android:launchMode="singleTask"
android:label="@string/main_activity_name"
android:theme="@style/Theme.AppCompat">
<intent-filter>
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallListAdapter.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallListAdapter.kt
index c47516a..9a52dc1 100644
--- a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallListAdapter.kt
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallListAdapter.kt
@@ -103,6 +103,7 @@
}
when (ItemsViewModel.callObject.mCallControl!!.setActive()) {
is CallControlResult.Success -> {
+ ItemsViewModel.callObject.updateNotificationToOngoing()
holder.currentState.text = "CurrentState=[active]"
}
is CallControlResult.Error -> {
@@ -130,6 +131,7 @@
holder.disconnectButton.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch {
endAudioRecording()
+ ItemsViewModel.callObject.clearNotification()
ItemsViewModel.callObject.mCallControl?.disconnect(
DisconnectCause(DisconnectCause.LOCAL)
)
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt
index 6de2436..432f601 100644
--- a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt
@@ -18,6 +18,9 @@
import android.annotation.SuppressLint
import android.app.Activity
+import android.app.NotificationManager
+import android.content.Context
+import android.content.Intent
import android.os.Bundle
import android.telecom.DisconnectCause
import android.util.Log
@@ -31,11 +34,13 @@
import androidx.core.telecom.CallEndpointCompat
import androidx.core.telecom.CallsManager
import androidx.core.telecom.extensions.RaiseHandState
-import androidx.core.telecom.test.Utilities.Companion.ALL_CALL_CAPABILITIES
-import androidx.core.telecom.test.Utilities.Companion.INCOMING_NAME
-import androidx.core.telecom.test.Utilities.Companion.INCOMING_URI
-import androidx.core.telecom.test.Utilities.Companion.OUTGOING_NAME
-import androidx.core.telecom.test.Utilities.Companion.OUTGOING_URI
+import androidx.core.telecom.test.Constants.Companion.ALL_CALL_CAPABILITIES
+import androidx.core.telecom.test.Constants.Companion.INCOMING_NAME
+import androidx.core.telecom.test.Constants.Companion.INCOMING_URI
+import androidx.core.telecom.test.Constants.Companion.OUTGOING_NAME
+import androidx.core.telecom.test.Constants.Companion.OUTGOING_URI
+import androidx.core.telecom.test.NotificationsUtilities.Companion.IS_ANSWER_ACTION
+import androidx.core.telecom.test.NotificationsUtilities.Companion.NOTIFICATION_CHANNEL_ID
import androidx.core.telecom.util.ExperimentalAppActions
import androidx.core.view.WindowCompat
import androidx.recyclerview.widget.LinearLayoutManager
@@ -45,6 +50,7 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@@ -55,29 +61,38 @@
// Activity
private val TAG = CallingMainActivity::class.simpleName
private val mScope = CoroutineScope(Dispatchers.Default)
- private var mCallCount: Int = 0
-
+ private lateinit var mContext: Context
+ private var mCurrentCallCount: Int = 0
// Telecom
- private var mCallsManager: CallsManager? = null
-
+ private lateinit var mCallsManager: CallsManager
// Ongoing Call List
private var mRecyclerView: RecyclerView? = null
private var mCallObjects: ArrayList<CallRow> = ArrayList()
private lateinit var mAdapter: CallListAdapter
-
// Pre-Call Endpoint List
private var mPreCallEndpointsRecyclerView: RecyclerView? = null
private var mCurrentPreCallEndpoints: ArrayList<CallEndpointCompat> = arrayListOf()
private lateinit var mPreCallEndpointAdapter: PreCallEndpointsAdapter
+ // Notification
+ private var mNextNotificationId: Int = 1
+ private lateinit var mNotificationManager: NotificationManager
+ private val mNotificationActionInfoFlow: MutableStateFlow<NotificationActionInfo> =
+ MutableStateFlow(NotificationActionInfo(-1, false))
+
+ /**
+ * NotificationActionInfo couples information propagated from the Call-Style notification on
+ * which action button was clicked (e.g. answer the call or decline ) *
+ */
+ data class NotificationActionInfo(val id: Int, val isAnswer: Boolean)
override fun onCreate(savedInstanceState: Bundle?) {
+ Log.i(TAG, "onCreate")
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
-
setContentView(R.layout.activity_main)
-
+ mContext = applicationContext
+ initNotifications(mContext)
mCallsManager = CallsManager(this)
- mCallCount = 0
val raiseHandCheckBox = findViewById<CheckBox>(R.id.RaiseHandCheckbox)
val kickParticipantCheckBox = findViewById<CheckBox>(R.id.KickPartCheckbox)
@@ -154,6 +169,23 @@
mPreCallEndpointsRecyclerView?.adapter = mPreCallEndpointAdapter
}
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ Log.i(TAG, "onNewIntent: intent=[$intent]")
+ maybeHandleNotificationAction(intent)
+ }
+
+ private fun maybeHandleNotificationAction(intent: Intent?) {
+ if (intent != null) {
+ val id = intent.getIntExtra(NotificationsUtilities.NOTIFICATION_ID, -1)
+ if (id != -1) {
+ val isAnswer = intent.getBooleanExtra(IS_ANSWER_ACTION, false)
+ Log.i(TAG, "handleNotification: id=$id, isAnswer=$isAnswer")
+ mNotificationActionInfoFlow.value = NotificationActionInfo(id, isAnswer)
+ }
+ }
+ }
+
override fun onDestroy() {
super.onDestroy()
for (call in mCallObjects) {
@@ -165,6 +197,12 @@
}
}
}
+ NotificationsUtilities.deleteNotificationChannel(mContext)
+ }
+
+ private fun initNotifications(c: Context) {
+ NotificationsUtilities.initNotificationChannel(c)
+ mNotificationManager = c.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
}
@SuppressLint("WrongConstant")
@@ -179,7 +217,7 @@
if (streamingCheckBox.isChecked) {
capabilities = capabilities or CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING
}
- mCallsManager?.registerAppWithTelecom(capabilities)
+ mCallsManager.registerAppWithTelecom(capabilities)
}
private suspend fun addCallWithAttributes(
@@ -189,7 +227,8 @@
isKickParticipantEnabled: Boolean
) {
Log.i(TAG, "addCallWithAttributes: attributes=$attributes")
- val callObject = VoipCall()
+ val callObject = VoipCall(this, attributes)
+ callObject.setNotificationId(mNextNotificationId++)
try {
val handler = CoroutineExceptionHandler { _, exception ->
@@ -209,28 +248,43 @@
}
} catch (e: Exception) {
logException(e, "addCallWithAttributes: catch inner")
+ NotificationsUtilities.clearNotification(mContext, callObject.mNotificationId)
} finally {
Log.i(TAG, "addCallWithAttributes: finally block")
}
}
} catch (e: Exception) {
logException(e, "addCallWithAttributes: catch outer")
+ NotificationsUtilities.clearNotification(mContext, callObject.mNotificationId)
}
}
private suspend fun addCall(attributes: CallAttributesCompat, callObject: VoipCall) {
- mCallsManager!!.addCall(
+ mCallsManager.addCall(
attributes,
callObject.mOnAnswerLambda,
callObject.mOnDisconnectLambda,
callObject.mOnSetActiveLambda,
callObject.mOnSetInActiveLambda,
) {
+ postNotification(attributes, callObject)
mPreCallEndpointAdapter.mSelectedCallEndpoint = null
// inject client control interface into the VoIP call object
callObject.setCallId(getCallId().toString())
callObject.setCallControl(this)
+ launch {
+ mNotificationActionInfoFlow.collect {
+ if (it.id == callObject.mNotificationId) {
+ if (it.isAnswer) {
+ answer(CallAttributesCompat.CALL_TYPE_AUDIO_CALL)
+ } else {
+ disconnect(DisconnectCause(DisconnectCause.LOCAL))
+ }
+ handleUpdateToNotification(it, attributes, callObject)
+ }
+ }
+ }
// Collect updates
launch { currentCallEndpoint.collect { callObject.onCallEndpointChanged(it) } }
@@ -241,6 +295,23 @@
}
}
+ private fun handleUpdateToNotification(
+ it: NotificationActionInfo,
+ attributes: CallAttributesCompat,
+ callObject: VoipCall
+ ) {
+ if (it.isAnswer) {
+ NotificationsUtilities.updateNotificationToOngoing(
+ mContext,
+ callObject.mNotificationId,
+ NOTIFICATION_CHANNEL_ID,
+ attributes.displayName.toString()
+ )
+ } else {
+ NotificationsUtilities.clearNotification(mContext, callObject.mNotificationId)
+ }
+ }
+
@OptIn(ExperimentalAppActions::class)
private suspend fun addCallWithExtensions(
attributes: CallAttributesCompat,
@@ -248,7 +319,7 @@
isRaiseHandEnabled: Boolean = false,
isKickParticipantEnabled: Boolean = false
) {
- mCallsManager!!.addCallWithExtensions(
+ mCallsManager.addCallWithExtensions(
attributes,
callObject.mOnAnswerLambda,
callObject.mOnDisconnectLambda,
@@ -282,6 +353,7 @@
}
}
onCall {
+ postNotification(attributes, callObject)
mPreCallEndpointAdapter.mSelectedCallEndpoint = null
// inject client control interface into the VoIP call object
callObject.setCallId(getCallId().toString())
@@ -293,7 +365,18 @@
)
)
addCallRow(callObject)
-
+ launch {
+ mNotificationActionInfoFlow.collect {
+ if (it.id == callObject.mNotificationId) {
+ if (it.isAnswer) {
+ answer(CallAttributesCompat.CALL_TYPE_AUDIO_CALL)
+ } else {
+ disconnect(DisconnectCause(DisconnectCause.LOCAL))
+ }
+ handleUpdateToNotification(it, attributes, callObject)
+ }
+ }
+ }
// Collect updates
participants.participants
.onEach {
@@ -329,7 +412,7 @@
}
private fun fetchPreCallEndpoints(cancelFlowButton: Button) {
- val endpointsFlow = mCallsManager!!.getAvailableStartingCallEndpoints()
+ val endpointsFlow = mCallsManager.getAvailableStartingCallEndpoints()
CoroutineScope(Dispatchers.Default).launch {
launch {
val endpointsCoroutineScope = this
@@ -351,12 +434,24 @@
}
}
+ private fun postNotification(attributes: CallAttributesCompat, voipCall: VoipCall) {
+ val notification =
+ NotificationsUtilities.createInitialCallStyleNotification(
+ mContext,
+ voipCall.mNotificationId,
+ NOTIFICATION_CHANNEL_ID,
+ attributes.displayName.toString(),
+ attributes.direction == DIRECTION_OUTGOING
+ )
+ mNotificationManager.notify(voipCall.mNotificationId, notification)
+ }
+
private fun logException(e: Exception, prefix: String) {
Log.i(TAG, "$prefix: e=[$e], e.msg=[${e.message}], e.stack:${e.printStackTrace()}")
}
private fun addCallRow(callObject: VoipCall) {
- mCallObjects.add(CallRow(++mCallCount, callObject))
+ mCallObjects.add(CallRow(++mCurrentCallCount, callObject))
callObject.setCallAdapter(mAdapter)
updateCallList()
}
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/Constants.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/Constants.kt
new file mode 100644
index 0000000..3086853
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/Constants.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 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
+ *
+ * http://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 androidx.core.telecom.test
+
+import android.net.Uri
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallAttributesCompat
+
+@RequiresApi(34)
+class Constants {
+ companion object {
+ const val APP_SCHEME = "MyCustomScheme:"
+ const val TEST_NUMBER = "6506958985"
+ const val ALL_CALL_CAPABILITIES =
+ (CallAttributesCompat.SUPPORTS_SET_INACTIVE or
+ CallAttributesCompat.SUPPORTS_STREAM or
+ CallAttributesCompat.SUPPORTS_TRANSFER)
+
+ // outgoing attributes constants
+ const val OUTGOING_NAME = "Darth Maul"
+ val OUTGOING_URI: Uri = Uri.parse(APP_SCHEME + TEST_NUMBER)
+
+ // incoming attributes constants
+ const val INCOMING_NAME = "Sundar Pichai"
+ val INCOMING_URI: Uri = Uri.parse(APP_SCHEME + TEST_NUMBER)
+ }
+}
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/NotificationsUtilities.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/NotificationsUtilities.kt
new file mode 100644
index 0000000..1d9509d
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/NotificationsUtilities.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.core.telecom.test
+
+import android.R
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Person
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.util.ExperimentalAppActions
+
+@ExperimentalAppActions
+@RequiresApi(Build.VERSION_CODES.S)
+class NotificationsUtilities {
+ companion object {
+ val TAG: String = NotificationsUtilities::class.java.getSimpleName()
+ val NOTIFICATION_CHANNEL_ID = "CoreTelecomTestVoipApp"
+ val CHANNEL_NAME = "Core Telecom Test Voip App Channel"
+ val CHANNEL_DESCRIPTION =
+ "Core Telecom Test Voip calls will post call-style" + " notifications to this channel"
+ val CALL_STYLE_INITIAL_TEXT: String = "New Call"
+ val CALL_STYLE_ONGOING_TEXT: String = "Ongoing Call"
+ val CALL_STYLE_TITLE: String = "Core-Telecom Test VoIP App"
+ val NOTIFICATION_ACTION_ANSWER = "androidx.core.telecom.test.ANSWER"
+ val NOTIFICATION_ACTION_DECLINE = "androidx.core.telecom.test.DECLINE"
+ val NOTIFICATION_ID = "androidx.core.telecom.test.ID"
+ val IS_ANSWER_ACTION = "androidx.core.telecom.test.IS_ANSWER_ACTION"
+
+ fun createInitialCallStyleNotification(
+ context: Context,
+ uniqueId: Int,
+ channelId: String?,
+ callerName: String?,
+ isOutgoing: Boolean
+ ): Notification {
+ val fullScreenIntent = getDeclinePendingIntent(context, uniqueId)
+ val person: Person = Person.Builder().setName(callerName).setImportant(true).build()
+ return Notification.Builder(context, channelId)
+ .setContentText(CALL_STYLE_INITIAL_TEXT)
+ .setContentTitle(CALL_STYLE_TITLE)
+ .setSmallIcon(R.drawable.sym_def_app_icon)
+ .setStyle(getCallStyle(context, isOutgoing, person, fullScreenIntent, uniqueId))
+ .setFullScreenIntent(fullScreenIntent, true)
+ .setOngoing(isOutgoing)
+ .build()
+ }
+
+ private fun getCallStyle(
+ c: Context,
+ isOutgoing: Boolean,
+ person: Person,
+ fullScreenIntent: PendingIntent,
+ notificationId: Int,
+ ): Notification.CallStyle {
+ return if (isOutgoing) {
+ Notification.CallStyle.forOngoingCall(person, fullScreenIntent)
+ } else {
+ Notification.CallStyle.forIncomingCall(
+ person,
+ getDeclinePendingIntent(c, notificationId),
+ getAnswerPendingIntent(c, notificationId)
+ )
+ }
+ }
+
+ fun updateNotificationToOngoing(
+ context: Context,
+ notificationId: Int,
+ channelId: String?,
+ callerName: String?,
+ ) {
+ val endCallAction = getDeclinePendingIntent(context, notificationId)
+ val callStyleNotification =
+ Notification.Builder(context, channelId)
+ .setContentText(CALL_STYLE_ONGOING_TEXT)
+ .setContentTitle(CALL_STYLE_TITLE)
+ .setSmallIcon(R.drawable.sym_def_app_icon)
+ .setStyle(
+ Notification.CallStyle.forOngoingCall(
+ Person.Builder().setName(callerName).setImportant(true).build(),
+ endCallAction
+ )
+ )
+ .setFullScreenIntent(endCallAction, true)
+ .setOngoing(true)
+ .build()
+
+ val notificationManager = context.getSystemService(NotificationManager::class.java)
+ notificationManager.notify(notificationId, callStyleNotification)
+ }
+
+ private fun getDeclinePendingIntent(context: Context, notificationId: Int): PendingIntent {
+ return PendingIntent.getActivity(
+ context,
+ notificationId,
+ getDeclineIntent(context, notificationId),
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ }
+
+ private fun getDeclineIntent(c: Context, notificationId: Int): Intent {
+ val declineIntent =
+ Intent(c, CallingMainActivity::class.java).apply {
+ action = NOTIFICATION_ACTION_DECLINE
+ putExtra(NOTIFICATION_ID, notificationId)
+ putExtra(IS_ANSWER_ACTION, false)
+ }
+ declineIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ return declineIntent
+ }
+
+ private fun getAnswerPendingIntent(c: Context, notificationId: Int): PendingIntent {
+ return PendingIntent.getActivity(
+ c,
+ notificationId,
+ getAnswerIntent(c, notificationId),
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ }
+
+ private fun getAnswerIntent(c: Context, notificationId: Int): Intent {
+ val answerIntent =
+ Intent(c, CallingMainActivity::class.java).apply {
+ action = NOTIFICATION_ACTION_ANSWER
+ putExtra(NOTIFICATION_ID, notificationId)
+ putExtra(IS_ANSWER_ACTION, true)
+ }
+ answerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ return answerIntent
+ }
+
+ fun clearNotification(c: Context, notificationId: Int) {
+ val notificationManager = c.getSystemService(NotificationManager::class.java)
+ notificationManager?.cancel(notificationId)
+ }
+
+ fun initNotificationChannel(c: Context) {
+ val importance = NotificationManager.IMPORTANCE_DEFAULT
+ val channel =
+ NotificationChannel(NOTIFICATION_CHANNEL_ID, CHANNEL_NAME, importance).apply {
+ description = CHANNEL_DESCRIPTION
+ }
+ val nm = c.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ nm.createNotificationChannel(channel)
+ }
+
+ fun deleteNotificationChannel(c: Context) {
+ val notificationManager = c.getSystemService(NotificationManager::class.java)
+ try {
+ notificationManager.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID)
+ } catch (e: Exception) {
+ Log.i(
+ TAG,
+ String.format(
+ "notificationManager: hit exception=[%s] while deleting the" +
+ " call channel with id=[%s]",
+ e,
+ NOTIFICATION_CHANNEL_ID
+ )
+ )
+ }
+ }
+ }
+}
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/Utilities.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/Utilities.kt
deleted file mode 100644
index cb6123b..0000000
--- a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/Utilities.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx.core.telecom.test
-
-import android.net.Uri
-import androidx.annotation.RequiresApi
-import androidx.core.telecom.CallAttributesCompat
-
-@RequiresApi(34)
-class Utilities {
- companion object {
- const val APP_SCHEME = "MyCustomScheme:"
- const val TEST_NUMBER = "6506958985"
- const val ALL_CALL_CAPABILITIES =
- (CallAttributesCompat.SUPPORTS_SET_INACTIVE or
- CallAttributesCompat.SUPPORTS_STREAM or
- CallAttributesCompat.SUPPORTS_TRANSFER)
-
- // outgoing attributes constants
- const val OUTGOING_NAME = "Darth Maul"
- val OUTGOING_URI: Uri = Uri.parse(APP_SCHEME + TEST_NUMBER)
-
- // incoming attributes constants
- const val INCOMING_NAME = "Sundar Pichai"
- val INCOMING_URI: Uri = Uri.parse(APP_SCHEME + TEST_NUMBER)
- }
-}
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/VoipCall.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/VoipCall.kt
index a57edc5..25bb650 100644
--- a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/VoipCall.kt
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/VoipCall.kt
@@ -16,12 +16,15 @@
package androidx.core.telecom.test
+import android.content.Context
import android.telecom.DisconnectCause
import android.util.Log
import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallAttributesCompat
import androidx.core.telecom.CallControlScope
import androidx.core.telecom.CallEndpointCompat
import androidx.core.telecom.extensions.LocalCallSilenceExtension
+import androidx.core.telecom.test.NotificationsUtilities.Companion.NOTIFICATION_CHANNEL_ID
import androidx.core.telecom.util.ExperimentalAppActions
import kotlin.coroutines.coroutineContext
import kotlinx.coroutines.CoroutineScope
@@ -29,9 +32,9 @@
@ExperimentalAppActions
@RequiresApi(34)
-class VoipCall {
+class VoipCall(val context: Context, val attributes: CallAttributesCompat) {
private val TAG = VoipCall::class.simpleName
-
+ var mNotificationId: Int = -1
var mAdapter: CallListAdapter? = null
var mCallControl: CallControlScope? = null
var mParticipantControl: ParticipantControl? = null
@@ -40,6 +43,7 @@
var mIsMuted = false
var mTelecomCallId: String = ""
var mIsLocallySilence: Boolean = false
+ var hasUpdatedToOngoing = false
var mLocalCallSilenceExtension: LocalCallSilenceExtension? = null
val mOnSetActiveLambda: suspend () -> Unit = {
@@ -55,10 +59,12 @@
val mOnAnswerLambda: suspend (type: Int) -> Unit = {
Log.i(TAG, "onAnswer: callType=[$it]")
mAdapter?.updateCallState(mTelecomCallId, "Answered")
+ updateNotificationToOngoing()
}
val mOnDisconnectLambda: suspend (cause: DisconnectCause) -> Unit = {
Log.i(TAG, "onDisconnect: disconnectCause=[$it]")
+ NotificationsUtilities.clearNotification(context, mNotificationId)
mAdapter?.updateCallState(mTelecomCallId, "Disconnected")
}
@@ -70,6 +76,27 @@
mParticipantControl = participantControl
}
+ fun setNotificationId(id: Int) {
+ mNotificationId = id
+ }
+
+ fun clearNotification() {
+ Log.i(TAG, "clearNotification with id=[$mNotificationId]")
+ NotificationsUtilities.clearNotification(context, mNotificationId)
+ }
+
+ fun updateNotificationToOngoing() {
+ if (!hasUpdatedToOngoing) {
+ NotificationsUtilities.updateNotificationToOngoing(
+ context = context,
+ notificationId = mNotificationId,
+ channelId = NOTIFICATION_CHANNEL_ID,
+ callerName = attributes.displayName.toString()
+ )
+ }
+ hasUpdatedToOngoing = true
+ }
+
@OptIn(ExperimentalAppActions::class)
suspend fun toggleLocalCallSilence() {
CoroutineScope(coroutineContext).launch {
diff --git a/core/core-telecom/integration-tests/testicsapp/build.gradle b/core/core-telecom/integration-tests/testicsapp/build.gradle
index c3ce464..27427c3 100644
--- a/core/core-telecom/integration-tests/testicsapp/build.gradle
+++ b/core/core-telecom/integration-tests/testicsapp/build.gradle
@@ -46,7 +46,7 @@
//@Serialize
implementation(libs.kotlinSerializationCore)
// Test package
- implementation project(":core:core-telecom")
+ implementation(project(":core:core-telecom"))
// Compose
implementation("androidx.activity:activity-compose:1.9.1")
// Themes and Dynamic coloring
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt b/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
index afcd9bc..94ecba9 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
@@ -418,7 +418,6 @@
* @param onEvent Incoming {@link CallEvents} from an InCallService implementation
* @see addCall For more documentation on the operations/parameters of this class
*/
- @Suppress("ClassVerificationFailure")
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)
internal suspend fun addCall(
callAttributes: CallAttributesCompat,
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSession.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSession.kt
index 14c8903..a96b470 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSession.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSession.kt
@@ -49,7 +49,6 @@
import kotlinx.coroutines.withTimeout
@RequiresApi(34)
-@Suppress("ClassVerificationFailure")
internal class CallSession(
val coroutineContext: CoroutineContext,
val attributes: CallAttributesCompat,
diff --git a/core/core/build.gradle b/core/core/build.gradle
index 38a9b3f..4737ed5 100644
--- a/core/core/build.gradle
+++ b/core/core/build.gradle
@@ -15,6 +15,7 @@
}
dependencies {
+ api(libs.jspecify)
// Atomically versioned.
constraints {
implementation(project(":core:core-ktx"))
@@ -108,6 +109,4 @@
"features."
failOnDeprecationWarnings = false
samples(project(":core:core:core-samples"))
- // TODO: b/326456246
- optOutJSpecify = true
}
diff --git a/core/core/src/androidTest/java/android/support/v4/testutils/TestUtils.java b/core/core/src/androidTest/java/android/support/v4/testutils/TestUtils.java
index 1b3a83a..53a27de 100644
--- a/core/core/src/androidTest/java/android/support/v4/testutils/TestUtils.java
+++ b/core/core/src/androidTest/java/android/support/v4/testutils/TestUtils.java
@@ -27,7 +27,8 @@
import android.graphics.drawable.Drawable;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
+
+import org.jspecify.annotations.NonNull;
public class TestUtils {
/**
diff --git a/core/core/src/androidTest/java/android/support/v4/testutils/TextViewActions.java b/core/core/src/androidTest/java/android/support/v4/testutils/TextViewActions.java
index e425594..a38c9e4c 100644
--- a/core/core/src/androidTest/java/android/support/v4/testutils/TextViewActions.java
+++ b/core/core/src/androidTest/java/android/support/v4/testutils/TextViewActions.java
@@ -23,7 +23,6 @@
import android.widget.TextView;
import androidx.annotation.DrawableRes;
-import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.StyleRes;
import androidx.core.widget.TextViewCompat;
@@ -31,6 +30,7 @@
import androidx.test.espresso.ViewAction;
import org.hamcrest.Matcher;
+import org.jspecify.annotations.Nullable;
public class TextViewActions {
/**
diff --git a/core/core/src/androidTest/java/androidx/core/app/GrammaticalInfectionActivity.java b/core/core/src/androidTest/java/androidx/core/app/GrammaticalInfectionActivity.java
index 07e58d1..82204ca 100644
--- a/core/core/src/androidTest/java/androidx/core/app/GrammaticalInfectionActivity.java
+++ b/core/core/src/androidTest/java/androidx/core/app/GrammaticalInfectionActivity.java
@@ -21,7 +21,7 @@
import android.os.Bundle;
import android.view.WindowManager;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
diff --git a/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java b/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
index 007491b..5523b58 100644
--- a/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
@@ -30,12 +30,12 @@
import android.os.Parcelable;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
+import org.jspecify.annotations.NonNull;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
diff --git a/core/core/src/androidTest/java/androidx/core/app/NotificationChannelCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/NotificationChannelCompatTest.java
index e4087d5..7cedf48 100644
--- a/core/core/src/androidTest/java/androidx/core/app/NotificationChannelCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/NotificationChannelCompatTest.java
@@ -40,7 +40,6 @@
import java.util.Collection;
import java.util.Objects;
-
@RunWith(AndroidJUnit4.class)
@SmallTest
public class NotificationChannelCompatTest {
diff --git a/core/core/src/androidTest/java/androidx/core/app/NotificationChannelGroupCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/NotificationChannelGroupCompatTest.java
index 95ad4a2..ff98d88 100644
--- a/core/core/src/androidTest/java/androidx/core/app/NotificationChannelGroupCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/NotificationChannelGroupCompatTest.java
@@ -22,12 +22,12 @@
import android.app.NotificationChannelGroup;
import android.os.Build;
-import androidx.annotation.NonNull;
import androidx.core.app.NotificationChannelGroupCompat.Builder;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
+import org.jspecify.annotations.NonNull;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -35,7 +35,6 @@
import javax.annotation.Nullable;
-
@RunWith(AndroidJUnit4.class)
@SmallTest
public class NotificationChannelGroupCompatTest {
diff --git a/core/core/src/androidTest/java/androidx/core/app/NotificationManagerCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/NotificationManagerCompatTest.java
index a264df7..e3a99a8 100644
--- a/core/core/src/androidTest/java/androidx/core/app/NotificationManagerCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/NotificationManagerCompatTest.java
@@ -26,14 +26,12 @@
import static androidx.core.app.NotificationManagerCompat.IMPORTANCE_MAX;
import static androidx.core.app.NotificationManagerCompat.IMPORTANCE_MIN;
-import static org.mockito.Mockito.spy;
-
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -64,7 +62,6 @@
import java.util.List;
import java.util.UUID;
-
@RunWith(AndroidJUnit4.class)
@SmallTest
public class NotificationManagerCompatTest {
diff --git a/core/core/src/androidTest/java/androidx/core/app/RemoteActionCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/RemoteActionCompatTest.java
index 488dcdf..a9e6a506 100644
--- a/core/core/src/androidTest/java/androidx/core/app/RemoteActionCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/RemoteActionCompatTest.java
@@ -33,7 +33,6 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-
@RunWith(AndroidJUnit4.class)
@SmallTest
public class RemoteActionCompatTest extends BaseInstrumentationTestCase<TestActivity> {
diff --git a/core/core/src/androidTest/java/androidx/core/content/res/ResourcesCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/res/ResourcesCompatTest.java
index e877a26..0a67b74 100644
--- a/core/core/src/androidTest/java/androidx/core/content/res/ResourcesCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/res/ResourcesCompatTest.java
@@ -36,7 +36,6 @@
import android.support.v4.testutils.TestUtils;
import android.util.DisplayMetrics;
-import androidx.annotation.NonNull;
import androidx.core.graphics.TypefaceCompat;
import androidx.core.provider.FontsContractCompat;
import androidx.core.provider.MockFontProvider;
@@ -47,6 +46,7 @@
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import org.jspecify.annotations.NonNull;
import org.junit.Before;
import org.junit.Test;
diff --git a/core/core/src/androidTest/java/androidx/core/graphics/PaintTest.java b/core/core/src/androidTest/java/androidx/core/graphics/PaintTest.java
index acbb9cf..3c1d464 100644
--- a/core/core/src/androidTest/java/androidx/core/graphics/PaintTest.java
+++ b/core/core/src/androidTest/java/androidx/core/graphics/PaintTest.java
@@ -27,11 +27,11 @@
import android.graphics.PorterDuffXfermode;
import android.graphics.Xfermode;
-import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
+import org.jspecify.annotations.Nullable;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -93,7 +93,7 @@
* equivalent PorterDuff.Mode for the BlendMode
*/
private void verifyPorterDuffMatchesCompat(@Nullable BlendModeCompat compat,
- @Nullable PorterDuff.Mode mode) {
+ PorterDuff.@Nullable Mode mode) {
Paint p = new Paint();
boolean result = PaintCompat.setBlendMode(p, compat);
if (compat != null && mode == null) {
diff --git a/core/core/src/androidTest/java/androidx/core/graphics/PaintTestApi29.java b/core/core/src/androidTest/java/androidx/core/graphics/PaintTestApi29.java
index df91c96e..1565f4b 100644
--- a/core/core/src/androidTest/java/androidx/core/graphics/PaintTestApi29.java
+++ b/core/core/src/androidTest/java/androidx/core/graphics/PaintTestApi29.java
@@ -23,11 +23,11 @@
import android.graphics.BlendMode;
import android.graphics.Paint;
-import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
+import org.jspecify.annotations.NonNull;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/core/core/src/androidTest/java/androidx/core/graphics/TypefaceCompatTest.java b/core/core/src/androidTest/java/androidx/core/graphics/TypefaceCompatTest.java
index eb5eb6b..c5e054f 100644
--- a/core/core/src/androidTest/java/androidx/core/graphics/TypefaceCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/graphics/TypefaceCompatTest.java
@@ -34,7 +34,6 @@
import android.graphics.Typeface;
import android.os.Build;
-import androidx.annotation.NonNull;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.core.content.res.FontResourcesParserCompat;
@@ -51,6 +50,7 @@
import com.google.common.truth.Truth;
+import org.jspecify.annotations.NonNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
diff --git a/core/core/src/androidTest/java/androidx/core/os/TraceCompatTest.java b/core/core/src/androidTest/java/androidx/core/os/TraceCompatTest.java
index c62c8b5..528be61 100644
--- a/core/core/src/androidTest/java/androidx/core/os/TraceCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/os/TraceCompatTest.java
@@ -26,14 +26,14 @@
import android.os.Build;
import android.os.ParcelFileDescriptor;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
diff --git a/core/core/src/androidTest/java/androidx/core/provider/FontsContractCompatTest.java b/core/core/src/androidTest/java/androidx/core/provider/FontsContractCompatTest.java
index 3b65ec1..57b810f 100644
--- a/core/core/src/androidTest/java/androidx/core/provider/FontsContractCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/provider/FontsContractCompatTest.java
@@ -44,13 +44,13 @@
import android.os.Looper;
import android.util.Base64;
-import androidx.annotation.NonNull;
import androidx.core.provider.FontsContractCompat.FontFamilyResult;
import androidx.core.provider.FontsContractCompat.FontInfo;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import org.jspecify.annotations.NonNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
diff --git a/core/core/src/androidTest/java/androidx/core/text/method/LinkMovementMethodCompatTest.java b/core/core/src/androidTest/java/androidx/core/text/method/LinkMovementMethodCompatTest.java
index 7eb6b39..131e129 100644
--- a/core/core/src/androidTest/java/androidx/core/text/method/LinkMovementMethodCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/text/method/LinkMovementMethodCompatTest.java
@@ -38,12 +38,12 @@
import android.view.inputmethod.InputConnection;
import android.widget.TextView;
-import androidx.annotation.NonNull;
import androidx.core.app.TestActivity;
import androidx.test.annotation.UiThreadTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
+import org.jspecify.annotations.NonNull;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
diff --git a/core/core/src/androidTest/java/androidx/core/util/AtomicFileTest.java b/core/core/src/androidTest/java/androidx/core/util/AtomicFileTest.java
index 534b4c5..5b01337 100644
--- a/core/core/src/androidTest/java/androidx/core/util/AtomicFileTest.java
+++ b/core/core/src/androidTest/java/androidx/core/util/AtomicFileTest.java
@@ -22,11 +22,11 @@
import android.app.Instrumentation;
import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -66,12 +66,9 @@
private static final byte[] NEW_BYTES = "new".getBytes(UTF_8);
private static final byte[] LEGACY_BACKUP_BYTES = "bak".getBytes(UTF_8);
- @Nullable
- public String[] mExistingFileNames;
- @Nullable
- public WriteAction mWriteAction;
- @Nullable
- public byte[] mExpectedBytes;
+ public String @Nullable [] mExistingFileNames;
+ public @Nullable WriteAction mWriteAction;
+ public byte @Nullable [] mExpectedBytes;
private final Instrumentation mInstrumentation =
InstrumentationRegistry.getInstrumentation();
@@ -273,7 +270,7 @@
}
}
- private static void writeBytes(@NonNull File file, @NonNull byte[] bytes) throws IOException {
+ private static void writeBytes(@NonNull File file, byte @NonNull [] bytes) throws IOException {
try (FileOutputStream outputStream = new FileOutputStream(file)) {
outputStream.write(bytes);
}
@@ -319,15 +316,12 @@
// JUnit on API 17 somehow turns null parameters into the string "null". Wrapping the parameters
// inside a class solves this problem.
private static class Parameters {
- @Nullable
- public String[] existingFileNames;
- @Nullable
- public WriteAction writeAction;
- @Nullable
- public byte[] expectedBytes;
+ public String @Nullable [] existingFileNames;
+ public @Nullable WriteAction writeAction;
+ public byte @Nullable [] expectedBytes;
- Parameters(@Nullable String[] existingFileNames, @Nullable WriteAction writeAction,
- @Nullable byte[] expectedBytes) {
+ Parameters(String @Nullable [] existingFileNames, @Nullable WriteAction writeAction,
+ byte @Nullable [] expectedBytes) {
this.existingFileNames = existingFileNames;
this.writeAction = writeAction;
this.expectedBytes = expectedBytes;
diff --git a/core/core/src/androidTest/java/androidx/core/view/AccessibilityDelegateCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/AccessibilityDelegateCompatTest.java
index 7dd5bc8..6d2d5f9 100644
--- a/core/core/src/androidTest/java/androidx/core/view/AccessibilityDelegateCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/AccessibilityDelegateCompatTest.java
@@ -48,8 +48,6 @@
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat;
@@ -63,6 +61,8 @@
import com.google.common.truth.Truth;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/core/core/src/androidTest/java/androidx/core/view/DragStartHelperTest.java b/core/core/src/androidTest/java/androidx/core/view/DragStartHelperTest.java
index 0dfb788..10208b6 100644
--- a/core/core/src/androidTest/java/androidx/core/view/DragStartHelperTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/DragStartHelperTest.java
@@ -34,7 +34,6 @@
import android.view.View;
import android.view.ViewConfiguration;
-import androidx.annotation.NonNull;
import androidx.core.test.R;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
@@ -42,6 +41,7 @@
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
+import org.jspecify.annotations.NonNull;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
@@ -64,16 +64,14 @@
boolean onDragStart(View view, DragStartHelper helper, Point touchPosition);
}
- @NonNull
- private DragStartListener createListener(boolean returnValue) {
+ private @NonNull DragStartListener createListener(boolean returnValue) {
final DragStartListener listener = mock(DragStartListener.class);
when(listener.onDragStart(any(View.class), any(DragStartHelper.class), any(Point.class)))
.thenReturn(returnValue);
return listener;
}
- @NonNull
- private DragStartHelper createDragStartHelper(final DragStartListener listener) {
+ private @NonNull DragStartHelper createDragStartHelper(final DragStartListener listener) {
return new DragStartHelper(mDragSource, new DragStartHelper.OnDragStartListener() {
@Override
public boolean onDragStart(@NonNull View v, @NonNull DragStartHelper helper) {
diff --git a/core/core/src/androidTest/java/androidx/core/view/NestedScrollingChildHelperTest.java b/core/core/src/androidTest/java/androidx/core/view/NestedScrollingChildHelperTest.java
index 849ee9c..c2bd340 100644
--- a/core/core/src/androidTest/java/androidx/core/view/NestedScrollingChildHelperTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/NestedScrollingChildHelperTest.java
@@ -28,13 +28,13 @@
import android.view.ViewGroup;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.widget.NestedScrollView;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -190,14 +190,14 @@
}
@Override
- public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
- int type) {
+ public void onNestedPreScroll(@NonNull View target, int dx, int dy,
+ int @NonNull [] consumed, int type) {
}
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed, int type, @Nullable int[] consumed) {
+ int dxUnconsumed, int dyUnconsumed, int type, int @Nullable [] consumed) {
}
}
diff --git a/core/core/src/androidTest/java/androidx/core/view/NestedScrollingHelperIntegrationTest.java b/core/core/src/androidTest/java/androidx/core/view/NestedScrollingHelperIntegrationTest.java
index ba0f2ba..05ff986 100644
--- a/core/core/src/androidTest/java/androidx/core/view/NestedScrollingHelperIntegrationTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/NestedScrollingHelperIntegrationTest.java
@@ -23,12 +23,12 @@
import android.view.ViewGroup;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -151,7 +151,7 @@
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy,
- @NonNull int[] consumed) {
+ int @NonNull [] consumed) {
dispatchNestedPreScroll(dx, dy, consumed, null);
}
@@ -198,14 +198,14 @@
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
- int dyUnconsumed, @Nullable int[] offsetInWindow) {
+ int dyUnconsumed, int @Nullable [] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
- public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
- @Nullable int[] offsetInWindow) {
+ public boolean dispatchNestedPreScroll(int dx, int dy, int @Nullable [] consumed,
+ int @Nullable [] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed,
offsetInWindow);
}
@@ -253,8 +253,8 @@
}
@Override
- public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
- int type) {
+ public void onNestedPreScroll(@NonNull View target, int dx, int dy,
+ int @NonNull [] consumed, int type) {
dispatchNestedPreScroll(dx, dy, consumed, null);
}
@@ -275,14 +275,14 @@
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
- int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
+ int dyUnconsumed, int @Nullable [] offsetInWindow, int type) {
return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow, type);
}
@Override
- public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
- @Nullable int[] offsetInWindow, int type) {
+ public boolean dispatchNestedPreScroll(int dx, int dy, int @Nullable [] consumed,
+ int @Nullable [] offsetInWindow, int type) {
return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed,
offsetInWindow, type);
}
@@ -297,15 +297,15 @@
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed, int type, @NonNull int[] consumed) {
+ int dxUnconsumed, int dyUnconsumed, int type, int @NonNull [] consumed) {
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed,
dyUnconsumed, null, type, consumed);
}
@Override
public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
- int dyUnconsumed, @Nullable int[] offsetInWindow, int type,
- @NonNull int[] consumed) {
+ int dyUnconsumed, int @Nullable [] offsetInWindow, int type,
+ int @NonNull [] consumed) {
mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed);
}
diff --git a/core/core/src/androidTest/java/androidx/core/view/PointerIconCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/PointerIconCompatTest.java
index c3df6d4..fd0202e 100644
--- a/core/core/src/androidTest/java/androidx/core/view/PointerIconCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/PointerIconCompatTest.java
@@ -36,7 +36,6 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-
@RunWith(AndroidJUnit4.class)
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
diff --git a/core/core/src/androidTest/java/androidx/core/view/ViewCompatReceiveContentTest.java b/core/core/src/androidTest/java/androidx/core/view/ViewCompatReceiveContentTest.java
index b3d7604..1b05c2c 100644
--- a/core/core/src/androidTest/java/androidx/core/view/ViewCompatReceiveContentTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/ViewCompatReceiveContentTest.java
@@ -24,14 +24,14 @@
import android.os.Build;
import android.view.View;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.R;
import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.rule.ActivityTestRule;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -107,8 +107,7 @@
} catch (IllegalArgumentException expected) { }
}
- @Nullable
- private static OnReceiveContentListener getListener(@NonNull View view) {
+ private static @Nullable OnReceiveContentListener getListener(@NonNull View view) {
return (OnReceiveContentListener) view.getTag(R.id.tag_on_receive_content_listener);
}
}
diff --git a/core/core/src/androidTest/java/androidx/core/view/ViewCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/ViewCompatTest.java
index 8b6fd8e..f3a29d8 100644
--- a/core/core/src/androidTest/java/androidx/core/view/ViewCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/ViewCompatTest.java
@@ -68,8 +68,6 @@
import android.view.contentcapture.ContentCaptureSession;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.graphics.Insets;
import androidx.core.test.R;
import androidx.core.view.autofill.AutofillIdCompat;
@@ -80,6 +78,8 @@
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -621,7 +621,7 @@
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
- int dyUnconsumed, @Nullable int[] offsetInWindow) {
+ int dyUnconsumed, int @Nullable [] offsetInWindow) {
return true;
}
}
@@ -635,7 +635,7 @@
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
- int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
+ int dyUnconsumed, int @Nullable [] offsetInWindow, int type) {
return true;
}
}
@@ -649,8 +649,8 @@
@Override
public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
- int dyUnconsumed, @Nullable int[] offsetInWindow, int type,
- @NonNull int[] consumed) {
+ int dyUnconsumed, int @Nullable [] offsetInWindow, int type,
+ int @NonNull [] consumed) {
}
}
}
diff --git a/core/core/src/androidTest/java/androidx/core/view/ViewConfigurationCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/ViewConfigurationCompatTest.java
index d194ec9..f0387d6 100644
--- a/core/core/src/androidTest/java/androidx/core/view/ViewConfigurationCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/ViewConfigurationCompatTest.java
@@ -33,12 +33,12 @@
import android.view.InputDevice;
import android.view.ViewConfiguration;
-import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
+import org.jspecify.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -330,8 +330,7 @@
mContext, vc, inputDeviceId, axis, source));
}
- @Nullable
- private InputDevice findInputDevice(int source) {
+ private @Nullable InputDevice findInputDevice(int source) {
InputManager inputManager =
(InputManager) mContext.getSystemService(Context.INPUT_SERVICE);
int[] deviceIds = inputManager.getInputDeviceIds();
diff --git a/core/core/src/androidTest/java/androidx/core/view/ViewGroupCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/ViewGroupCompatTest.java
index 4009846..22fdbd4 100644
--- a/core/core/src/androidTest/java/androidx/core/view/ViewGroupCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/ViewGroupCompatTest.java
@@ -34,20 +34,19 @@
import android.view.WindowInsets;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
import androidx.core.graphics.Insets;
import androidx.core.test.R;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import org.jspecify.annotations.NonNull;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
-
@RunWith(AndroidJUnit4.class)
@MediumTest
public class ViewGroupCompatTest extends BaseInstrumentationTestCase<ViewCompatActivity> {
@@ -136,9 +135,9 @@
// View.OnApplyWindowInsetsListener set by ViewGroupCompat#installCompatInsetsDispatch
ViewCompat.setWindowInsetsAnimationCallback(mViewGroup,
new WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
- @NonNull
@Override
- public WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets,
+ public @NonNull WindowInsetsCompat onProgress(
+ @NonNull WindowInsetsCompat insets,
@NonNull List<WindowInsetsAnimationCompat> runningAnimations) {
return insets;
}
diff --git a/core/core/src/androidTest/java/androidx/core/view/ViewParentCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/ViewParentCompatTest.java
index 0f0a738..f93fcb5 100644
--- a/core/core/src/androidTest/java/androidx/core/view/ViewParentCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/ViewParentCompatTest.java
@@ -24,18 +24,17 @@
import android.view.View;
import android.view.ViewParent;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SdkSuppress;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-
@RunWith(AndroidJUnit4.class)
@MediumTest
public class ViewParentCompatTest {
@@ -189,7 +188,7 @@
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed, int type, @Nullable int[] consumed) {
+ int dxUnconsumed, int dyUnconsumed, int type, int @Nullable [] consumed) {
}
}
}
diff --git a/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java
index 70bf03c..7a93e2d 100644
--- a/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java
@@ -33,16 +33,15 @@
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
-import androidx.annotation.Nullable;
import androidx.core.app.TestActivity;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SdkSuppress;
+import org.jspecify.annotations.Nullable;
import org.junit.Test;
import org.junit.runner.RunWith;
-
@RunWith(AndroidJUnit4.class)
@MediumTest
public class EditorInfoCompatTest extends BaseInstrumentationTestCase<TestActivity> {
diff --git a/core/core/src/androidTest/java/androidx/core/view/inputmethod/ImeBaseSplitTestActivity.java b/core/core/src/androidTest/java/androidx/core/view/inputmethod/ImeBaseSplitTestActivity.java
index 279d763..f4ae401 100644
--- a/core/core/src/androidTest/java/androidx/core/view/inputmethod/ImeBaseSplitTestActivity.java
+++ b/core/core/src/androidTest/java/androidx/core/view/inputmethod/ImeBaseSplitTestActivity.java
@@ -19,10 +19,11 @@
import android.app.Activity;
import android.os.Bundle;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.R;
+import org.jspecify.annotations.Nullable;
+
@RequiresApi(30)
public class ImeBaseSplitTestActivity extends Activity {
diff --git a/core/core/src/androidTest/java/androidx/core/view/inputmethod/ImeSecondarySplitTestActivity.java b/core/core/src/androidTest/java/androidx/core/view/inputmethod/ImeSecondarySplitTestActivity.java
index 1820e0e..90cd5b9 100644
--- a/core/core/src/androidTest/java/androidx/core/view/inputmethod/ImeSecondarySplitTestActivity.java
+++ b/core/core/src/androidTest/java/androidx/core/view/inputmethod/ImeSecondarySplitTestActivity.java
@@ -21,13 +21,14 @@
import android.widget.Button;
import android.widget.EditText;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.R;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
+import org.jspecify.annotations.Nullable;
+
@RequiresApi(30)
public class ImeSecondarySplitTestActivity extends Activity {
diff --git a/core/core/src/androidTest/java/androidx/core/view/inputmethod/ImeSecondarySplitViewCompatTestActivity.java b/core/core/src/androidTest/java/androidx/core/view/inputmethod/ImeSecondarySplitViewCompatTestActivity.java
index 5bcd1e286..9349ef6 100644
--- a/core/core/src/androidTest/java/androidx/core/view/inputmethod/ImeSecondarySplitViewCompatTestActivity.java
+++ b/core/core/src/androidTest/java/androidx/core/view/inputmethod/ImeSecondarySplitViewCompatTestActivity.java
@@ -21,14 +21,14 @@
import android.widget.Button;
import android.widget.EditText;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.R;
import androidx.core.view.ViewCompat;
-import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
+import org.jspecify.annotations.Nullable;
+
@RequiresApi(30)
public class ImeSecondarySplitViewCompatTestActivity extends Activity {
diff --git a/core/core/src/androidTest/java/androidx/core/view/inputmethod/InputConnectionCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/inputmethod/InputConnectionCompatTest.java
index db51dd9..fe64bce 100644
--- a/core/core/src/androidTest/java/androidx/core/view/inputmethod/InputConnectionCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/inputmethod/InputConnectionCompatTest.java
@@ -46,7 +46,6 @@
import java.util.Objects;
-
@RunWith(AndroidJUnit4.class)
@MediumTest
public class InputConnectionCompatTest {
diff --git a/core/core/src/androidTest/java/androidx/core/widget/EdgeEffectCompatTest.java b/core/core/src/androidTest/java/androidx/core/widget/EdgeEffectCompatTest.java
index 80537e2..d7c743c 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/EdgeEffectCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/EdgeEffectCompatTest.java
@@ -27,13 +27,13 @@
import android.view.View;
import android.widget.EdgeEffect;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.test.R;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/core/core/src/androidTest/java/androidx/core/widget/ListViewCompatTest.java b/core/core/src/androidTest/java/androidx/core/widget/ListViewCompatTest.java
index 0d4afc2..ad117be 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/ListViewCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/ListViewCompatTest.java
@@ -30,12 +30,12 @@
import android.widget.ListView;
import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.test.R;
import androidx.test.filters.MediumTest;
import androidx.test.rule.ActivityTestRule;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;
@@ -89,8 +89,8 @@
}, false);
}
- private void runOnMainAndLayoutSync(@NonNull final ActivityTestRule activityTestRule,
- @NonNull final View view, @Nullable final Runnable runner, final boolean forceLayout)
+ private void runOnMainAndLayoutSync(final @NonNull ActivityTestRule activityTestRule,
+ final @NonNull View view, final @Nullable Runnable runner, final boolean forceLayout)
throws Throwable {
final View rootView = view.getRootView();
diff --git a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingA11yScrollTest.java b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingA11yScrollTest.java
index 27ca404..5eb26a2 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingA11yScrollTest.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingA11yScrollTest.java
@@ -35,8 +35,6 @@
import android.view.ViewGroup;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.test.R;
import androidx.core.view.NestedScrollingParent3;
import androidx.core.view.ViewCompat;
@@ -46,6 +44,8 @@
import androidx.test.platform.app.InstrumentationRegistry;
import org.jetbrains.annotations.NotNull;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -255,14 +255,14 @@
}
@Override
- public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
- int type) {
+ public void onNestedPreScroll(@NonNull View target, int dx, int dy,
+ int @NonNull [] consumed, int type) {
}
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed, int type, @Nullable int[] consumed) {
+ int dxUnconsumed, int dyUnconsumed, int type, int @Nullable [] consumed) {
}
@Override
diff --git a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingChildTest.java b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingChildTest.java
index d01a587..0f85e13 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingChildTest.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingChildTest.java
@@ -36,8 +36,6 @@
import android.view.ViewConfiguration;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.view.NestedScrollingChild3;
import androidx.core.view.NestedScrollingParent3;
import androidx.core.view.ViewCompat;
@@ -49,6 +47,8 @@
import androidx.testutils.MotionEventData;
import androidx.testutils.SimpleGestureGeneratorKt;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -465,8 +465,8 @@
}
@Override
- public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
- int type) {
+ public void onNestedPreScroll(@NonNull View target, int dx, int dy,
+ int @NonNull [] consumed, int type) {
}
@@ -487,25 +487,25 @@
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
- int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
+ int dyUnconsumed, int @Nullable [] offsetInWindow, int type) {
return false;
}
@Override
- public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
- @Nullable int[] offsetInWindow, int type) {
+ public boolean dispatchNestedPreScroll(int dx, int dy, int @Nullable [] consumed,
+ int @Nullable [] offsetInWindow, int type) {
return false;
}
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed, int type, @NonNull int[] consumed) {
+ int dxUnconsumed, int dyUnconsumed, int type, int @NonNull [] consumed) {
}
@Override
public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
- int dyUnconsumed, @Nullable int[] offsetInWindow, int type,
- @NonNull int[] consumed) {
+ int dyUnconsumed, int @Nullable [] offsetInWindow, int type,
+ int @NonNull [] consumed) {
}
@Override
@@ -578,7 +578,7 @@
@Override
public void onNestedPreScroll(
- @NonNull View target, int dx, int dy, @NonNull int[] consumed) {
+ @NonNull View target, int dx, int dy, int @NonNull [] consumed) {
}
@@ -589,7 +589,7 @@
}
@Override
- public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
+ public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
return false;
}
diff --git a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingFlingTest.java b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingFlingTest.java
index aaab4b2..97a7a86 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingFlingTest.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingFlingTest.java
@@ -38,8 +38,6 @@
import android.view.ViewGroup;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.test.R;
import androidx.core.view.NestedScrollingParent3;
import androidx.core.view.ViewCompat;
@@ -50,6 +48,8 @@
import androidx.testutils.SimpleGestureGeneratorKt;
import org.jetbrains.annotations.NotNull;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -360,14 +360,14 @@
}
@Override
- public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
- int type) {
+ public void onNestedPreScroll(@NonNull View target, int dx, int dy,
+ int @NonNull [] consumed, int type) {
}
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed, int type, @Nullable int[] consumed) {
+ int dxUnconsumed, int dyUnconsumed, int type, int @Nullable [] consumed) {
}
@Override
diff --git a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingParent2Test.java b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingParent2Test.java
index 2358c5c..55d9bee 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingParent2Test.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingParent2Test.java
@@ -34,8 +34,6 @@
import android.view.ViewGroup;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.view.NestedScrollingChild2;
import androidx.core.view.NestedScrollingParent2;
import androidx.core.view.ViewCompat;
@@ -43,6 +41,8 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -565,8 +565,8 @@
}
@Override
- public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
- int type) {
+ public void onNestedPreScroll(@NonNull View target, int dx, int dy,
+ int @NonNull [] consumed, int type) {
}
@@ -587,13 +587,13 @@
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
- int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
+ int dyUnconsumed, int @Nullable [] offsetInWindow, int type) {
return false;
}
@Override
- public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
- @Nullable int[] offsetInWindow, int type) {
+ public boolean dispatchNestedPreScroll(int dx, int dy, int @Nullable [] consumed,
+ int @Nullable [] offsetInWindow, int type) {
return false;
}
@@ -645,40 +645,40 @@
}
@Override
- public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes) {
+ public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes) {
return false;
}
@Override
- public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes) {
+ public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes) {
}
@Override
- public void onStopNestedScroll(@NonNull View target) {
+ public void onStopNestedScroll(@NonNull View target) {
}
@Override
- public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
+ public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
}
@Override
public void onNestedPreScroll(
- @NonNull View target, int dx, int dy, @NonNull int[] consumed) {
+ @NonNull View target, int dx, int dy, int @NonNull [] consumed) {
}
@Override
- public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY,
+ public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY,
boolean consumed) {
return false;
}
@Override
- public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
+ public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
return false;
}
diff --git a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingParent3Test.java b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingParent3Test.java
index 8dd2de0..0759b03 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingParent3Test.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingParent3Test.java
@@ -31,8 +31,6 @@
import android.view.ViewGroup;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.view.NestedScrollingChild3;
import androidx.core.view.NestedScrollingParent3;
import androidx.core.view.ViewCompat;
@@ -40,6 +38,8 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -297,8 +297,8 @@
}
@Override
- public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
- int type) {
+ public void onNestedPreScroll(@NonNull View target, int dx, int dy,
+ int @NonNull [] consumed, int type) {
}
@@ -319,25 +319,25 @@
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
- int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
+ int dyUnconsumed, int @Nullable [] offsetInWindow, int type) {
return false;
}
@Override
- public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
- @Nullable int[] offsetInWindow, int type) {
+ public boolean dispatchNestedPreScroll(int dx, int dy, int @Nullable [] consumed,
+ int @Nullable [] offsetInWindow, int type) {
return false;
}
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed, int type, @Nullable int[] consumed) {
+ int dxUnconsumed, int dyUnconsumed, int type, int @Nullable [] consumed) {
}
@Override
public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
- int dyUnconsumed, @Nullable int[] offsetInWindow, int type,
- @NonNull int[] consumed) {
+ int dyUnconsumed, int @Nullable [] offsetInWindow, int type,
+ int @NonNull [] consumed) {
}
}
diff --git a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewWithCollapsingToolbarTest.java b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewWithCollapsingToolbarTest.java
index 2885cc3..83d20ae 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewWithCollapsingToolbarTest.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewWithCollapsingToolbarTest.java
@@ -28,11 +28,11 @@
import android.widget.LinearLayout;
import android.widget.TextView;
-import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
+import org.jspecify.annotations.NonNull;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/core/core/src/androidTest/java/androidx/core/widget/TestContentView.java b/core/core/src/androidTest/java/androidx/core/widget/TestContentView.java
index 12ce05c..a303d91 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/TestContentView.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/TestContentView.java
@@ -23,8 +23,8 @@
import android.util.AttributeSet;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
diff --git a/core/core/src/main/java/android/support/v4/os/ResultReceiver.java b/core/core/src/main/java/android/support/v4/os/ResultReceiver.java
index 9d4db1d..ebe4030 100644
--- a/core/core/src/main/java/android/support/v4/os/ResultReceiver.java
+++ b/core/core/src/main/java/android/support/v4/os/ResultReceiver.java
@@ -25,9 +25,10 @@
import android.os.Parcelable;
import android.os.RemoteException;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
/**
* Generic interface for receiving a callback result from someone. Use this
* by creating a subclass and implement {@link #onReceiveResult}, which you can
diff --git a/core/core/src/main/java/androidx/core/accessibilityservice/AccessibilityServiceInfoCompat.java b/core/core/src/main/java/androidx/core/accessibilityservice/AccessibilityServiceInfoCompat.java
index eacb1fc..da2e2718 100644
--- a/core/core/src/main/java/androidx/core/accessibilityservice/AccessibilityServiceInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/accessibilityservice/AccessibilityServiceInfoCompat.java
@@ -22,8 +22,8 @@
import android.os.Build;
import android.view.View;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* Helper for accessing features in {@link AccessibilityServiceInfo}.
@@ -195,8 +195,7 @@
@Deprecated
@androidx.annotation.ReplaceWith(expression = "info.loadDescription(packageManager)")
@SuppressWarnings("deprecation")
- @Nullable
- public static String loadDescription(
+ public static @Nullable String loadDescription(
@NonNull AccessibilityServiceInfo info, @NonNull PackageManager packageManager) {
return info.loadDescription(packageManager);
}
@@ -209,8 +208,7 @@
* @param feedbackType The feedback type.
* @return The string representation.
*/
- @NonNull
- public static String feedbackTypeToString(int feedbackType) {
+ public static @NonNull String feedbackTypeToString(int feedbackType) {
StringBuilder builder = new StringBuilder();
builder.append("[");
while (feedbackType > 0) {
@@ -249,8 +247,7 @@
* @param flag The flag.
* @return The string representation.
*/
- @Nullable
- public static String flagToString(int flag) {
+ public static @Nullable String flagToString(int flag) {
switch (flag) {
case AccessibilityServiceInfo.DEFAULT:
return "DEFAULT";
@@ -297,8 +294,7 @@
* @param capability The capability.
* @return The string representation.
*/
- @NonNull
- public static String capabilityToString(int capability) {
+ public static @NonNull String capabilityToString(int capability) {
switch (capability) {
case CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT:
return "CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT";
diff --git a/core/core/src/main/java/androidx/core/app/ActivityCompat.java b/core/core/src/main/java/androidx/core/app/ActivityCompat.java
index fd0a998..300112a 100644
--- a/core/core/src/main/java/androidx/core/app/ActivityCompat.java
+++ b/core/core/src/main/java/androidx/core/app/ActivityCompat.java
@@ -39,14 +39,15 @@
import androidx.annotation.IdRes;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.content.ContextCompat;
import androidx.core.content.LocusIdCompat;
import androidx.core.view.DragAndDropPermissionsCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
@@ -84,8 +85,8 @@
*
* @see #requestPermissions(android.app.Activity, String[], int)
*/
- void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
- @NonNull int[] grantResults);
+ void onRequestPermissionsResult(int requestCode, String @NonNull [] permissions,
+ int @NonNull [] grantResults);
}
/**
@@ -119,7 +120,7 @@
* @see ActivityCompat#requestPermissions(Activity, String[], int)
*/
boolean requestPermissions(@NonNull Activity activity,
- @NonNull String[] permissions, @IntRange(from = 0) int requestCode);
+ String @NonNull [] permissions, @IntRange(from = 0) int requestCode);
/**
* Determines whether the delegate should handle the permission request as part of
@@ -173,9 +174,8 @@
/**
*/
- @Nullable
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
- public static PermissionCompatDelegate getPermissionCompatDelegate() {
+ public static @Nullable PermissionCompatDelegate getPermissionCompatDelegate() {
return sDelegate;
}
@@ -322,8 +322,7 @@
* referrer information, applications can spoof it.</p>
*/
@SuppressWarnings("deprecation")
- @Nullable
- public static Uri getReferrer(@NonNull Activity activity) {
+ public static @Nullable Uri getReferrer(@NonNull Activity activity) {
if (Build.VERSION.SDK_INT >= 22) {
return Api22Impl.getReferrer(activity);
}
@@ -356,8 +355,8 @@
* @see androidx.core.view.ViewCompat#requireViewById(View, int)
*/
@SuppressWarnings("TypeParameterUnusedInFormals")
- @NonNull
- public static <T extends View> T requireViewById(@NonNull Activity activity, @IdRes int id) {
+ public static <T extends View> @NonNull T requireViewById(@NonNull Activity activity,
+ @IdRes int id) {
if (Build.VERSION.SDK_INT >= 28) {
return Api28Impl.requireViewById(activity, id);
}
@@ -501,7 +500,7 @@
* @see #shouldShowRequestPermissionRationale(android.app.Activity, String)
*/
public static void requestPermissions(final @NonNull Activity activity,
- final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode) {
+ final String @NonNull [] permissions, final @IntRange(from = 0) int requestCode) {
if (sDelegate != null
&& sDelegate.requestPermissions(activity, permissions, requestCode)) {
// Delegate has handled the permission request.
@@ -637,8 +636,7 @@
* URIs. {@code null} if no content URIs are associated with the event or if permissions could
* not be granted.
*/
- @Nullable
- public static DragAndDropPermissionsCompat requestDragAndDropPermissions(
+ public static @Nullable DragAndDropPermissionsCompat requestDragAndDropPermissions(
@NonNull Activity activity, @NonNull DragEvent dragEvent) {
return DragAndDropPermissionsCompat.request(activity, dragEvent);
}
@@ -650,7 +648,7 @@
*
* @param activity The activity to recreate
*/
- public static void recreate(@NonNull final Activity activity) {
+ public static void recreate(final @NonNull Activity activity) {
if (Build.VERSION.SDK_INT >= 28) {
// On Android P and later, we can safely rely on the platform recreate()
activity.recreate();
@@ -699,8 +697,8 @@
* <li>API 29 and earlier, this method is no-op.
* </ul>
*/
- public static void setLocusContext(@NonNull final Activity activity,
- @Nullable final LocusIdCompat locusId, @Nullable final Bundle bundle) {
+ public static void setLocusContext(final @NonNull Activity activity,
+ final @Nullable LocusIdCompat locusId, final @Nullable Bundle bundle) {
if (Build.VERSION.SDK_INT >= 30) {
Api30Impl.setLocusContext(activity, locusId, bundle);
}
@@ -765,8 +763,8 @@
// This class is not instantiable.
}
- static void setLocusContext(@NonNull final Activity activity,
- @Nullable final LocusIdCompat locusId, @Nullable final Bundle bundle) {
+ static void setLocusContext(final @NonNull Activity activity,
+ final @Nullable LocusIdCompat locusId, final @Nullable Bundle bundle) {
activity.setLocusContext(locusId == null ? null : locusId.toLocusId(), bundle);
}
@@ -781,7 +779,7 @@
// This class is not instantiable.
}
- static boolean isLaunchedFromBubble(@NonNull final Activity activity) {
+ static boolean isLaunchedFromBubble(final @NonNull Activity activity) {
return activity.isLaunchedFromBubble();
}
diff --git a/core/core/src/main/java/androidx/core/app/ActivityManagerCompat.java b/core/core/src/main/java/androidx/core/app/ActivityManagerCompat.java
index 6afb250..e3dcf71 100644
--- a/core/core/src/main/java/androidx/core/app/ActivityManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/app/ActivityManagerCompat.java
@@ -18,7 +18,7 @@
import android.app.ActivityManager;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Helper for accessing features in {@link android.app.ActivityManager} in a backwards compatible
diff --git a/core/core/src/main/java/androidx/core/app/ActivityOptionsCompat.java b/core/core/src/main/java/androidx/core/app/ActivityOptionsCompat.java
index f6bcc91..3bed9b4 100644
--- a/core/core/src/main/java/androidx/core/app/ActivityOptionsCompat.java
+++ b/core/core/src/main/java/androidx/core/app/ActivityOptionsCompat.java
@@ -30,11 +30,12 @@
import android.view.View;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.util.Pair;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -83,8 +84,7 @@
* @return Returns a new ActivityOptions object that you can use to supply
* these options as the options Bundle when starting an activity.
*/
- @NonNull
- public static ActivityOptionsCompat makeCustomAnimation(@NonNull Context context,
+ public static @NonNull ActivityOptionsCompat makeCustomAnimation(@NonNull Context context,
int enterResId, int exitResId) {
return new ActivityOptionsCompatImpl(
ActivityOptions.makeCustomAnimation(context, enterResId, exitResId));
@@ -110,8 +110,7 @@
* @return Returns a new ActivityOptions object that you can use to supply
* these options as the options Bundle when starting an activity.
*/
- @NonNull
- public static ActivityOptionsCompat makeScaleUpAnimation(@NonNull View source,
+ public static @NonNull ActivityOptionsCompat makeScaleUpAnimation(@NonNull View source,
int startX, int startY, int startWidth, int startHeight) {
return new ActivityOptionsCompatImpl(
ActivityOptions.makeScaleUpAnimation(source, startX, startY, startWidth,
@@ -132,8 +131,7 @@
* @return Returns a new ActivityOptions object that you can use to
* supply these options as the options Bundle when starting an activity.
*/
- @NonNull
- public static ActivityOptionsCompat makeClipRevealAnimation(@NonNull View source,
+ public static @NonNull ActivityOptionsCompat makeClipRevealAnimation(@NonNull View source,
int startX, int startY, int width, int height) {
if (Build.VERSION.SDK_INT >= 23) {
return new ActivityOptionsCompatImpl(
@@ -161,8 +159,7 @@
* @return Returns a new ActivityOptions object that you can use to supply
* these options as the options Bundle when starting an activity.
*/
- @NonNull
- public static ActivityOptionsCompat makeThumbnailScaleUpAnimation(@NonNull View source,
+ public static @NonNull ActivityOptionsCompat makeThumbnailScaleUpAnimation(@NonNull View source,
@NonNull Bitmap thumbnail, int startX, int startY) {
return new ActivityOptionsCompatImpl(
ActivityOptions.makeThumbnailScaleUpAnimation(source, thumbnail, startX, startY));
@@ -186,9 +183,9 @@
* @return Returns a new ActivityOptions object that you can use to
* supply these options as the options Bundle when starting an activity.
*/
- @NonNull
- public static ActivityOptionsCompat makeSceneTransitionAnimation(@NonNull Activity activity,
- @NonNull View sharedElement, @NonNull String sharedElementName) {
+ public static @NonNull ActivityOptionsCompat makeSceneTransitionAnimation(
+ @NonNull Activity activity, @NonNull View sharedElement,
+ @NonNull String sharedElementName) {
return new ActivityOptionsCompatImpl(
ActivityOptions.makeSceneTransitionAnimation(activity, sharedElement,
sharedElementName));
@@ -211,10 +208,9 @@
* @return Returns a new ActivityOptions object that you can use to
* supply these options as the options Bundle when starting an activity.
*/
- @NonNull
@SuppressWarnings("unchecked")
- public static ActivityOptionsCompat makeSceneTransitionAnimation(@NonNull Activity activity,
- @Nullable Pair<View, String>... sharedElements) {
+ public static @NonNull ActivityOptionsCompat makeSceneTransitionAnimation(
+ @NonNull Activity activity, Pair<View, String> @Nullable ... sharedElements) {
android.util.Pair<View, String>[] pairs = null;
if (sharedElements != null) {
pairs = new android.util.Pair[sharedElements.length];
@@ -237,8 +233,7 @@
* {@link android.R.attr#launchMode launchMode} values of
* <code>singleInstance</code> or <code>singleTask</code>.
*/
- @NonNull
- public static ActivityOptionsCompat makeTaskLaunchBehind() {
+ public static @NonNull ActivityOptionsCompat makeTaskLaunchBehind() {
return new ActivityOptionsCompatImpl(ActivityOptions.makeTaskLaunchBehind());
}
@@ -246,8 +241,7 @@
* Create a basic ActivityOptions that has no special animation associated with it.
* Other options can still be set.
*/
- @NonNull
- public static ActivityOptionsCompat makeBasic() {
+ public static @NonNull ActivityOptionsCompat makeBasic() {
if (Build.VERSION.SDK_INT >= 23) {
return new ActivityOptionsCompatImpl(ActivityOptions.makeBasic());
}
@@ -282,9 +276,8 @@
}
}
- @NonNull
@Override
- public ActivityOptionsCompat setLaunchBounds(@Nullable Rect screenSpacePixelRect) {
+ public @NonNull ActivityOptionsCompat setLaunchBounds(@Nullable Rect screenSpacePixelRect) {
if (Build.VERSION.SDK_INT < 24) {
return this;
}
@@ -300,9 +293,8 @@
return mActivityOptions.getLaunchBounds();
}
- @NonNull
@Override
- public ActivityOptionsCompat setShareIdentityEnabled(boolean shareIdentity) {
+ public @NonNull ActivityOptionsCompat setShareIdentityEnabled(boolean shareIdentity) {
if (Build.VERSION.SDK_INT < 34) {
return this;
}
@@ -311,9 +303,8 @@
}
@SuppressLint("WrongConstant")
- @NonNull
@Override
- public ActivityOptionsCompat setPendingIntentBackgroundActivityStartMode(
+ public @NonNull ActivityOptionsCompat setPendingIntentBackgroundActivityStartMode(
@BackgroundActivityStartMode int state) {
if (Build.VERSION.SDK_INT >= 34) {
mActivityOptions.setPendingIntentBackgroundActivityStartMode(state);
@@ -334,9 +325,8 @@
}
}
- @NonNull
@Override
- public ActivityOptionsCompat setLaunchDisplayId(int launchDisplayId) {
+ public @NonNull ActivityOptionsCompat setLaunchDisplayId(int launchDisplayId) {
if (Build.VERSION.SDK_INT >= 26) {
mActivityOptions.setLaunchDisplayId(launchDisplayId);
}
@@ -357,8 +347,7 @@
* {@link android.content.pm.PackageManager#FEATURE_PICTURE_IN_PICTURE} enabled.
* @param screenSpacePixelRect Launch bounds to use for the activity or null for fullscreen.
*/
- @NonNull
- public ActivityOptionsCompat setLaunchBounds(@Nullable Rect screenSpacePixelRect) {
+ public @NonNull ActivityOptionsCompat setLaunchBounds(@Nullable Rect screenSpacePixelRect) {
return this;
}
@@ -367,8 +356,7 @@
* @see #setLaunchBounds(Rect)
* @return Bounds used to launch the activity.
*/
- @Nullable
- public Rect getLaunchBounds() {
+ public @Nullable Rect getLaunchBounds() {
return null;
}
@@ -379,8 +367,7 @@
* object; you must not modify it, but can supply it to the startActivity
* methods that take an options Bundle.
*/
- @Nullable
- public Bundle toBundle() {
+ public @Nullable Bundle toBundle() {
return null;
}
@@ -442,8 +429,7 @@
* @see Activity#getLaunchedFromPackage()
* @see Activity#getLaunchedFromUid()
*/
- @NonNull
- public ActivityOptionsCompat setShareIdentityEnabled(boolean shareIdentity) {
+ public @NonNull ActivityOptionsCompat setShareIdentityEnabled(boolean shareIdentity) {
return this;
}
@@ -456,8 +442,7 @@
* {@link ActivityOptions#MODE_BACKGROUND_ACTIVITY_START_ALLOWED} if the PendingIntent is from a
* trusted source and/or executed on behalf the user.
*/
- @NonNull
- public ActivityOptionsCompat setPendingIntentBackgroundActivityStartMode(
+ public @NonNull ActivityOptionsCompat setPendingIntentBackgroundActivityStartMode(
@BackgroundActivityStartMode int state) {
return this;
}
@@ -489,8 +474,7 @@
* @param launchDisplayId The id of the display where the activity should be launched.
* @return {@code this} {@link ActivityOptions} instance.
*/
- @NonNull
- public ActivityOptionsCompat setLaunchDisplayId(int launchDisplayId) {
+ public @NonNull ActivityOptionsCompat setLaunchDisplayId(int launchDisplayId) {
return this;
}
}
diff --git a/core/core/src/main/java/androidx/core/app/ActivityRecreator.java b/core/core/src/main/java/androidx/core/app/ActivityRecreator.java
index 02f60f9..034feb2 100644
--- a/core/core/src/main/java/androidx/core/app/ActivityRecreator.java
+++ b/core/core/src/main/java/androidx/core/app/ActivityRecreator.java
@@ -30,9 +30,10 @@
import android.os.Looper;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
@@ -103,7 +104,7 @@
*
* @return true if a recreate() task was successfully scheduled.
*/
- static boolean recreate(@NonNull final Activity activity) {
+ static boolean recreate(final @NonNull Activity activity) {
// On Android O and later we can rely on the platform recreate()
if (SDK_INT >= 28) {
activity.recreate();
diff --git a/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java b/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java
index c841e77..ab57c77 100644
--- a/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java
@@ -24,9 +24,10 @@
import android.app.PendingIntent;
import android.os.Build;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
/**
* Compatibility library for {@link AlarmManager} with fallbacks for older platforms.
*/
diff --git a/core/core/src/main/java/androidx/core/app/AppComponentFactory.java b/core/core/src/main/java/androidx/core/app/AppComponentFactory.java
index d4e620e..3dfb007 100644
--- a/core/core/src/main/java/androidx/core/app/AppComponentFactory.java
+++ b/core/core/src/main/java/androidx/core/app/AppComponentFactory.java
@@ -25,10 +25,11 @@
import android.content.ContentProvider;
import android.content.Intent;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.reflect.InvocationTargetException;
/**
@@ -42,9 +43,8 @@
/**
* @see #instantiateActivityCompat
*/
- @NonNull
@Override
- public final Activity instantiateActivity(
+ public final @NonNull Activity instantiateActivity(
@NonNull ClassLoader cl, @NonNull String className, @Nullable Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return checkCompatWrapper(instantiateActivityCompat(cl, className, intent));
@@ -53,9 +53,8 @@
/**
* @see #instantiateApplicationCompat
*/
- @NonNull
@Override
- public final Application instantiateApplication(
+ public final @NonNull Application instantiateApplication(
@NonNull ClassLoader cl, @NonNull String className)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return checkCompatWrapper(instantiateApplicationCompat(cl, className));
@@ -64,9 +63,8 @@
/**
* @see #instantiateReceiverCompat
*/
- @NonNull
@Override
- public final BroadcastReceiver instantiateReceiver(
+ public final @NonNull BroadcastReceiver instantiateReceiver(
@NonNull ClassLoader cl, @NonNull String className, @Nullable Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return checkCompatWrapper(instantiateReceiverCompat(cl, className, intent));
@@ -75,9 +73,8 @@
/**
* @see #instantiateProviderCompat
*/
- @NonNull
@Override
- public final ContentProvider instantiateProvider(
+ public final @NonNull ContentProvider instantiateProvider(
@NonNull ClassLoader cl, @NonNull String className)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return checkCompatWrapper(instantiateProviderCompat(cl, className));
@@ -86,9 +83,8 @@
/**
* @see #instantiateServiceCompat
*/
- @NonNull
@Override
- public final Service instantiateService(
+ public final @NonNull Service instantiateService(
@NonNull ClassLoader cl, @NonNull String className, @Nullable Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return checkCompatWrapper(instantiateServiceCompat(cl, className, intent));
diff --git a/core/core/src/main/java/androidx/core/app/AppLaunchChecker.java b/core/core/src/main/java/androidx/core/app/AppLaunchChecker.java
index 65f8974..1a764b9 100644
--- a/core/core/src/main/java/androidx/core/app/AppLaunchChecker.java
+++ b/core/core/src/main/java/androidx/core/app/AppLaunchChecker.java
@@ -23,9 +23,10 @@
import android.content.SharedPreferences;
import android.os.Bundle;
-import androidx.annotation.NonNull;
import androidx.core.content.IntentCompat;
+import org.jspecify.annotations.NonNull;
+
/**
* This class provides APIs for determining how an app has been launched.
* This can be useful if you want to confirm that a user has launched your
diff --git a/core/core/src/main/java/androidx/core/app/AppLocalesStorageHelper.java b/core/core/src/main/java/androidx/core/app/AppLocalesStorageHelper.java
index d76b6b2..22c3e0b 100644
--- a/core/core/src/main/java/androidx/core/app/AppLocalesStorageHelper.java
+++ b/core/core/src/main/java/androidx/core/app/AppLocalesStorageHelper.java
@@ -22,9 +22,9 @@
import android.util.Log;
import android.util.Xml;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
@@ -54,8 +54,7 @@
/**
* Returns app locales after reading from storage, fetched using the application context.
*/
- @NonNull
- public static String readLocales(@NonNull Context context) {
+ public static @NonNull String readLocales(@NonNull Context context) {
synchronized (sAppLocaleStorageSync) {
String appLocales = "";
diff --git a/core/core/src/main/java/androidx/core/app/AppOpsManagerCompat.java b/core/core/src/main/java/androidx/core/app/AppOpsManagerCompat.java
index 9f18fd6..c7eabbd 100644
--- a/core/core/src/main/java/androidx/core/app/AppOpsManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/app/AppOpsManagerCompat.java
@@ -22,10 +22,11 @@
import android.content.Context;
import android.os.Binder;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Helper for accessing features in {@link android.app.AppOpsManager}.
*/
@@ -72,8 +73,7 @@
* @param permission The permission.
* @return The app op associated with the permission or null.
*/
- @Nullable
- public static String permissionToOp(@NonNull String permission) {
+ public static @Nullable String permissionToOp(@NonNull String permission) {
if (SDK_INT >= 23) {
return Api23Impl.permissionToOp(permission);
} else {
diff --git a/core/core/src/main/java/androidx/core/app/BundleCompat.java b/core/core/src/main/java/androidx/core/app/BundleCompat.java
index 9be2181..3695143 100644
--- a/core/core/src/main/java/androidx/core/app/BundleCompat.java
+++ b/core/core/src/main/java/androidx/core/app/BundleCompat.java
@@ -19,8 +19,8 @@
import android.os.Bundle;
import android.os.IBinder;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* Helper for accessing features in {@link Bundle}.
@@ -42,8 +42,7 @@
*/
@Deprecated
@androidx.annotation.ReplaceWith(expression = "bundle.getBinder(key)")
- @Nullable
- public static IBinder getBinder(@NonNull Bundle bundle, @Nullable String key) {
+ public static @Nullable IBinder getBinder(@NonNull Bundle bundle, @Nullable String key) {
return bundle.getBinder(key);
}
diff --git a/core/core/src/main/java/androidx/core/app/CoreComponentFactory.java b/core/core/src/main/java/androidx/core/app/CoreComponentFactory.java
index d20458c..ba5f5cd 100644
--- a/core/core/src/main/java/androidx/core/app/CoreComponentFactory.java
+++ b/core/core/src/main/java/androidx/core/app/CoreComponentFactory.java
@@ -24,11 +24,12 @@
import android.content.ContentProvider;
import android.content.Intent;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Instance of AppComponentFactory for support libraries.
* @see CompatWrapped
@@ -36,39 +37,37 @@
@RequiresApi(api = 28)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class CoreComponentFactory extends AppComponentFactory {
- @NonNull
@Override
- public Activity instantiateActivity(
+ public @NonNull Activity instantiateActivity(
@NonNull ClassLoader cl, @NonNull String className, @Nullable Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return checkCompatWrapper(super.instantiateActivity(cl, className, intent));
}
- @NonNull
@Override
- public Application instantiateApplication(@NonNull ClassLoader cl, @NonNull String className)
+ public @NonNull Application instantiateApplication(@NonNull ClassLoader cl,
+ @NonNull String className)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return checkCompatWrapper(super.instantiateApplication(cl, className));
}
- @NonNull
@Override
- public BroadcastReceiver instantiateReceiver(@NonNull ClassLoader cl, @NonNull String className,
+ public @NonNull BroadcastReceiver instantiateReceiver(@NonNull ClassLoader cl,
+ @NonNull String className,
@Nullable Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return checkCompatWrapper(super.instantiateReceiver(cl, className, intent));
}
- @NonNull
@Override
- public ContentProvider instantiateProvider(@NonNull ClassLoader cl, @NonNull String className)
+ public @NonNull ContentProvider instantiateProvider(@NonNull ClassLoader cl,
+ @NonNull String className)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return checkCompatWrapper(super.instantiateProvider(cl, className));
}
- @NonNull
@Override
- public Service instantiateService(
+ public @NonNull Service instantiateService(
@NonNull ClassLoader cl, @NonNull String className, @Nullable Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return checkCompatWrapper(super.instantiateService(cl, className, intent));
diff --git a/core/core/src/main/java/androidx/core/app/DialogCompat.java b/core/core/src/main/java/androidx/core/app/DialogCompat.java
index 0d195ae..809012f 100644
--- a/core/core/src/main/java/androidx/core/app/DialogCompat.java
+++ b/core/core/src/main/java/androidx/core/app/DialogCompat.java
@@ -20,9 +20,10 @@
import android.os.Build;
import android.view.View;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
/**
* Helper for accessing features in {@link android.app.Dialog} in a backwards compatible
* fashion.
@@ -51,8 +52,7 @@
* @see Dialog#requireViewById(int)
* @see Dialog#findViewById(int)
*/
- @NonNull
- public static View requireViewById(@NonNull Dialog dialog, int id) {
+ public static @NonNull View requireViewById(@NonNull Dialog dialog, int id) {
if (Build.VERSION.SDK_INT >= 28) {
return Api28Impl.requireViewById(dialog, id);
} else {
diff --git a/core/core/src/main/java/androidx/core/app/FrameMetricsAggregator.java b/core/core/src/main/java/androidx/core/app/FrameMetricsAggregator.java
index dace804..561f194 100644
--- a/core/core/src/main/java/androidx/core/app/FrameMetricsAggregator.java
+++ b/core/core/src/main/java/androidx/core/app/FrameMetricsAggregator.java
@@ -27,11 +27,12 @@
import android.view.Window;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
@@ -237,8 +238,7 @@
* the {@code [TOTAL_INDEX]} item.
* @see #getMetrics()
*/
- @Nullable
- public SparseIntArray[] remove(@NonNull Activity activity) {
+ public SparseIntArray @Nullable [] remove(@NonNull Activity activity) {
return mInstance.remove(activity);
}
@@ -254,8 +254,7 @@
* @see #remove(Activity)
* @see #getMetrics()
*/
- @Nullable
- public SparseIntArray[] stop() {
+ public SparseIntArray @Nullable [] stop() {
return mInstance.stop();
}
@@ -267,8 +266,7 @@
* the {@code [TOTAL_INDEX]} item.
* @see #getMetrics()
*/
- @Nullable
- public SparseIntArray[] reset() {
+ public SparseIntArray @Nullable [] reset() {
return mInstance.reset();
}
@@ -295,8 +293,7 @@
* SparseIntArray object, e.g., data for {@code TOTAL_DURATION} is stored in
* the {@code [TOTAL_INDEX]} item.
*/
- @Nullable
- public SparseIntArray[] getMetrics() {
+ public SparseIntArray @Nullable [] getMetrics() {
return mInstance.getMetrics();
}
diff --git a/core/core/src/main/java/androidx/core/app/GrammaticalInflectionManagerCompat.java b/core/core/src/main/java/androidx/core/app/GrammaticalInflectionManagerCompat.java
index a95e565..7d1da89 100644
--- a/core/core/src/main/java/androidx/core/app/GrammaticalInflectionManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/app/GrammaticalInflectionManagerCompat.java
@@ -22,11 +22,12 @@
import androidx.annotation.AnyThread;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
diff --git a/core/core/src/main/java/androidx/core/app/JobIntentService.java b/core/core/src/main/java/androidx/core/app/JobIntentService.java
index c143946..2feaa78 100644
--- a/core/core/src/main/java/androidx/core/app/JobIntentService.java
+++ b/core/core/src/main/java/androidx/core/app/JobIntentService.java
@@ -30,10 +30,11 @@
import android.os.PowerManager;
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.HashMap;
diff --git a/core/core/src/main/java/androidx/core/app/LocaleManagerCompat.java b/core/core/src/main/java/androidx/core/app/LocaleManagerCompat.java
index 91ac468..7242f56 100644
--- a/core/core/src/main/java/androidx/core/app/LocaleManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/app/LocaleManagerCompat.java
@@ -24,11 +24,12 @@
import android.os.LocaleList;
import androidx.annotation.AnyThread;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.core.os.LocaleListCompat;
+import org.jspecify.annotations.NonNull;
+
import java.util.Locale;
/**
@@ -47,9 +48,8 @@
* is set, this method helps cater to rare use-cases which might require specifically knowing
* the system locale.
*/
- @NonNull
@AnyThread
- public static LocaleListCompat getSystemLocales(@NonNull Context context) {
+ public static @NonNull LocaleListCompat getSystemLocales(@NonNull Context context) {
LocaleListCompat systemLocales = LocaleListCompat.getEmptyLocaleList();
// TODO: modify the check to Build.Version.SDK_INT >= 33.
if (Build.VERSION.SDK_INT >= 33) {
@@ -74,8 +74,7 @@
* set.
*/
@AnyThread
- @NonNull
- public static LocaleListCompat getApplicationLocales(@NonNull Context context) {
+ public static @NonNull LocaleListCompat getApplicationLocales(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= 33) {
// If the API version is 33 or above we want to redirect the call to the framework API.
Object localeManager = getLocaleManagerForApplication(context);
diff --git a/core/core/src/main/java/androidx/core/app/NavUtils.java b/core/core/src/main/java/androidx/core/app/NavUtils.java
index 56fffc4..e08c4d8 100644
--- a/core/core/src/main/java/androidx/core/app/NavUtils.java
+++ b/core/core/src/main/java/androidx/core/app/NavUtils.java
@@ -26,8 +26,8 @@
import android.os.Build;
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* NavUtils provides helper functionality for applications implementing
@@ -118,8 +118,7 @@
* @param sourceActivity Activity to fetch a parent intent for
* @return a new Intent targeting the defined parent activity of sourceActivity
*/
- @Nullable
- public static Intent getParentActivityIntent(@NonNull Activity sourceActivity) {
+ public static @Nullable Intent getParentActivityIntent(@NonNull Activity sourceActivity) {
// Prefer the "real" JB definition, else fall back to the meta-data element.
Intent result = sourceActivity.getParentActivityIntent();
if (result != null) {
@@ -153,8 +152,7 @@
* @return a new Intent targeting the defined parent activity of sourceActivity
* @throws NameNotFoundException if the ComponentName for sourceActivityClass is invalid
*/
- @Nullable
- public static Intent getParentActivityIntent(@NonNull Context context,
+ public static @Nullable Intent getParentActivityIntent(@NonNull Context context,
@NonNull Class<?> sourceActivityClass)
throws NameNotFoundException {
String parentActivity = getParentActivityName(context,
@@ -180,8 +178,7 @@
* @return a new Intent targeting the defined parent activity of sourceActivity
* @throws NameNotFoundException if the ComponentName for sourceActivityClass is invalid
*/
- @Nullable
- public static Intent getParentActivityIntent(@NonNull Context context,
+ public static @Nullable Intent getParentActivityIntent(@NonNull Context context,
@NonNull ComponentName componentName)
throws NameNotFoundException {
String parentActivity = getParentActivityName(context, componentName);
@@ -206,8 +203,7 @@
* @return The fully qualified class name of sourceActivity's parent activity or null if
* it was not specified
*/
- @Nullable
- public static String getParentActivityName(@NonNull Activity sourceActivity) {
+ public static @Nullable String getParentActivityName(@NonNull Activity sourceActivity) {
try {
return getParentActivityName(sourceActivity, sourceActivity.getComponentName());
} catch (NameNotFoundException e) {
@@ -225,8 +221,7 @@
* @return The fully qualified class name of sourceActivity's parent activity or null if
* it was not specified
*/
- @Nullable
- public static String getParentActivityName(@NonNull Context context,
+ public static @Nullable String getParentActivityName(@NonNull Context context,
@NonNull ComponentName componentName)
throws NameNotFoundException {
PackageManager pm = context.getPackageManager();
diff --git a/core/core/src/main/java/androidx/core/app/NotificationChannelCompat.java b/core/core/src/main/java/androidx/core/app/NotificationChannelCompat.java
index af5365c..3c74502 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationChannelCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationChannelCompat.java
@@ -25,12 +25,13 @@
import android.os.Build;
import android.provider.Settings;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* A representation of settings that apply to a collection of similarly themed notifications.
*
@@ -50,8 +51,7 @@
private static final int DEFAULT_LIGHT_COLOR = 0;
// These fields are settable through the builder
- @NonNull
- final String mId;
+ final @NonNull String mId;
CharSequence mName;
int mImportance;
String mDescription;
@@ -99,8 +99,7 @@
* <p>The recommended maximum length is 40 characters; the value may be truncated if it
* is too long.
*/
- @NonNull
- public Builder setName(@Nullable CharSequence name) {
+ public @NonNull Builder setName(@Nullable CharSequence name) {
mChannel.mName = name;
return this;
}
@@ -114,8 +113,7 @@
* @param importance the amount the user should be interrupted by notifications from this
* channel.
*/
- @NonNull
- public Builder setImportance(int importance) {
+ public @NonNull Builder setImportance(int importance) {
mChannel.mImportance = importance;
return this;
}
@@ -126,8 +124,7 @@
* <p>The recommended maximum length is 300 characters; the value may be truncated if it is
* too long.
*/
- @NonNull
- public Builder setDescription(@Nullable String description) {
+ public @NonNull Builder setDescription(@Nullable String description) {
mChannel.mDescription = description;
return this;
}
@@ -144,8 +141,7 @@
* @param groupId the id of a group created by
* {@link NotificationManagerCompat#createNotificationChannelGroup}.
*/
- @NonNull
- public Builder setGroup(@Nullable String groupId) {
+ public @NonNull Builder setGroup(@Nullable String groupId) {
mChannel.mGroupId = groupId;
return this;
}
@@ -159,8 +155,7 @@
*
* @param showBadge true if badges should be allowed to be shown.
*/
- @NonNull
- public Builder setShowBadge(boolean showBadge) {
+ public @NonNull Builder setShowBadge(boolean showBadge) {
mChannel.mShowBadge = showBadge;
return this;
}
@@ -174,8 +169,8 @@
* Only modifiable before the channel is submitted to
* {@link NotificationManagerCompat#createNotificationChannel(NotificationChannelCompat)}.
*/
- @NonNull
- public Builder setSound(@Nullable Uri sound, @Nullable AudioAttributes audioAttributes) {
+ public @NonNull Builder setSound(@Nullable Uri sound,
+ @Nullable AudioAttributes audioAttributes) {
mChannel.mSound = sound;
mChannel.mAudioAttributes = audioAttributes;
return this;
@@ -188,8 +183,7 @@
* Only modifiable before the channel is submitted to
* {@link NotificationManagerCompat#createNotificationChannel(NotificationChannelCompat)}.
*/
- @NonNull
- public Builder setLightsEnabled(boolean lights) {
+ public @NonNull Builder setLightsEnabled(boolean lights) {
mChannel.mLights = lights;
return this;
}
@@ -202,8 +196,7 @@
* Only modifiable before the channel is submitted to
* {@link NotificationManagerCompat#createNotificationChannel(NotificationChannelCompat)}.
*/
- @NonNull
- public Builder setLightColor(int argb) {
+ public @NonNull Builder setLightColor(int argb) {
mChannel.mLightColor = argb;
return this;
}
@@ -215,8 +208,7 @@
* Only modifiable before the channel is submitted to
* {@link NotificationManagerCompat#createNotificationChannel(NotificationChannelCompat)}.
*/
- @NonNull
- public Builder setVibrationEnabled(boolean vibration) {
+ public @NonNull Builder setVibrationEnabled(boolean vibration) {
mChannel.mVibrationEnabled = vibration;
return this;
}
@@ -229,8 +221,7 @@
* Only modifiable before the channel is submitted to
* {@link NotificationManagerCompat#createNotificationChannel(NotificationChannelCompat)}.
*/
- @NonNull
- public Builder setVibrationPattern(@Nullable long[] vibrationPattern) {
+ public @NonNull Builder setVibrationPattern(long @Nullable [] vibrationPattern) {
mChannel.mVibrationEnabled = vibrationPattern != null && vibrationPattern.length > 0;
mChannel.mVibrationPattern = vibrationPattern;
return this;
@@ -252,8 +243,7 @@
* @param conversationId The {@link ShortcutInfoCompat#getId()} of the shortcut
* representing this channel's conversation.
*/
- @NonNull
- public Builder setConversationId(@NonNull String parentChannelId,
+ public @NonNull Builder setConversationId(@NonNull String parentChannelId,
@NonNull String conversationId) {
if (Build.VERSION.SDK_INT >= 30) {
mChannel.mParentId = parentChannelId;
@@ -265,8 +255,7 @@
/**
* Creates a {@link NotificationChannelCompat} instance.
*/
- @NonNull
- public NotificationChannelCompat build() {
+ public @NonNull NotificationChannelCompat build() {
return mChannel;
}
}
@@ -335,8 +324,7 @@
/**
* Creates a {@link Builder} instance with all the writeable property values of this instance.
*/
- @NonNull
- public Builder toBuilder() {
+ public @NonNull Builder toBuilder() {
return new Builder(mId, mImportance)
.setName(mName)
.setDescription(mDescription)
@@ -353,24 +341,21 @@
/**
* Returns the id of this channel.
*/
- @NonNull
- public String getId() {
+ public @NonNull String getId() {
return mId;
}
/**
* Returns the user visible name of this channel.
*/
- @Nullable
- public CharSequence getName() {
+ public @Nullable CharSequence getName() {
return mName;
}
/**
* Returns the user visible description of this channel.
*/
- @Nullable
- public String getDescription() {
+ public @Nullable String getDescription() {
return mDescription;
}
@@ -390,16 +375,14 @@
/**
* Returns the notification sound for this channel.
*/
- @Nullable
- public Uri getSound() {
+ public @Nullable Uri getSound() {
return mSound;
}
/**
* Returns the audio attributes for sound played by notifications posted to this channel.
*/
- @Nullable
- public AudioAttributes getAudioAttributes() {
+ public @Nullable AudioAttributes getAudioAttributes() {
return mAudioAttributes;
}
@@ -429,8 +412,7 @@
* Returns the vibration pattern for notifications posted to this channel. Will be ignored if
* vibration is not enabled ({@link #shouldVibrate()}.
*/
- @Nullable
- public long[] getVibrationPattern() {
+ public long @Nullable [] getVibrationPattern() {
return mVibrationPattern;
}
@@ -449,8 +431,7 @@
*
* This is used only for visually grouping channels in the UI.
*/
- @Nullable
- public String getGroup() {
+ public @Nullable String getGroup() {
return mGroupId;
}
@@ -459,8 +440,7 @@
* a conversation related channel.
* See {@link Builder#setConversationId(String, String)}.
*/
- @Nullable
- public String getParentChannelId() {
+ public @Nullable String getParentChannelId() {
return mParentId;
}
@@ -469,8 +449,7 @@
* if it's associated with a conversation.
* See {@link Builder#setConversationId(String, String)}.
*/
- @Nullable
- public String getConversationId() {
+ public @Nullable String getConversationId() {
return mConversationId;
}
diff --git a/core/core/src/main/java/androidx/core/app/NotificationChannelGroupCompat.java b/core/core/src/main/java/androidx/core/app/NotificationChannelGroupCompat.java
index ea2ca37..353ff75 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationChannelGroupCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationChannelGroupCompat.java
@@ -21,11 +21,12 @@
import android.content.Intent;
import android.os.Build;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -72,8 +73,7 @@
* <p>The recommended maximum length is 40 characters; the value may be truncated if it
* is too long.
*/
- @NonNull
- public Builder setName(@Nullable CharSequence name) {
+ public @NonNull Builder setName(@Nullable CharSequence name) {
mGroup.mName = name;
return this;
}
@@ -85,8 +85,7 @@
* is too
* long.
*/
- @NonNull
- public Builder setDescription(@Nullable String description) {
+ public @NonNull Builder setDescription(@Nullable String description) {
mGroup.mDescription = description;
return this;
}
@@ -94,8 +93,7 @@
/**
* Creates a {@link NotificationChannelGroupCompat} instance.
*/
- @NonNull
- public NotificationChannelGroupCompat build() {
+ public @NonNull NotificationChannelGroupCompat build() {
return mGroup;
}
}
@@ -159,8 +157,7 @@
/**
* Creates a {@link Builder} instance with all the writeable property values of this instance.
*/
- @NonNull
- public Builder toBuilder() {
+ public @NonNull Builder toBuilder() {
return new Builder(mId)
.setName(mName)
.setDescription(mDescription);
@@ -169,24 +166,21 @@
/**
* Gets the id of the group.
*/
- @NonNull
- public String getId() {
+ public @NonNull String getId() {
return mId;
}
/**
* Gets the user visible name of the group.
*/
- @Nullable
- public CharSequence getName() {
+ public @Nullable CharSequence getName() {
return mName;
}
/**
* Gets the user visible description of the group.
*/
- @Nullable
- public String getDescription() {
+ public @Nullable String getDescription() {
return mDescription;
}
@@ -211,8 +205,7 @@
* <p>This is a read-only property which is only valid on instances fetched from the
* {@link NotificationManagerCompat}.
*/
- @NonNull
- public List<NotificationChannelCompat> getChannels() {
+ public @NonNull List<NotificationChannelCompat> getChannels() {
return mChannels;
}
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompat.java b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
index d1317ab..2459494 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
@@ -64,8 +64,6 @@
import androidx.annotation.DimenRes;
import androidx.annotation.Dimension;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.R;
@@ -76,6 +74,9 @@
import androidx.core.text.BidiFormatter;
import androidx.core.view.GravityCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.text.NumberFormat;
@@ -1020,8 +1021,7 @@
public ArrayList<Action> mActions = new ArrayList<>();
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @NonNull
- public ArrayList<Person> mPersonList = new ArrayList<>();
+ public @NonNull ArrayList<Person> mPersonList = new ArrayList<>();
// Invisible actions are stored in the CarExtender bundle without actually being owned by
// CarExtender. This is to comply with an optimization of the Android OS which removes
@@ -1197,8 +1197,7 @@
}
/** Remove all extras which have been parsed by the rest of the copy process */
- @Nullable
- private static Bundle getExtrasWithoutDuplicateData(
+ private static @Nullable Bundle getExtrasWithoutDuplicateData(
@NonNull Notification notification, @Nullable Style style) {
if (notification.extras == null) {
return null;
@@ -1439,8 +1438,7 @@
*
* <p>Prior to {@link Build.VERSION_CODES#O} this field has no effect.
*/
- @NonNull
- public Builder setSettingsText(@Nullable CharSequence text) {
+ public @NonNull Builder setSettingsText(@Nullable CharSequence text) {
mSettingsText = limitCharSequenceLength(text);
return this;
}
@@ -1459,7 +1457,7 @@
* <p>Note: The reply text will only be shown on notifications that have least one action
* with a {@code RemoteInput}.</p>
*/
- public @NonNull Builder setRemoteInputHistory(@Nullable CharSequence[] text) {
+ public @NonNull Builder setRemoteInputHistory(CharSequence @Nullable [] text) {
mRemoteInputHistory = text;
return this;
}
@@ -1554,8 +1552,7 @@
* even if other notifications are suppressed.
*/
@SuppressWarnings("deprecation")
- @NonNull
- public Builder setFullScreenIntent(@Nullable PendingIntent intent,
+ public @NonNull Builder setFullScreenIntent(@Nullable PendingIntent intent,
boolean highPriority) {
mFullScreenIntent = intent;
setFlag(FLAG_HIGH_PRIORITY, highPriority);
@@ -1686,7 +1683,7 @@
* @see android.os.Vibrator for a discussion of the <code>pattern</code>
* parameter.
*/
- public @NonNull Builder setVibrate(@Nullable long[] pattern) {
+ public @NonNull Builder setVibrate(long @Nullable [] pattern) {
mNotification.vibrate = pattern;
return this;
}
@@ -1910,7 +1907,7 @@
* @param person the person to add.
* @see #EXTRA_PEOPLE_LIST
*/
- public @NonNull Builder addPerson(@Nullable final Person person) {
+ public @NonNull Builder addPerson(final @Nullable Person person) {
if (person != null) {
mPersonList.add(person);
}
@@ -2086,8 +2083,7 @@
* @param intent {@link android.app.PendingIntent} to be fired when the action is invoked.
*/
@RequiresApi(21)
- @NonNull
- public Builder addInvisibleAction(int icon, @Nullable CharSequence title,
+ public @NonNull Builder addInvisibleAction(int icon, @Nullable CharSequence title,
@Nullable PendingIntent intent) {
mInvisibleActions.add(new Action(icon, title, intent));
return this;
@@ -2395,7 +2391,7 @@
* {@link #setContentTitle(CharSequence) contentTitle} if they were empty.
*
*/
- public @NonNull Builder setShortcutInfo(@Nullable final ShortcutInfoCompat shortcutInfo) {
+ public @NonNull Builder setShortcutInfo(final @Nullable ShortcutInfoCompat shortcutInfo) {
// TODO: b/156784300 add check to filter long-lived and sharing shortcut
if (shortcutInfo == null) {
return this;
@@ -2423,7 +2419,7 @@
* {@link android.view.contentcapture.ContentCaptureContext}) so the device's intelligence
* services can correlate them.
*/
- public @NonNull Builder setLocusId(@Nullable final LocusIdCompat locusId) {
+ public @NonNull Builder setLocusId(final @Nullable LocusIdCompat locusId) {
mLocusId = locusId;
return this;
}
@@ -2476,8 +2472,8 @@
* {@link android.os.Build.VERSION_CODES#S}.
*/
@SuppressWarnings("MissingGetterMatchingBuilder") // no underlying getter in platform API
- @NonNull
- public Builder setForegroundServiceBehavior(@ServiceNotificationBehavior int behavior) {
+ public @NonNull Builder setForegroundServiceBehavior(
+ @ServiceNotificationBehavior int behavior) {
mFgsDeferBehavior = behavior;
return this;
}
@@ -2726,9 +2722,8 @@
/**
*/
- @Nullable
@RestrictTo(LIBRARY_GROUP_PREFIX)
- protected String getClassName() {
+ protected @Nullable String getClassName() {
// We can't crash for apps that write their own subclasses, so we return null
return null;
}
@@ -2818,8 +2813,8 @@
/**
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @Nullable
- public static Style extractStyleFromNotification(@NonNull Notification notification) {
+ public static @Nullable Style extractStyleFromNotification(
+ @NonNull Notification notification) {
Bundle extras = NotificationCompat.getExtras(notification);
if (extras == null) {
return null;
@@ -2827,8 +2822,7 @@
return constructStyleForExtras(extras);
}
- @Nullable
- private static Style constructCompatStyleByPlatformName(
+ private static @Nullable Style constructCompatStyleByPlatformName(
@Nullable String platformTemplateClass) {
if (platformTemplateClass == null) {
return null;
@@ -2854,8 +2848,7 @@
return null;
}
- @Nullable
- static Style constructCompatStyleByName(@Nullable String templateClass) {
+ static @Nullable Style constructCompatStyleByName(@Nullable String templateClass) {
if (templateClass != null) {
switch (templateClass) {
case BigTextStyle.TEMPLATE_CLASS_NAME:
@@ -2875,8 +2868,7 @@
return null;
}
- @Nullable
- static Style constructCompatStyleForBundle(@NonNull Bundle extras) {
+ static @Nullable Style constructCompatStyleForBundle(@NonNull Bundle extras) {
// If the compat template name provided in the bundle can be resolved to a class, use
// that style class.
Style style = constructCompatStyleByName(extras.getString(EXTRA_COMPAT_TEMPLATE));
@@ -2903,8 +2895,7 @@
return constructCompatStyleByPlatformName(extras.getString(EXTRA_TEMPLATE));
}
- @Nullable
- static Style constructStyleForExtras(@NonNull Bundle extras) {
+ static @Nullable Style constructStyleForExtras(@NonNull Bundle extras) {
final Style style = constructCompatStyleForBundle(extras);
if (style == null) {
return null;
@@ -2920,8 +2911,7 @@
/**
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @NonNull
- public RemoteViews applyStandardTemplate(boolean showSmallIcon,
+ public @NonNull RemoteViews applyStandardTemplate(boolean showSmallIcon,
int resId, boolean fitIn1U) {
Resources res = mBuilder.mContext.getResources();
RemoteViews contentView = new RemoteViews(mBuilder.mContext.getPackageName(), resId);
@@ -3237,8 +3227,7 @@
* Set the content description of the big picture.
*/
@RequiresApi(31)
- @NonNull
- public BigPictureStyle setContentDescription(
+ public @NonNull BigPictureStyle setContentDescription(
@Nullable CharSequence contentDescription) {
mPictureContentDescription = contentDescription;
return this;
@@ -3268,8 +3257,7 @@
* state of this notification.
*/
@RequiresApi(31)
- @NonNull
- public BigPictureStyle showBigPictureWhenCollapsed(boolean show) {
+ public @NonNull BigPictureStyle showBigPictureWhenCollapsed(boolean show) {
mShowBigPictureWhenCollapsed = show;
return this;
}
@@ -3297,8 +3285,7 @@
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
- @NonNull
- protected String getClassName() {
+ protected @NonNull String getClassName() {
return TEMPLATE_CLASS_NAME;
}
@@ -3373,8 +3360,7 @@
/**
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @Nullable
- public static IconCompat getPictureIcon(@Nullable Bundle extras) {
+ public static @Nullable IconCompat getPictureIcon(@Nullable Bundle extras) {
if (extras == null) return null;
// When this style adds a picture, we only add one of the keys. If both were added,
// it would most likely be a legacy app trying to override the picture in some way.
@@ -3387,8 +3373,7 @@
}
}
- @Nullable
- private static IconCompat asIconCompat(@Nullable Parcelable bitmapOrIcon) {
+ private static @Nullable IconCompat asIconCompat(@Nullable Parcelable bitmapOrIcon) {
if (bitmapOrIcon != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (bitmapOrIcon instanceof Icon) {
@@ -3537,8 +3522,7 @@
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
- @NonNull
- protected String getClassName() {
+ protected @NonNull String getClassName() {
return TEMPLATE_CLASS_NAME;
}
@@ -3677,8 +3661,7 @@
* @deprecated Use {@link #getUser()} instead.
*/
@Deprecated
- @Nullable
- public CharSequence getUserDisplayName() {
+ public @Nullable CharSequence getUserDisplayName() {
return mUser.getName();
}
@@ -3710,8 +3693,7 @@
/**
* Return the title to be displayed on this conversation. Can be {@code null}.
*/
- @Nullable
- public CharSequence getConversationTitle() {
+ public @Nullable CharSequence getConversationTitle() {
return mConversationTitle;
}
@@ -3734,8 +3716,7 @@
* {@link #addMessage(Message)}
*/
@Deprecated
- @NonNull
- public MessagingStyle addMessage(@Nullable CharSequence text, long timestamp,
+ public @NonNull MessagingStyle addMessage(@Nullable CharSequence text, long timestamp,
@Nullable CharSequence sender) {
mMessages.add(
new Message(text, timestamp, new Person.Builder().setName(sender).build()));
@@ -3869,8 +3850,7 @@
* @return {@code null} if there is no {@link MessagingStyle} set, or if the SDK version is
* < {@code 16} (JellyBean).
*/
- @Nullable
- public static MessagingStyle extractMessagingStyleFromNotification(
+ public static @Nullable MessagingStyle extractMessagingStyleFromNotification(
@NonNull Notification notification) {
Style style = Style.extractStyleFromNotification(notification);
if (style instanceof MessagingStyle) {
@@ -3883,8 +3863,7 @@
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
- @NonNull
- protected String getClassName() {
+ protected @NonNull String getClassName() {
return TEMPLATE_CLASS_NAME;
}
@@ -3980,8 +3959,7 @@
}
}
- @Nullable
- private Message findLatestIncomingMessage() {
+ private @Nullable Message findLatestIncomingMessage() {
for (int i = mMessages.size() - 1; i >= 0; i--) {
Message message = mMessages.get(i);
// Incoming messages have a non-empty sender.
@@ -4031,8 +4009,7 @@
return sb;
}
- @NonNull
- private TextAppearanceSpan makeFontColorSpan(int color) {
+ private @NonNull TextAppearanceSpan makeFontColorSpan(int color) {
return new TextAppearanceSpan(null, 0, 0, ColorStateList.valueOf(color), null);
}
@@ -4122,11 +4099,11 @@
private final CharSequence mText;
private final long mTimestamp;
- @Nullable private final Person mPerson;
+ private final @Nullable Person mPerson;
private Bundle mExtras = new Bundle();
- @Nullable private String mDataMimeType;
- @Nullable private Uri mDataUri;
+ private @Nullable String mDataMimeType;
+ private @Nullable Uri mDataUri;
/**
* Creates a new {@link Message} with the given text, timestamp, and sender.
@@ -4203,8 +4180,7 @@
* Get the text to be used for this message, or the fallback text if a type and content
* Uri have been set
*/
- @Nullable
- public CharSequence getText() {
+ public @Nullable CharSequence getText() {
return mText;
}
@@ -4214,8 +4190,7 @@
}
/** Get the extras Bundle for this message. */
- @NonNull
- public Bundle getExtras() {
+ public @NonNull Bundle getExtras() {
return mExtras;
}
@@ -4225,20 +4200,17 @@
* @deprecated Use {@link #getPerson()}
*/
@Deprecated
- @Nullable
- public CharSequence getSender() {
+ public @Nullable CharSequence getSender() {
return mPerson == null ? null : mPerson.getName();
}
/** Returns the {@link Person} sender of this message. */
- @Nullable
- public Person getPerson() {
+ public @Nullable Person getPerson() {
return mPerson;
}
/** Get the MIME type of the data pointed to by the URI. */
- @Nullable
- public String getDataMimeType() {
+ public @Nullable String getDataMimeType() {
return mDataMimeType;
}
@@ -4246,8 +4218,7 @@
* Get the the Uri pointing to the content of the message. Can be null, in which case
* {@see #getText()} is used.
*/
- @Nullable
- public Uri getDataUri() {
+ public @Nullable Uri getDataUri() {
return mDataUri;
}
@@ -4282,8 +4253,7 @@
return bundle;
}
- @NonNull
- static Bundle[] getBundleArrayForMessages(@NonNull List<Message> messages) {
+ static Bundle @NonNull [] getBundleArrayForMessages(@NonNull List<Message> messages) {
Bundle[] bundles = new Bundle[messages.size()];
final int N = messages.size();
for (int i = 0; i < N; i++) {
@@ -4292,8 +4262,8 @@
return bundles;
}
- @NonNull
- static List<Message> getMessagesFromBundleArray(@NonNull Parcelable[] bundles) {
+ static @NonNull List<Message> getMessagesFromBundleArray(
+ Parcelable @NonNull [] bundles) {
List<Message> messages = new ArrayList<>(bundles.length);
for (int i = 0; i < bundles.length; i++) {
if (bundles[i] instanceof Bundle) {
@@ -4306,9 +4276,8 @@
return messages;
}
- @Nullable
@SuppressWarnings("deprecation")
- static Message getMessageFromBundle(@NonNull Bundle bundle) {
+ static @Nullable Message getMessageFromBundle(@NonNull Bundle bundle) {
try {
if (!bundle.containsKey(KEY_TEXT) || !bundle.containsKey(KEY_TIMESTAMP)) {
return null;
@@ -4354,9 +4323,8 @@
* {@link Notification.MessagingStyle.Message}.
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @NonNull
@RequiresApi(24)
- Notification.MessagingStyle.Message toAndroidMessage() {
+ Notification.MessagingStyle.@NonNull Message toAndroidMessage() {
Notification.MessagingStyle.Message frameworkMessage;
Person person = getPerson();
// Use Person for P and above
@@ -4590,8 +4558,7 @@
* @param declineIntent the intent to be sent when the user taps the decline action
* @param answerIntent the intent to be sent when the user taps the answer action
*/
- @NonNull
- public static CallStyle forIncomingCall(@NonNull Person person,
+ public static @NonNull CallStyle forIncomingCall(@NonNull Person person,
@NonNull PendingIntent declineIntent, @NonNull PendingIntent answerIntent) {
return new CallStyle(CALL_TYPE_INCOMING, person,
null /* hangUpIntent */,
@@ -4610,8 +4577,7 @@
* the person also needs to have a non-empty name associated with it
* @param hangUpIntent the intent to be sent when the user taps the hang up action
*/
- @NonNull
- public static CallStyle forOngoingCall(@NonNull Person person,
+ public static @NonNull CallStyle forOngoingCall(@NonNull Person person,
@NonNull PendingIntent hangUpIntent) {
return new CallStyle(CALL_TYPE_ONGOING, person,
requireNonNull(hangUpIntent, "hangUpIntent is required"),
@@ -4632,8 +4598,7 @@
* @param hangUpIntent the intent to be sent when the user taps the hang up action
* @param answerIntent the intent to be sent when the user taps the answer action
*/
- @NonNull
- public static CallStyle forScreeningCall(@NonNull Person person,
+ public static @NonNull CallStyle forScreeningCall(@NonNull Person person,
@NonNull PendingIntent hangUpIntent, @NonNull PendingIntent answerIntent) {
return new CallStyle(CALL_TYPE_SCREENING, person,
requireNonNull(hangUpIntent, "hangUpIntent is required"),
@@ -4667,8 +4632,7 @@
* Sets whether the call is a video call, which may affect the icons or text used on the
* required action buttons.
*/
- @NonNull
- public CallStyle setIsVideo(boolean isVideo) {
+ public @NonNull CallStyle setIsVideo(boolean isVideo) {
mIsVideo = isVideo;
return this;
}
@@ -4678,8 +4642,7 @@
* text} as a verification status of the caller.
*/
@RequiresApi(23)
- @NonNull
- public CallStyle setVerificationIcon(@Nullable Icon verificationIcon) {
+ public @NonNull CallStyle setVerificationIcon(@Nullable Icon verificationIcon) {
mVerificationIcon = verificationIcon == null ? null :
IconCompat.createFromIcon(verificationIcon);
return this;
@@ -4689,8 +4652,7 @@
* Sets an optional icon to be displayed with {@link #setVerificationText(CharSequence)
* text} as a verification status of the caller.
*/
- @NonNull
- public CallStyle setVerificationIcon(@Nullable Bitmap verificationIcon) {
+ public @NonNull CallStyle setVerificationIcon(@Nullable Bitmap verificationIcon) {
mVerificationIcon = IconCompat.createWithBitmap(verificationIcon);
return this;
}
@@ -4699,8 +4661,7 @@
* Sets optional text to be displayed with an {@link #setVerificationIcon(Icon) icon}
* as a verification status of the caller.
*/
- @NonNull
- public CallStyle setVerificationText(@Nullable CharSequence verificationText) {
+ public @NonNull CallStyle setVerificationText(@Nullable CharSequence verificationText) {
mVerificationText = verificationText;
return this;
}
@@ -4710,8 +4671,7 @@
* The system may change this color to ensure sufficient contrast with the background.
* The system may choose to disregard this hint if the notification is not colorized.
*/
- @NonNull
- public CallStyle setAnswerButtonColorHint(@ColorInt int color) {
+ public @NonNull CallStyle setAnswerButtonColorHint(@ColorInt int color) {
mAnswerButtonColor = color;
return this;
}
@@ -4722,8 +4682,7 @@
* The system may change this color to ensure sufficient contrast with the background.
* The system may choose to disregard this hint if the notification is not colorized.
*/
- @NonNull
- public CallStyle setDeclineButtonColorHint(@ColorInt int color) {
+ public @NonNull CallStyle setDeclineButtonColorHint(@ColorInt int color) {
mDeclineButtonColor = color;
return this;
}
@@ -4818,8 +4777,7 @@
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
- @NonNull
- protected String getClassName() {
+ protected @NonNull String getClassName() {
return TEMPLATE_CLASS_NAME;
}
@@ -4915,8 +4873,7 @@
* Provides the default text for a CallStyle notification. Corresponds to Notification
* .CallStyle
*/
- @Nullable
- private String getDefaultText() {
+ private @Nullable String getDefaultText() {
switch (mCallType) {
case CALL_TYPE_INCOMING:
return mBuilder.mContext.getResources().getString(
@@ -4931,9 +4888,8 @@
return null;
}
- @NonNull
@RequiresApi(20)
- private Action makeNegativeAction() {
+ private @NonNull Action makeNegativeAction() {
int icon = R.drawable.ic_call_decline_low;
if (Build.VERSION.SDK_INT >= 21) {
icon = R.drawable.ic_call_decline;
@@ -4951,9 +4907,8 @@
}
}
- @Nullable
@RequiresApi(20)
- private Action makeAnswerAction() {
+ private @Nullable Action makeAnswerAction() {
int videoIcon = R.drawable.ic_call_answer_video_low;
int icon = R.drawable.ic_call_answer_low;
if (Build.VERSION.SDK_INT >= 21) {
@@ -4969,10 +4924,9 @@
mAnswerIntent);
}
- @NonNull
@RequiresApi(20)
- private Action makeAction(int icon, int title, Integer colorInt, int defaultColorRes,
- PendingIntent intent) {
+ private @NonNull Action makeAction(int icon, int title, Integer colorInt,
+ int defaultColorRes, PendingIntent intent) {
if (colorInt == null) {
colorInt = ContextCompat.getColor(mBuilder.mContext, defaultColorRes);
}
@@ -5001,9 +4955,8 @@
*
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @NonNull
@RequiresApi(20)
- public ArrayList<Action> getActionsListWithSystemActions() {
+ public @NonNull ArrayList<Action> getActionsListWithSystemActions() {
// Define the system actions we expect to see.
final Action firstAction = makeNegativeAction();
final Action lastAction = makeAnswerAction();
@@ -5173,17 +5126,17 @@
private Api31Impl() {
}
- static Notification.CallStyle forIncomingCall(@NonNull android.app.Person person,
+ static Notification.CallStyle forIncomingCall(android.app.@NonNull Person person,
@NonNull PendingIntent declineIntent, @NonNull PendingIntent answerIntent) {
return Notification.CallStyle.forIncomingCall(person, declineIntent, answerIntent);
}
- static Notification.CallStyle forOngoingCall(@NonNull android.app.Person person,
+ static Notification.CallStyle forOngoingCall(android.app.@NonNull Person person,
@NonNull PendingIntent hangUpIntent) {
return Notification.CallStyle.forOngoingCall(person, hangUpIntent);
}
- static Notification.CallStyle forScreeningCall(@NonNull android.app.Person person,
+ static Notification.CallStyle forScreeningCall(android.app.@NonNull Person person,
@NonNull PendingIntent hangUpIntent, @NonNull PendingIntent answerIntent) {
return Notification.CallStyle.forScreeningCall(person, hangUpIntent, answerIntent);
}
@@ -5290,8 +5243,7 @@
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
- @NonNull
- protected String getClassName() {
+ protected @NonNull String getClassName() {
return TEMPLATE_CLASS_NAME;
}
@@ -5376,8 +5328,7 @@
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
- @NonNull
- protected String getClassName() {
+ protected @NonNull String getClassName() {
return TEMPLATE_CLASS_NAME;
}
@@ -5477,10 +5428,9 @@
* {@link Notification#contentView} when no big content view has set, or
* {@link Notification#headsUpContentView} when set. Otherwise, returns the empty list.
*/
- @NonNull
@SuppressWarnings("MixedMutabilityReturnType")
@RequiresApi(24)
- public static List<CharSequence> getTextsFromContentView(@NonNull Context context,
+ public static @NonNull List<CharSequence> getTextsFromContentView(@NonNull Context context,
@NonNull Notification notification) {
final String styleClassName = notification.extras.getString(EXTRA_TEMPLATE);
if (!Notification.DecoratedCustomViewStyle.class.getName().equals(styleClassName)) {
@@ -5689,7 +5639,7 @@
static final String EXTRA_SEMANTIC_ACTION = "android.support.action.semanticAction";
final Bundle mExtras;
- @Nullable private IconCompat mIcon;
+ private @Nullable IconCompat mIcon;
private final RemoteInput[] mRemoteInputs;
/**
@@ -5744,10 +5694,10 @@
}
Action(int icon, @Nullable CharSequence title, @Nullable PendingIntent intent,
- @Nullable Bundle extras,
- @Nullable RemoteInput[] remoteInputs, @Nullable RemoteInput[] dataOnlyRemoteInputs,
- boolean allowGeneratedReplies, @SemanticAction int semanticAction,
- boolean showsUserInterface, boolean isContextual, boolean requireAuth) {
+ @Nullable Bundle extras, RemoteInput @Nullable [] remoteInputs,
+ RemoteInput @Nullable [] dataOnlyRemoteInputs, boolean allowGeneratedReplies,
+ @SemanticAction int semanticAction, boolean showsUserInterface,
+ boolean isContextual, boolean requireAuth) {
this(icon == 0 ? null : IconCompat.createWithResource(null, "", icon), title,
intent, extras, remoteInputs, dataOnlyRemoteInputs, allowGeneratedReplies,
semanticAction, showsUserInterface, isContextual, requireAuth);
@@ -5757,9 +5707,10 @@
@SuppressWarnings("deprecation")
Action(@Nullable IconCompat icon, @Nullable CharSequence title,
@Nullable PendingIntent intent, @Nullable Bundle extras,
- @Nullable RemoteInput[] remoteInputs, @Nullable RemoteInput[] dataOnlyRemoteInputs,
- boolean allowGeneratedReplies, @SemanticAction int semanticAction,
- boolean showsUserInterface, boolean isContextual, boolean requireAuth) {
+ RemoteInput @Nullable [] remoteInputs,
+ RemoteInput @Nullable [] dataOnlyRemoteInputs, boolean allowGeneratedReplies,
+ @SemanticAction int semanticAction, boolean showsUserInterface,
+ boolean isContextual, boolean requireAuth) {
this.mIcon = icon;
if (icon != null && icon.getType() == IconCompat.TYPE_RESOURCE) {
this.icon = icon.getResId();
@@ -5835,7 +5786,7 @@
* May return null if no remote inputs were added. Only returns inputs which accept
* a text input. For inputs which only accept data use {@link #getDataOnlyRemoteInputs}.
*/
- public @Nullable RemoteInput[] getRemoteInputs() {
+ public RemoteInput @Nullable [] getRemoteInputs() {
return mRemoteInputs;
}
@@ -5869,7 +5820,7 @@
* <p>This method exists so that legacy RemoteInput collectors that pre-date the addition
* of non-textual RemoteInputs do not access these remote inputs.
*/
- public @Nullable RemoteInput[] getDataOnlyRemoteInputs() {
+ public RemoteInput @Nullable [] getDataOnlyRemoteInputs() {
return mDataOnlyRemoteInputs;
}
@@ -5901,8 +5852,7 @@
*
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @NonNull
- public static Builder fromAndroidAction(@NonNull Notification.Action action) {
+ public static @NonNull Builder fromAndroidAction(Notification.@NonNull Action action) {
final Builder builder;
if (Build.VERSION.SDK_INT >= 23 && Api23Impl.getIcon(action) != null) {
IconCompat iconCompat = IconCompat.createFromIconOrNullIfZeroResId(
@@ -5986,7 +5936,7 @@
private Builder(@Nullable IconCompat icon, @Nullable CharSequence title,
@Nullable PendingIntent intent, @NonNull Bundle extras,
- @Nullable RemoteInput[] remoteInputs, boolean allowGeneratedReplies,
+ RemoteInput @Nullable [] remoteInputs, boolean allowGeneratedReplies,
@SemanticAction int semanticAction, boolean showsUserInterface,
boolean isContextual, boolean authRequired) {
mIcon = icon;
@@ -6088,8 +6038,7 @@
* If this is false and the device is locked, the OS will decide whether authentication
* should be required.
*/
- @NonNull
- public Builder setAuthenticationRequired(boolean authenticationRequired) {
+ public @NonNull Builder setAuthenticationRequired(boolean authenticationRequired) {
mAuthenticationRequired = authenticationRequired;
return this;
}
@@ -6329,7 +6278,7 @@
* method of {@link NotificationCompat.Action.Builder}.
*/
@Override
- public @NonNull Action.Builder extend(@NonNull Action.Builder builder) {
+ public Action.@NonNull Builder extend(Action.@NonNull Builder builder) {
Bundle wearableBundle = new Bundle();
if (mFlags != DEFAULT_FLAGS) {
@@ -6440,8 +6389,7 @@
* @deprecated This method has no effect starting with Wear 2.0.
*/
@Deprecated
- @Nullable
- public CharSequence getConfirmLabel() {
+ public @Nullable CharSequence getConfirmLabel() {
return mConfirmLabel;
}
@@ -6802,8 +6750,8 @@
*/
@SuppressWarnings("deprecation")
@Override
- @NonNull
- public NotificationCompat.Builder extend(@NonNull NotificationCompat.Builder builder) {
+ public NotificationCompat.@NonNull Builder extend(
+ NotificationCompat.@NonNull Builder builder) {
Bundle wearableBundle = new Bundle();
if (!mActions.isEmpty()) {
@@ -6909,8 +6857,7 @@
}
@Override
- @NonNull
- public WearableExtender clone() {
+ public @NonNull WearableExtender clone() {
WearableExtender that = new WearableExtender();
that.mActions = new ArrayList<>(this.mActions);
that.mFlags = this.mFlags;
@@ -7381,8 +7328,7 @@
* @deprecated This method has no effect starting with Wear 2.0.
*/
@Deprecated
- @NonNull
- public WearableExtender setHintAvoidBackgroundClipping(
+ public @NonNull WearableExtender setHintAvoidBackgroundClipping(
boolean hintAvoidBackgroundClipping) {
setFlag(FLAG_HINT_AVOID_BACKGROUND_CLIPPING, hintAvoidBackgroundClipping);
return this;
@@ -7787,8 +7733,8 @@
* method of {@link NotificationCompat.Builder}.
*/
@Override
- @NonNull
- public NotificationCompat.Builder extend(@NonNull NotificationCompat.Builder builder) {
+ public NotificationCompat.@NonNull Builder extend(
+ NotificationCompat.@NonNull Builder builder) {
if (Build.VERSION.SDK_INT < 21) {
return builder;
}
@@ -7868,8 +7814,8 @@
* instead.
*/
@Deprecated
- @NonNull
- public CarExtender setUnreadConversation(@Nullable UnreadConversation unreadConversation) {
+ public @NonNull CarExtender setUnreadConversation(
+ @Nullable UnreadConversation unreadConversation) {
mUnreadConversation = unreadConversation;
return this;
}
@@ -7901,10 +7847,10 @@
private final String[] mParticipants;
private final long mLatestTimestamp;
- UnreadConversation(@Nullable String[] messages, @Nullable RemoteInput remoteInput,
+ UnreadConversation(String @Nullable [] messages, @Nullable RemoteInput remoteInput,
@Nullable PendingIntent replyPendingIntent,
@Nullable PendingIntent readPendingIntent,
- @Nullable String[] participants, long latestTimestamp) {
+ String @Nullable [] participants, long latestTimestamp) {
mMessages = messages;
mRemoteInput = remoteInput;
mReadPendingIntent = readPendingIntent;
@@ -7916,7 +7862,7 @@
/**
* Gets the list of messages conveyed by this notification.
*/
- public @Nullable String[] getMessages() {
+ public String @Nullable [] getMessages() {
return mMessages;
}
@@ -7947,7 +7893,7 @@
/**
* Gets the participants in the conversation.
*/
- public @Nullable String[] getParticipants() {
+ public String @Nullable [] getParticipants() {
return mParticipants;
}
@@ -8231,8 +8177,8 @@
* method of {@link NotificationCompat.Builder}.
*/
@Override
- @NonNull
- public NotificationCompat.Builder extend(@NonNull NotificationCompat.Builder builder) {
+ public NotificationCompat.@NonNull Builder extend(
+ NotificationCompat.@NonNull Builder builder) {
// TvExtender was introduced in API level 26; note that before API level 26, the extras
// added by TvExtender are not expected to be used; thus, we avoid setting them to save
// memory.
@@ -8418,8 +8364,7 @@
* null if this bubble is created via {@link Builder#Builder(String)}.
*/
@SuppressLint("InvalidNullConversion")
- @Nullable
- public PendingIntent getIntent() {
+ public @Nullable PendingIntent getIntent() {
return mPendingIntent;
}
@@ -8428,16 +8373,14 @@
* {@link Builder#Builder(String)} or null if created via
* {@link Builder#Builder(PendingIntent, IconCompat)}.
*/
- @Nullable
- public String getShortcutId() {
+ public @Nullable String getShortcutId() {
return mShortcutId;
}
/**
* @return the pending intent to send when the bubble is dismissed by a user, if one exists.
*/
- @Nullable
- public PendingIntent getDeleteIntent() {
+ public @Nullable PendingIntent getDeleteIntent() {
return mDeleteIntent;
}
@@ -8446,8 +8389,7 @@
* if the bubble is created via {@link Builder#Builder(String)}.
*/
@SuppressLint("InvalidNullConversion")
- @Nullable
- public IconCompat getIcon() {
+ public @Nullable IconCompat getIcon() {
return mIcon;
}
@@ -8502,7 +8444,7 @@
* @return a {@link Notification.BubbleMetadata} containing the same data if compatMetadata
* is non-null, otherwise null.
*/
- public static @Nullable android.app.Notification.BubbleMetadata toPlatform(
+ public static android.app.Notification.@Nullable BubbleMetadata toPlatform(
@Nullable BubbleMetadata compatMetadata) {
if (compatMetadata == null) {
return null;
@@ -8524,7 +8466,7 @@
* platformMetadata is non-null, otherwise null.
*/
public static @Nullable BubbleMetadata fromPlatform(
- @Nullable android.app.Notification.BubbleMetadata platformMetadata) {
+ android.app.Notification.@Nullable BubbleMetadata platformMetadata) {
if (platformMetadata == null) {
return null;
}
@@ -8620,8 +8562,7 @@
* @throws IllegalStateException if this builder was created via
* {@link #Builder(String)}.
*/
- @NonNull
- public BubbleMetadata.Builder setIntent(@NonNull PendingIntent intent) {
+ public BubbleMetadata.@NonNull Builder setIntent(@NonNull PendingIntent intent) {
if (mShortcutId != null) {
throw new IllegalStateException("Created as a shortcut bubble, cannot set a "
+ "PendingIntent. Consider using "
@@ -8648,8 +8589,7 @@
* @throws IllegalStateException if this builder was created via
* {@link #Builder(String)}.
*/
- @NonNull
- public BubbleMetadata.Builder setIcon(@NonNull IconCompat icon) {
+ public BubbleMetadata.@NonNull Builder setIcon(@NonNull IconCompat icon) {
if (mShortcutId != null) {
throw new IllegalStateException("Created as a shortcut bubble, cannot set an "
+ "Icon. Consider using "
@@ -8671,8 +8611,8 @@
* previous value set will be cleared after calling this method, and this value will
* be used instead.
*/
- @NonNull
- public BubbleMetadata.Builder setDesiredHeight(@Dimension(unit = DP) int height) {
+ public BubbleMetadata.@NonNull Builder setDesiredHeight(
+ @Dimension(unit = DP) int height) {
mDesiredHeight = Math.max(height, 0);
mDesiredHeightResId = 0;
return this;
@@ -8687,8 +8627,8 @@
* previous value set will be cleared after calling this method, and this value will
* be used instead.
*/
- @NonNull
- public BubbleMetadata.Builder setDesiredHeightResId(@DimenRes int heightResId) {
+ public BubbleMetadata.@NonNull Builder setDesiredHeightResId(
+ @DimenRes int heightResId) {
mDesiredHeightResId = heightResId;
mDesiredHeight = 0;
return this;
@@ -8705,8 +8645,7 @@
* <p>Generally this flag should only be set if the user has performed an action to
* request or create a bubble.</p>
*/
- @NonNull
- public BubbleMetadata.Builder setAutoExpandBubble(boolean shouldExpand) {
+ public BubbleMetadata.@NonNull Builder setAutoExpandBubble(boolean shouldExpand) {
setFlag(FLAG_AUTO_EXPAND_BUBBLE, shouldExpand);
return this;
}
@@ -8722,8 +8661,7 @@
* request or create a bubble, or if the user has seen the content in the notification
* and the notification is no longer relevant.</p>
*/
- @NonNull
- public BubbleMetadata.Builder setSuppressNotification(
+ public BubbleMetadata.@NonNull Builder setSuppressNotification(
boolean shouldSuppressNotif) {
setFlag(FLAG_SUPPRESS_NOTIFICATION, shouldSuppressNotif);
return this;
@@ -8732,8 +8670,8 @@
/**
* Sets an optional intent to send when this bubble is explicitly removed by the user.
*/
- @NonNull
- public BubbleMetadata.Builder setDeleteIntent(@Nullable PendingIntent deleteIntent) {
+ public BubbleMetadata.@NonNull Builder setDeleteIntent(
+ @Nullable PendingIntent deleteIntent) {
mDeleteIntent = deleteIntent;
return this;
}
@@ -8743,8 +8681,7 @@
* <p>Will throw {@link NullPointerException} if required fields have not been set
* on this builder.</p>
*/
- @NonNull
- public BubbleMetadata build() {
+ public @NonNull BubbleMetadata build() {
if (mShortcutId == null && mPendingIntent == null) {
throw new NullPointerException(
"Must supply pending intent or shortcut to bubble");
@@ -8759,8 +8696,7 @@
return data;
}
- @NonNull
- private BubbleMetadata.Builder setFlag(int mask, boolean value) {
+ private BubbleMetadata.@NonNull Builder setFlag(int mask, boolean value) {
if (value) {
mFlags |= mask;
} else {
@@ -8785,7 +8721,7 @@
* compatMetadata is non-null, otherwise null.
*/
@RequiresApi(29)
- @Nullable static android.app.Notification.BubbleMetadata toPlatform(
+ static android.app.Notification.@Nullable BubbleMetadata toPlatform(
@Nullable BubbleMetadata compatMetadata) {
if (compatMetadata == null) {
return null;
@@ -8824,8 +8760,8 @@
* platformMetadata is non-null, otherwise null.
*/
@RequiresApi(29)
- @Nullable static BubbleMetadata fromPlatform(
- @Nullable android.app.Notification.BubbleMetadata platformMetadata) {
+ static @Nullable BubbleMetadata fromPlatform(
+ android.app.Notification.@Nullable BubbleMetadata platformMetadata) {
if (platformMetadata == null) {
return null;
}
@@ -8868,7 +8804,7 @@
* compatMetadata is non-null, otherwise null.
*/
@RequiresApi(30)
- @Nullable static android.app.Notification.BubbleMetadata toPlatform(
+ static android.app.Notification.@Nullable BubbleMetadata toPlatform(
@Nullable BubbleMetadata compatMetadata) {
if (compatMetadata == null) {
return null;
@@ -8909,8 +8845,8 @@
* platformMetadata is non-null, otherwise null.
*/
@RequiresApi(30)
- @Nullable static BubbleMetadata fromPlatform(
- @Nullable android.app.Notification.BubbleMetadata platformMetadata) {
+ static @Nullable BubbleMetadata fromPlatform(
+ android.app.Notification.@Nullable BubbleMetadata platformMetadata) {
if (platformMetadata == null) {
return null;
}
@@ -8947,7 +8883,7 @@
* to do an array copy.
*/
@SuppressWarnings("deprecation")
- static @NonNull Notification[] getNotificationArrayFromBundle(@NonNull Bundle bundle,
+ static Notification @NonNull [] getNotificationArrayFromBundle(@NonNull Bundle bundle,
@NonNull String key) {
Parcelable[] array = bundle.getParcelableArray(key);
if (array instanceof Notification[] || array == null) {
@@ -8969,8 +8905,7 @@
*/
@Deprecated
@androidx.annotation.ReplaceWith(expression = "notification.extras")
- @Nullable
- public static Bundle getExtras(@NonNull Notification notification) {
+ public static @Nullable Bundle getExtras(@NonNull Notification notification) {
return notification.extras;
}
@@ -9023,7 +8958,7 @@
@SuppressWarnings("deprecation")
@RequiresApi(20)
- static @NonNull Action getActionCompatFromAction(@NonNull Notification.Action action) {
+ static @NonNull Action getActionCompatFromAction(Notification.@NonNull Action action) {
final RemoteInput[] remoteInputs;
final android.app.RemoteInput[] srcArray = Api20Impl.getRemoteInputs(action);
if (srcArray == null) {
@@ -9359,8 +9294,7 @@
* {@link androidx.core.content.pm.ShortcutInfoCompat} and
* {@link android.view.contentcapture.ContentCaptureContext}).
*/
- @Nullable
- public static LocusIdCompat getLocusId(@NonNull Notification notification) {
+ public static @Nullable LocusIdCompat getLocusId(@NonNull Notification notification) {
if (Build.VERSION.SDK_INT >= 29) {
LocusId locusId = Api29Impl.getLocusId(notification);
return locusId == null ? null : LocusIdCompat.toLocusIdCompat(locusId);
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java b/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
index 8dbcd23..06cb36d 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
@@ -37,12 +37,13 @@
import android.util.SparseArray;
import android.widget.RemoteViews;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.collection.ArraySet;
import androidx.core.graphics.drawable.IconCompat;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.List;
@@ -291,9 +292,8 @@
}
}
- @Nullable
- private static List<String> combineLists(@Nullable final List<String> first,
- @Nullable final List<String> second) {
+ private static @Nullable List<String> combineLists(final @Nullable List<String> first,
+ final @Nullable List<String> second) {
if (first == null) {
return second;
}
@@ -306,8 +306,7 @@
return new ArrayList<>(people);
}
- @Nullable
- private static List<String> getPeople(@Nullable final List<Person> people) {
+ private static @Nullable List<String> getPeople(final @Nullable List<Person> people) {
if (people == null) {
return null;
}
diff --git a/core/core/src/main/java/androidx/core/app/NotificationManagerCompat.java b/core/core/src/main/java/androidx/core/app/NotificationManagerCompat.java
index 7e005fd..54721fe 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationManagerCompat.java
@@ -50,13 +50,14 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
@@ -211,8 +212,7 @@
public static final int IMPORTANCE_MAX = 5;
/** Get a {@link NotificationManagerCompat} instance for a provided context. */
- @NonNull
- public static NotificationManagerCompat from(@NonNull Context context) {
+ public static @NonNull NotificationManagerCompat from(@NonNull Context context) {
return new NotificationManagerCompat(context);
}
@@ -351,8 +351,7 @@
*
* @return A list of {@link StatusBarNotification}.
*/
- @NonNull
- public List<StatusBarNotification> getActiveNotifications() {
+ public @NonNull List<StatusBarNotification> getActiveNotifications() {
if (Build.VERSION.SDK_INT >= 23) {
return Api23Impl.getActiveNotifications(mNotificationManager);
} else {
@@ -602,8 +601,7 @@
*
* Returns {@code null} on older SDKs which don't support Notification Channels.
*/
- @Nullable
- public NotificationChannel getNotificationChannel(@NonNull String channelId) {
+ public @Nullable NotificationChannel getNotificationChannel(@NonNull String channelId) {
if (Build.VERSION.SDK_INT >= 26) {
return Api26Impl.getNotificationChannel(mNotificationManager, channelId);
}
@@ -615,8 +613,8 @@
*
* Returns {@code null} on older SDKs which don't support Notification Channels.
*/
- @Nullable
- public NotificationChannelCompat getNotificationChannelCompat(@NonNull String channelId) {
+ public @Nullable NotificationChannelCompat getNotificationChannelCompat(
+ @NonNull String channelId) {
if (Build.VERSION.SDK_INT >= 26) {
NotificationChannel channel = getNotificationChannel(channelId);
if (channel != null) {
@@ -634,8 +632,7 @@
*
* Returns {@code null} on older SDKs which don't support Notification Channels.
*/
- @Nullable
- public NotificationChannel getNotificationChannel(@NonNull String channelId,
+ public @Nullable NotificationChannel getNotificationChannel(@NonNull String channelId,
@NonNull String conversationId) {
if (Build.VERSION.SDK_INT >= 30) {
return Api30Impl.getNotificationChannel(mNotificationManager, channelId,
@@ -652,9 +649,8 @@
*
* Returns {@code null} on older SDKs which don't support Notification Channels.
*/
- @Nullable
- public NotificationChannelCompat getNotificationChannelCompat(@NonNull String channelId,
- @NonNull String conversationId) {
+ public @Nullable NotificationChannelCompat getNotificationChannelCompat(
+ @NonNull String channelId, @NonNull String conversationId) {
if (Build.VERSION.SDK_INT >= 26) {
NotificationChannel channel = getNotificationChannel(channelId, conversationId);
if (channel != null) {
@@ -669,8 +665,8 @@
*
* Returns {@code null} on older SDKs which don't support Notification Channels.
*/
- @Nullable
- public NotificationChannelGroup getNotificationChannelGroup(@NonNull String channelGroupId) {
+ public @Nullable NotificationChannelGroup getNotificationChannelGroup(
+ @NonNull String channelGroupId) {
if (Build.VERSION.SDK_INT >= 28) {
return Api28Impl.getNotificationChannelGroup(mNotificationManager, channelGroupId);
} else if (Build.VERSION.SDK_INT >= 26) {
@@ -690,8 +686,7 @@
*
* Returns {@code null} on older SDKs which don't support Notification Channels.
*/
- @Nullable
- public NotificationChannelGroupCompat getNotificationChannelGroupCompat(
+ public @Nullable NotificationChannelGroupCompat getNotificationChannelGroupCompat(
@NonNull String channelGroupId) {
if (Build.VERSION.SDK_INT >= 28) {
NotificationChannelGroup group = getNotificationChannelGroup(channelGroupId);
@@ -711,8 +706,7 @@
* Returns all notification channels belonging to the calling app
* or an empty list on older SDKs which don't support Notification Channels.
*/
- @NonNull
- public List<NotificationChannel> getNotificationChannels() {
+ public @NonNull List<NotificationChannel> getNotificationChannels() {
if (Build.VERSION.SDK_INT >= 26) {
return Api26Impl.getNotificationChannels(mNotificationManager);
}
@@ -723,9 +717,8 @@
* Returns all notification channels belonging to the calling app
* or an empty list on older SDKs which don't support Notification Channels.
*/
- @NonNull
@SuppressWarnings("MixedMutabilityReturnType")
- public List<NotificationChannelCompat> getNotificationChannelsCompat() {
+ public @NonNull List<NotificationChannelCompat> getNotificationChannelsCompat() {
if (Build.VERSION.SDK_INT >= 26) {
List<NotificationChannel> channels = getNotificationChannels();
if (!channels.isEmpty()) {
@@ -743,8 +736,7 @@
* Returns all notification channel groups belonging to the calling app
* or an empty list on older SDKs which don't support Notification Channels.
*/
- @NonNull
- public List<NotificationChannelGroup> getNotificationChannelGroups() {
+ public @NonNull List<NotificationChannelGroup> getNotificationChannelGroups() {
if (Build.VERSION.SDK_INT >= 26) {
return Api26Impl.getNotificationChannelGroups(mNotificationManager);
}
@@ -755,9 +747,8 @@
* Returns all notification channel groups belonging to the calling app
* or an empty list on older SDKs which don't support Notification Channels.
*/
- @NonNull
@SuppressWarnings("MixedMutabilityReturnType")
- public List<NotificationChannelGroupCompat> getNotificationChannelGroupsCompat() {
+ public @NonNull List<NotificationChannelGroupCompat> getNotificationChannelGroupsCompat() {
if (Build.VERSION.SDK_INT >= 26) {
List<NotificationChannelGroup> groups = getNotificationChannelGroups();
if (!groups.isEmpty()) {
@@ -782,8 +773,7 @@
/**
* Get the set of packages that have an enabled notification listener component within them.
*/
- @NonNull
- public static Set<String> getEnabledListenerPackages(@NonNull Context context) {
+ public static @NonNull Set<String> getEnabledListenerPackages(@NonNull Context context) {
final String enabledNotificationListeners = Settings.Secure.getString(
context.getContentResolver(),
SETTING_ENABLED_NOTIFICATION_LISTENERS);
@@ -1182,9 +1172,8 @@
service.notify(packageName, id, tag, notif);
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
StringBuilder sb = new StringBuilder("NotifyTask[");
sb.append("packageName:").append(packageName);
sb.append(", id:").append(id);
@@ -1223,9 +1212,8 @@
}
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
StringBuilder sb = new StringBuilder("CancelTask[");
sb.append("packageName:").append(packageName);
sb.append(", id:").append(id);
diff --git a/core/core/src/main/java/androidx/core/app/PendingIntentCompat.java b/core/core/src/main/java/androidx/core/app/PendingIntentCompat.java
index 0a05c07..7c88576 100644
--- a/core/core/src/main/java/androidx/core/app/PendingIntentCompat.java
+++ b/core/core/src/main/java/androidx/core/app/PendingIntentCompat.java
@@ -1,395 +1,394 @@
-/*
- * Copyright (C) 2022 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
- *
- * http://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 androidx.core.app;
-
-import static android.app.PendingIntent.FLAG_IMMUTABLE;
-import static android.app.PendingIntent.FLAG_MUTABLE;
-
-import android.annotation.SuppressLint;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.os.Handler;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-
-import java.io.Closeable;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.concurrent.CountDownLatch;
-
-/** Helper for accessing features in {@link PendingIntent}. */
-public final class PendingIntentCompat {
-
- @IntDef(
- flag = true,
- value = {
- PendingIntent.FLAG_ONE_SHOT,
- PendingIntent.FLAG_NO_CREATE,
- PendingIntent.FLAG_CANCEL_CURRENT,
- PendingIntent.FLAG_UPDATE_CURRENT,
- Intent.FILL_IN_ACTION,
- Intent.FILL_IN_DATA,
- Intent.FILL_IN_CATEGORIES,
- Intent.FILL_IN_COMPONENT,
- Intent.FILL_IN_PACKAGE,
- Intent.FILL_IN_SOURCE_BOUNDS,
- Intent.FILL_IN_SELECTOR,
- Intent.FILL_IN_CLIP_DATA
- })
- @Retention(RetentionPolicy.SOURCE)
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- public @interface Flags {}
-
- /**
- * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
- * versions. The caller provides the flag as combination of all the other values except
- * mutability flag. This method combines mutability flag when necessary. See {@link
- * PendingIntent#getActivities(Context, int, Intent[], int, Bundle)}.
- */
- public static @NonNull PendingIntent getActivities(
- @NonNull Context context,
- int requestCode,
- @NonNull @SuppressLint("ArrayReturn") Intent[] intents,
- @Flags int flags,
- @Nullable Bundle options,
- boolean isMutable) {
- return PendingIntent.getActivities(context, requestCode, intents,
- addMutabilityFlags(isMutable, flags), options);
- }
-
- /**
- * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
- * versions. The caller provides the flag as combination of all the other values except
- * mutability flag. This method combines mutability flag when necessary. See {@link
- * PendingIntent#getActivities(Context, int, Intent[], int, Bundle)}.
- */
- public static @NonNull PendingIntent getActivities(
- @NonNull Context context,
- int requestCode,
- @NonNull @SuppressLint("ArrayReturn") Intent[] intents,
- @Flags int flags,
- boolean isMutable) {
- return PendingIntent.getActivities(
- context, requestCode, intents, addMutabilityFlags(isMutable, flags));
- }
-
- /**
- * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
- * versions. The caller provides the flag as combination of all the other values except
- * mutability flag. This method combines mutability flag when necessary.
- *
- * @return Returns an existing or new PendingIntent matching the given parameters. May return
- * {@code null} only if {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
- * @see PendingIntent#getActivity(Context, int, Intent, int)
- */
- public static @Nullable PendingIntent getActivity(
- @NonNull Context context,
- int requestCode,
- @NonNull Intent intent,
- @Flags int flags,
- boolean isMutable) {
- return PendingIntent.getActivity(
- context, requestCode, intent, addMutabilityFlags(isMutable, flags));
- }
-
- /**
- * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
- * versions. The caller provides the flag as combination of all the other values except
- * mutability flag. This method combines mutability flag when necessary.
- *
- * @return Returns an existing or new PendingIntent matching the given parameters. May return
- * {@code null} only if {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
- * @see PendingIntent#getActivity(Context, int, Intent, int, Bundle)
- */
- public static @Nullable PendingIntent getActivity(
- @NonNull Context context,
- int requestCode,
- @NonNull Intent intent,
- @Flags int flags,
- @Nullable Bundle options,
- boolean isMutable) {
- return PendingIntent.getActivity(context, requestCode, intent,
- addMutabilityFlags(isMutable, flags), options);
- }
-
- /**
- * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
- * versions. The caller provides the flag as combination of all the other values except
- * mutability flag. This method combines mutability flag when necessary.
- *
- * @return Returns an existing or new PendingIntent matching the given parameters. May return
- * {@code null} only if {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
- * @see PendingIntent#getBroadcast(Context, int, Intent, int)
- */
- public static @Nullable PendingIntent getBroadcast(
- @NonNull Context context,
- int requestCode,
- @NonNull Intent intent,
- @Flags int flags,
- boolean isMutable) {
- return PendingIntent.getBroadcast(
- context, requestCode, intent, addMutabilityFlags(isMutable, flags));
- }
-
- /**
- * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
- * versions. The caller provides the flag as combination of all the other values except
- * mutability flag. This method combines mutability flag when necessary. See {@link
- * PendingIntent#getForegroundService(Context, int, Intent, int)} .
- */
- @RequiresApi(26)
- public static @NonNull PendingIntent getForegroundService(
- @NonNull Context context,
- int requestCode,
- @NonNull Intent intent,
- @Flags int flags,
- boolean isMutable) {
- return Api26Impl.getForegroundService(
- context, requestCode, intent, addMutabilityFlags(isMutable, flags));
- }
-
- /**
- * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
- * versions. The caller provides the flag as combination of all the other values except
- * mutability flag. This method combines mutability flag when necessary.
- *
- * @return Returns an existing or new PendingIntent matching the given parameters. May return
- * {@code null} only if {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
- * @see PendingIntent#getService(Context, int, Intent, int)
- */
- public static @Nullable PendingIntent getService(
- @NonNull Context context,
- int requestCode,
- @NonNull Intent intent,
- @Flags int flags,
- boolean isMutable) {
- return PendingIntent.getService(
- context, requestCode, intent, addMutabilityFlags(isMutable, flags));
- }
-
- /**
- * {@link PendingIntent#send()} variants that support {@link PendingIntent.OnFinished} callbacks
- * have a bug on many API levels that the callback may be invoked even if the PendingIntent was
- * never sent (ie, such as if the PendingIntent was canceled, and the send() invocation threw a
- * {@link PendingIntent.CanceledException}). Using this compatibility method fixes that bug and
- * guarantees that {@link PendingIntent.OnFinished} callbacks will only be invoked if send()
- * completed successfully.
- *
- * <p>See {@link PendingIntent#send(int, PendingIntent.OnFinished, Handler)}.
- */
- @SuppressLint("LambdaLast") // compat shim so arguments should be in the same order
- public static void send(
- @NonNull PendingIntent pendingIntent,
- int code,
- @Nullable PendingIntent.OnFinished onFinished,
- @Nullable Handler handler) throws PendingIntent.CanceledException {
- try (GatedCallback gatedCallback = new GatedCallback(onFinished)) {
- pendingIntent.send(code, gatedCallback.getCallback(), handler);
- gatedCallback.complete();
- }
- }
-
- /**
- * {@link PendingIntent#send()} variants that support {@link PendingIntent.OnFinished} callbacks
- * have a bug on many API levels that the callback may be invoked even if the PendingIntent was
- * never sent (ie, such as if the PendingIntent was canceled, and the send() invocation threw a
- * {@link PendingIntent.CanceledException}). Using this compatibility method fixes that bug and
- * guarantees that {@link PendingIntent.OnFinished} callbacks will only be invoked if send()
- * completed successfully.
- *
- * <p>See {@link PendingIntent#send(Context, int, Intent, PendingIntent.OnFinished, Handler)}.
- */
- @SuppressLint("LambdaLast") // compat shim so arguments must be in the same order
- public static void send(
- @NonNull PendingIntent pendingIntent,
- // compat shim so arguments must be in the same order
- @SuppressLint("ContextFirst") @NonNull Context context,
- int code,
- @NonNull Intent intent,
- @Nullable PendingIntent.OnFinished onFinished,
- @Nullable Handler handler) throws PendingIntent.CanceledException {
- send(pendingIntent, context, code, intent, onFinished, handler, null, null);
- }
-
- /**
- * {@link PendingIntent#send()} variants that support {@link PendingIntent.OnFinished} callbacks
- * have a bug on many API levels that the callback may be invoked even if the PendingIntent was
- * never sent (ie, such as if the PendingIntent was canceled, and the send() invocation threw a
- * {@link PendingIntent.CanceledException}). Using this compatibility method fixes that bug and
- * guarantees that {@link PendingIntent.OnFinished} callbacks will only be invoked if send()
- * completed successfully.
- *
- * <p>See {@link
- * PendingIntent#send(Context, int, Intent, PendingIntent.OnFinished, Handler, String, Bundle)}
- */
- @SuppressLint("LambdaLast") // compat shim so arguments must be in the same order
- public static void send(
- @NonNull PendingIntent pendingIntent,
- // compat shim so arguments must be in the same order
- @SuppressLint("ContextFirst") @NonNull Context context,
- int code,
- @NonNull Intent intent,
- @Nullable PendingIntent.OnFinished onFinished,
- @Nullable Handler handler,
- @Nullable String requiredPermissions,
- @Nullable Bundle options) throws PendingIntent.CanceledException {
- try (GatedCallback gatedCallback = new GatedCallback(onFinished)) {
- if (VERSION.SDK_INT >= VERSION_CODES.M) {
- Api23Impl.send(
- pendingIntent,
- context,
- code,
- intent,
- onFinished,
- handler,
- requiredPermissions,
- options);
- } else {
- pendingIntent.send(context, code, intent, gatedCallback.getCallback(), handler,
- requiredPermissions);
- }
- gatedCallback.complete();
- }
- }
-
- static int addMutabilityFlags(boolean isMutable, int flags) {
- if (isMutable) {
- if (VERSION.SDK_INT >= 31) {
- flags |= FLAG_MUTABLE;
- }
- } else {
- if (VERSION.SDK_INT >= 23) {
- flags |= FLAG_IMMUTABLE;
- }
- }
-
- return flags;
- }
-
- private PendingIntentCompat() {}
-
- @RequiresApi(23)
- private static class Api23Impl {
- private Api23Impl() {}
-
- public static void send(
- @NonNull PendingIntent pendingIntent,
- @NonNull Context context,
- int code,
- @NonNull Intent intent,
- @Nullable PendingIntent.OnFinished onFinished,
- @Nullable Handler handler,
- @Nullable String requiredPermission,
- @Nullable Bundle options) throws PendingIntent.CanceledException {
- pendingIntent.send(
- context,
- code,
- intent,
- onFinished,
- handler,
- requiredPermission,
- options);
- }
- }
-
- @RequiresApi(26)
- private static class Api26Impl {
- private Api26Impl() {}
-
- public static PendingIntent getForegroundService(
- Context context, int requestCode, Intent intent, int flags) {
- return PendingIntent.getForegroundService(context, requestCode, intent, flags);
- }
- }
-
- // see b/201299281 for more info and context
- private static class GatedCallback implements Closeable {
-
- private final CountDownLatch mComplete = new CountDownLatch(1);
-
- @Nullable
- private PendingIntent.OnFinished mCallback;
- private boolean mSuccess;
-
- GatedCallback(@Nullable PendingIntent.OnFinished callback) {
- this.mCallback = callback;
- mSuccess = false;
- }
-
- @Nullable
- public PendingIntent.OnFinished getCallback() {
- if (mCallback == null) {
- return null;
- } else {
- return this::onSendFinished;
- }
- }
-
- public void complete() {
- mSuccess = true;
- }
-
- @Override
- public void close() {
- if (!mSuccess) {
- mCallback = null;
- }
- mComplete.countDown();
- }
-
- private void onSendFinished(
- PendingIntent pendingIntent,
- Intent intent,
- int resultCode,
- String resultData,
- Bundle resultExtras) {
- boolean interrupted = false;
- try {
- while (true) {
- try {
- mComplete.await();
- break;
- } catch (InterruptedException e) {
- interrupted = true;
- }
- }
- } finally {
- if (interrupted) {
- Thread.currentThread().interrupt();
- }
- }
-
- if (mCallback != null) {
- mCallback.onSendFinished(
- pendingIntent,
- intent,
- resultCode,
- resultData,
- resultExtras);
- mCallback = null;
- }
- }
- }
-}
+/*
+ * Copyright (C) 2022 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
+ *
+ * http://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 androidx.core.app;
+
+import static android.app.PendingIntent.FLAG_IMMUTABLE;
+import static android.app.PendingIntent.FLAG_MUTABLE;
+
+import android.annotation.SuppressLint;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+import java.io.Closeable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.CountDownLatch;
+
+/** Helper for accessing features in {@link PendingIntent}. */
+public final class PendingIntentCompat {
+
+ @IntDef(
+ flag = true,
+ value = {
+ PendingIntent.FLAG_ONE_SHOT,
+ PendingIntent.FLAG_NO_CREATE,
+ PendingIntent.FLAG_CANCEL_CURRENT,
+ PendingIntent.FLAG_UPDATE_CURRENT,
+ Intent.FILL_IN_ACTION,
+ Intent.FILL_IN_DATA,
+ Intent.FILL_IN_CATEGORIES,
+ Intent.FILL_IN_COMPONENT,
+ Intent.FILL_IN_PACKAGE,
+ Intent.FILL_IN_SOURCE_BOUNDS,
+ Intent.FILL_IN_SELECTOR,
+ Intent.FILL_IN_CLIP_DATA
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public @interface Flags {}
+
+ /**
+ * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
+ * versions. The caller provides the flag as combination of all the other values except
+ * mutability flag. This method combines mutability flag when necessary. See {@link
+ * PendingIntent#getActivities(Context, int, Intent[], int, Bundle)}.
+ */
+ public static @NonNull PendingIntent getActivities(
+ @NonNull Context context,
+ int requestCode,
+ @SuppressLint("ArrayReturn") Intent @NonNull [] intents,
+ @Flags int flags,
+ @Nullable Bundle options,
+ boolean isMutable) {
+ return PendingIntent.getActivities(context, requestCode, intents,
+ addMutabilityFlags(isMutable, flags), options);
+ }
+
+ /**
+ * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
+ * versions. The caller provides the flag as combination of all the other values except
+ * mutability flag. This method combines mutability flag when necessary. See {@link
+ * PendingIntent#getActivities(Context, int, Intent[], int, Bundle)}.
+ */
+ public static @NonNull PendingIntent getActivities(
+ @NonNull Context context,
+ int requestCode,
+ @SuppressLint("ArrayReturn") Intent @NonNull [] intents,
+ @Flags int flags,
+ boolean isMutable) {
+ return PendingIntent.getActivities(
+ context, requestCode, intents, addMutabilityFlags(isMutable, flags));
+ }
+
+ /**
+ * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
+ * versions. The caller provides the flag as combination of all the other values except
+ * mutability flag. This method combines mutability flag when necessary.
+ *
+ * @return Returns an existing or new PendingIntent matching the given parameters. May return
+ * {@code null} only if {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
+ * @see PendingIntent#getActivity(Context, int, Intent, int)
+ */
+ public static @Nullable PendingIntent getActivity(
+ @NonNull Context context,
+ int requestCode,
+ @NonNull Intent intent,
+ @Flags int flags,
+ boolean isMutable) {
+ return PendingIntent.getActivity(
+ context, requestCode, intent, addMutabilityFlags(isMutable, flags));
+ }
+
+ /**
+ * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
+ * versions. The caller provides the flag as combination of all the other values except
+ * mutability flag. This method combines mutability flag when necessary.
+ *
+ * @return Returns an existing or new PendingIntent matching the given parameters. May return
+ * {@code null} only if {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
+ * @see PendingIntent#getActivity(Context, int, Intent, int, Bundle)
+ */
+ public static @Nullable PendingIntent getActivity(
+ @NonNull Context context,
+ int requestCode,
+ @NonNull Intent intent,
+ @Flags int flags,
+ @Nullable Bundle options,
+ boolean isMutable) {
+ return PendingIntent.getActivity(context, requestCode, intent,
+ addMutabilityFlags(isMutable, flags), options);
+ }
+
+ /**
+ * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
+ * versions. The caller provides the flag as combination of all the other values except
+ * mutability flag. This method combines mutability flag when necessary.
+ *
+ * @return Returns an existing or new PendingIntent matching the given parameters. May return
+ * {@code null} only if {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
+ * @see PendingIntent#getBroadcast(Context, int, Intent, int)
+ */
+ public static @Nullable PendingIntent getBroadcast(
+ @NonNull Context context,
+ int requestCode,
+ @NonNull Intent intent,
+ @Flags int flags,
+ boolean isMutable) {
+ return PendingIntent.getBroadcast(
+ context, requestCode, intent, addMutabilityFlags(isMutable, flags));
+ }
+
+ /**
+ * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
+ * versions. The caller provides the flag as combination of all the other values except
+ * mutability flag. This method combines mutability flag when necessary. See {@link
+ * PendingIntent#getForegroundService(Context, int, Intent, int)} .
+ */
+ @RequiresApi(26)
+ public static @NonNull PendingIntent getForegroundService(
+ @NonNull Context context,
+ int requestCode,
+ @NonNull Intent intent,
+ @Flags int flags,
+ boolean isMutable) {
+ return Api26Impl.getForegroundService(
+ context, requestCode, intent, addMutabilityFlags(isMutable, flags));
+ }
+
+ /**
+ * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
+ * versions. The caller provides the flag as combination of all the other values except
+ * mutability flag. This method combines mutability flag when necessary.
+ *
+ * @return Returns an existing or new PendingIntent matching the given parameters. May return
+ * {@code null} only if {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
+ * @see PendingIntent#getService(Context, int, Intent, int)
+ */
+ public static @Nullable PendingIntent getService(
+ @NonNull Context context,
+ int requestCode,
+ @NonNull Intent intent,
+ @Flags int flags,
+ boolean isMutable) {
+ return PendingIntent.getService(
+ context, requestCode, intent, addMutabilityFlags(isMutable, flags));
+ }
+
+ /**
+ * {@link PendingIntent#send()} variants that support {@link PendingIntent.OnFinished} callbacks
+ * have a bug on many API levels that the callback may be invoked even if the PendingIntent was
+ * never sent (ie, such as if the PendingIntent was canceled, and the send() invocation threw a
+ * {@link PendingIntent.CanceledException}). Using this compatibility method fixes that bug and
+ * guarantees that {@link PendingIntent.OnFinished} callbacks will only be invoked if send()
+ * completed successfully.
+ *
+ * <p>See {@link PendingIntent#send(int, PendingIntent.OnFinished, Handler)}.
+ */
+ @SuppressLint("LambdaLast") // compat shim so arguments should be in the same order
+ public static void send(
+ @NonNull PendingIntent pendingIntent,
+ int code,
+ PendingIntent.@Nullable OnFinished onFinished,
+ @Nullable Handler handler) throws PendingIntent.CanceledException {
+ try (GatedCallback gatedCallback = new GatedCallback(onFinished)) {
+ pendingIntent.send(code, gatedCallback.getCallback(), handler);
+ gatedCallback.complete();
+ }
+ }
+
+ /**
+ * {@link PendingIntent#send()} variants that support {@link PendingIntent.OnFinished} callbacks
+ * have a bug on many API levels that the callback may be invoked even if the PendingIntent was
+ * never sent (ie, such as if the PendingIntent was canceled, and the send() invocation threw a
+ * {@link PendingIntent.CanceledException}). Using this compatibility method fixes that bug and
+ * guarantees that {@link PendingIntent.OnFinished} callbacks will only be invoked if send()
+ * completed successfully.
+ *
+ * <p>See {@link PendingIntent#send(Context, int, Intent, PendingIntent.OnFinished, Handler)}.
+ */
+ @SuppressLint("LambdaLast") // compat shim so arguments must be in the same order
+ public static void send(
+ @NonNull PendingIntent pendingIntent,
+ // compat shim so arguments must be in the same order
+ @SuppressLint("ContextFirst") @NonNull Context context,
+ int code,
+ @NonNull Intent intent,
+ PendingIntent.@Nullable OnFinished onFinished,
+ @Nullable Handler handler) throws PendingIntent.CanceledException {
+ send(pendingIntent, context, code, intent, onFinished, handler, null, null);
+ }
+
+ /**
+ * {@link PendingIntent#send()} variants that support {@link PendingIntent.OnFinished} callbacks
+ * have a bug on many API levels that the callback may be invoked even if the PendingIntent was
+ * never sent (ie, such as if the PendingIntent was canceled, and the send() invocation threw a
+ * {@link PendingIntent.CanceledException}). Using this compatibility method fixes that bug and
+ * guarantees that {@link PendingIntent.OnFinished} callbacks will only be invoked if send()
+ * completed successfully.
+ *
+ * <p>See {@link
+ * PendingIntent#send(Context, int, Intent, PendingIntent.OnFinished, Handler, String, Bundle)}
+ */
+ @SuppressLint("LambdaLast") // compat shim so arguments must be in the same order
+ public static void send(
+ @NonNull PendingIntent pendingIntent,
+ // compat shim so arguments must be in the same order
+ @SuppressLint("ContextFirst") @NonNull Context context,
+ int code,
+ @NonNull Intent intent,
+ PendingIntent.@Nullable OnFinished onFinished,
+ @Nullable Handler handler,
+ @Nullable String requiredPermissions,
+ @Nullable Bundle options) throws PendingIntent.CanceledException {
+ try (GatedCallback gatedCallback = new GatedCallback(onFinished)) {
+ if (VERSION.SDK_INT >= VERSION_CODES.M) {
+ Api23Impl.send(
+ pendingIntent,
+ context,
+ code,
+ intent,
+ onFinished,
+ handler,
+ requiredPermissions,
+ options);
+ } else {
+ pendingIntent.send(context, code, intent, gatedCallback.getCallback(), handler,
+ requiredPermissions);
+ }
+ gatedCallback.complete();
+ }
+ }
+
+ static int addMutabilityFlags(boolean isMutable, int flags) {
+ if (isMutable) {
+ if (VERSION.SDK_INT >= 31) {
+ flags |= FLAG_MUTABLE;
+ }
+ } else {
+ if (VERSION.SDK_INT >= 23) {
+ flags |= FLAG_IMMUTABLE;
+ }
+ }
+
+ return flags;
+ }
+
+ private PendingIntentCompat() {}
+
+ @RequiresApi(23)
+ private static class Api23Impl {
+ private Api23Impl() {}
+
+ public static void send(
+ @NonNull PendingIntent pendingIntent,
+ @NonNull Context context,
+ int code,
+ @NonNull Intent intent,
+ PendingIntent.@Nullable OnFinished onFinished,
+ @Nullable Handler handler,
+ @Nullable String requiredPermission,
+ @Nullable Bundle options) throws PendingIntent.CanceledException {
+ pendingIntent.send(
+ context,
+ code,
+ intent,
+ onFinished,
+ handler,
+ requiredPermission,
+ options);
+ }
+ }
+
+ @RequiresApi(26)
+ private static class Api26Impl {
+ private Api26Impl() {}
+
+ public static PendingIntent getForegroundService(
+ Context context, int requestCode, Intent intent, int flags) {
+ return PendingIntent.getForegroundService(context, requestCode, intent, flags);
+ }
+ }
+
+ // see b/201299281 for more info and context
+ private static class GatedCallback implements Closeable {
+
+ private final CountDownLatch mComplete = new CountDownLatch(1);
+
+ private PendingIntent.@Nullable OnFinished mCallback;
+ private boolean mSuccess;
+
+ GatedCallback(PendingIntent.@Nullable OnFinished callback) {
+ this.mCallback = callback;
+ mSuccess = false;
+ }
+
+ public PendingIntent.@Nullable OnFinished getCallback() {
+ if (mCallback == null) {
+ return null;
+ } else {
+ return this::onSendFinished;
+ }
+ }
+
+ public void complete() {
+ mSuccess = true;
+ }
+
+ @Override
+ public void close() {
+ if (!mSuccess) {
+ mCallback = null;
+ }
+ mComplete.countDown();
+ }
+
+ private void onSendFinished(
+ PendingIntent pendingIntent,
+ Intent intent,
+ int resultCode,
+ String resultData,
+ Bundle resultExtras) {
+ boolean interrupted = false;
+ try {
+ while (true) {
+ try {
+ mComplete.await();
+ break;
+ } catch (InterruptedException e) {
+ interrupted = true;
+ }
+ }
+ } finally {
+ if (interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ if (mCallback != null) {
+ mCallback.onSendFinished(
+ pendingIntent,
+ intent,
+ resultCode,
+ resultData,
+ resultExtras);
+ mCallback = null;
+ }
+ }
+ }
+}
diff --git a/core/core/src/main/java/androidx/core/app/Person.java b/core/core/src/main/java/androidx/core/app/Person.java
index 441de1b..db0a07c 100644
--- a/core/core/src/main/java/androidx/core/app/Person.java
+++ b/core/core/src/main/java/androidx/core/app/Person.java
@@ -21,12 +21,13 @@
import android.os.Bundle;
import android.os.PersistableBundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.graphics.drawable.IconCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.Objects;
/**
@@ -45,8 +46,7 @@
* Extracts and returns the {@link Person} written to the {@code bundle}. A bundle can be
* created from a {@link Person} using {@link #toBundle()}.
*/
- @NonNull
- public static Person fromBundle(@NonNull Bundle bundle) {
+ public static @NonNull Person fromBundle(@NonNull Bundle bundle) {
Bundle iconBundle = bundle.getBundle(ICON_KEY);
return new Builder()
.setName(bundle.getCharSequence(NAME_KEY))
@@ -65,9 +65,8 @@
*
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @NonNull
@RequiresApi(22)
- public static Person fromPersistableBundle(@NonNull PersistableBundle bundle) {
+ public static @NonNull Person fromPersistableBundle(@NonNull PersistableBundle bundle) {
return Api22Impl.fromPersistableBundle(bundle);
}
@@ -77,19 +76,18 @@
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@RequiresApi(28)
- @NonNull
- public static Person fromAndroidPerson(@NonNull android.app.Person person) {
+ public static @NonNull Person fromAndroidPerson(android.app.@NonNull Person person) {
return Api28Impl.fromAndroidPerson(person);
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
- @Nullable CharSequence mName;
+@Nullable CharSequence mName;
@SuppressWarnings("WeakerAccess") /* synthetic access */
- @Nullable IconCompat mIcon;
+@Nullable IconCompat mIcon;
@SuppressWarnings("WeakerAccess") /* synthetic access */
- @Nullable String mUri;
+@Nullable String mUri;
@SuppressWarnings("WeakerAccess") /* synthetic access */
- @Nullable String mKey;
+@Nullable String mKey;
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mIsBot;
@SuppressWarnings("WeakerAccess") /* synthetic access */
@@ -109,8 +107,7 @@
* Writes and returns a new {@link Bundle} that represents this {@link Person}. This bundle can
* be converted back by using {@link #fromBundle(Bundle)}.
*/
- @NonNull
- public Bundle toBundle() {
+ public @NonNull Bundle toBundle() {
Bundle result = new Bundle();
result.putCharSequence(NAME_KEY, mName);
result.putBundle(ICON_KEY, mIcon != null ? mIcon.toBundle() : null);
@@ -128,15 +125,13 @@
*
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @NonNull
@RequiresApi(22)
- public PersistableBundle toPersistableBundle() {
+ public @NonNull PersistableBundle toPersistableBundle() {
return Api22Impl.toPersistableBundle(this);
}
/** Creates and returns a new {@link Builder} initialized with this Person's data. */
- @NonNull
- public Builder toBuilder() {
+ public @NonNull Builder toBuilder() {
return new Builder(this);
}
@@ -145,9 +140,8 @@
*
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @NonNull
@RequiresApi(28)
- public android.app.Person toAndroidPerson() {
+ public android.app.@NonNull Person toAndroidPerson() {
return Api28Impl.toAndroidPerson(this);
}
@@ -155,14 +149,12 @@
* Returns the name for this {@link Person} or {@code null} if no name was provided. This could
* be a full name, nickname, username, etc.
*/
- @Nullable
- public CharSequence getName() {
+ public @Nullable CharSequence getName() {
return mName;
}
/** Returns the icon for this {@link Person} or {@code null} if no icon was provided. */
- @Nullable
- public IconCompat getIcon() {
+ public @Nullable IconCompat getIcon() {
return mIcon;
}
@@ -179,8 +171,7 @@
* <p>*Note for these schemas, the path portion of the URI must exist in the contacts
* database in their appropriate column, otherwise the reference should be discarded.
*/
- @Nullable
- public String getUri() {
+ public @Nullable String getUri() {
return mUri;
}
@@ -188,8 +179,7 @@
* Returns the key for this {@link Person} or {@code null} if no key was provided. This is
* provided as a unique identifier between other {@link Person}s.
*/
- @Nullable
- public String getKey() {
+ public @Nullable String getKey() {
return mKey;
}
@@ -212,9 +202,8 @@
/**
* @return the URI associated with this person, or "name:mName" otherwise
*/
- @NonNull
@RestrictTo(LIBRARY_GROUP_PREFIX)
- public String resolveToLegacyUri() {
+ public @NonNull String resolveToLegacyUri() {
if (mUri != null) {
return mUri;
}
@@ -292,8 +281,7 @@
* Give this {@link Person} a name to use for display. This can be, for example, a full
* name, nickname, username, etc.
*/
- @NonNull
- public Builder setName(@Nullable CharSequence name) {
+ public @NonNull Builder setName(@Nullable CharSequence name) {
mName = name;
return this;
}
@@ -304,8 +292,7 @@
* <p>The system will prefer this icon over any images that are resolved from
* {@link #setUri(String)}.
*/
- @NonNull
- public Builder setIcon(@Nullable IconCompat icon) {
+ public @NonNull Builder setIcon(@Nullable IconCompat icon) {
mIcon = icon;
return this;
}
@@ -322,8 +309,7 @@
* <p>*Note for these schemas, the path portion of the URI must exist in the contacts
* database in their appropriate column, otherwise the reference will be discarded.
*/
- @NonNull
- public Builder setUri(@Nullable String uri) {
+ public @NonNull Builder setUri(@Nullable String uri) {
mUri = uri;
return this;
}
@@ -333,8 +319,7 @@
* {@link #setName(CharSequence)} value isn't unique. This value is preferred for
* identification, but if it's not provided, the person's name will be used in its place.
*/
- @NonNull
- public Builder setKey(@Nullable String key) {
+ public @NonNull Builder setKey(@Nullable String key) {
mKey = key;
return this;
}
@@ -343,8 +328,7 @@
* Sets whether or not this {@link Person} represents a machine rather than a human. This is
* used primarily for testing and automated tooling.
*/
- @NonNull
- public Builder setBot(boolean bot) {
+ public @NonNull Builder setBot(boolean bot) {
mIsBot = bot;
return this;
}
@@ -355,15 +339,13 @@
* {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}, and instead with
* the {@code mailto:} or {@code tel:} schemas.
*/
- @NonNull
- public Builder setImportant(boolean important) {
+ public @NonNull Builder setImportant(boolean important) {
mIsImportant = important;
return this;
}
/** Creates and returns the {@link Person} this builder represents. */
- @NonNull
- public Person build() {
+ public @NonNull Person build() {
return new Person(this);
}
}
diff --git a/core/core/src/main/java/androidx/core/app/RemoteActionCompat.java b/core/core/src/main/java/androidx/core/app/RemoteActionCompat.java
index 120350c..01610ec 100644
--- a/core/core/src/main/java/androidx/core/app/RemoteActionCompat.java
+++ b/core/core/src/main/java/androidx/core/app/RemoteActionCompat.java
@@ -24,7 +24,6 @@
import android.graphics.drawable.Icon;
import android.os.Build;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.graphics.drawable.IconCompat;
@@ -33,6 +32,8 @@
import androidx.versionedparcelable.VersionedParcelable;
import androidx.versionedparcelable.VersionedParcelize;
+import org.jspecify.annotations.NonNull;
+
/**
* Represents a remote action that can be called from another process. The action can have an
* associated visualization including metadata like an icon or title.
@@ -44,31 +45,27 @@
/**
*/
@SuppressWarnings("NotNullFieldNotInitialized") // VersionedParceleble inits this field.
- @NonNull
@RestrictTo(LIBRARY_GROUP)
@ParcelField(1)
- public IconCompat mIcon;
+ public @NonNull IconCompat mIcon;
/**
*/
@SuppressWarnings("NotNullFieldNotInitialized") // VersionedParceleble inits this field.
- @NonNull
@RestrictTo(LIBRARY_GROUP)
@ParcelField(2)
- public CharSequence mTitle;
+ public @NonNull CharSequence mTitle;
/**
*/
@SuppressWarnings("NotNullFieldNotInitialized") // VersionedParceleble inits this field.
- @NonNull
@RestrictTo(LIBRARY_GROUP)
@ParcelField(3)
- public CharSequence mContentDescription;
+ public @NonNull CharSequence mContentDescription;
/**
*/
@SuppressWarnings("NotNullFieldNotInitialized") // VersionedParceleble inits this field.
- @NonNull
@RestrictTo(LIBRARY_GROUP)
@ParcelField(4)
- public PendingIntent mActionIntent;
+ public @NonNull PendingIntent mActionIntent;
/**
*/
@RestrictTo(LIBRARY_GROUP)
@@ -113,8 +110,8 @@
* Creates an RemoteActionCompat from a RemoteAction.
*/
@RequiresApi(26)
- @NonNull
- public static RemoteActionCompat createFromRemoteAction(@NonNull RemoteAction remoteAction) {
+ public static @NonNull RemoteActionCompat createFromRemoteAction(
+ @NonNull RemoteAction remoteAction) {
Preconditions.checkNotNull(remoteAction);
RemoteActionCompat action = new RemoteActionCompat(IconCompat.createFromIcon(
Api26Impl.getIcon(remoteAction)),
@@ -192,8 +189,7 @@
*/
@SuppressWarnings("deprecation")
@RequiresApi(26)
- @NonNull
- public RemoteAction toRemoteAction() {
+ public @NonNull RemoteAction toRemoteAction() {
RemoteAction action = Api26Impl.createRemoteAction(mIcon.toIcon(), mTitle,
mContentDescription, mActionIntent);
Api26Impl.setEnabled(action, isEnabled());
diff --git a/core/core/src/main/java/androidx/core/app/RemoteInput.java b/core/core/src/main/java/androidx/core/app/RemoteInput.java
index 4dd86fc..634ea0d9 100644
--- a/core/core/src/main/java/androidx/core/app/RemoteInput.java
+++ b/core/core/src/main/java/androidx/core/app/RemoteInput.java
@@ -24,11 +24,12 @@
import android.os.Bundle;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
@@ -109,16 +110,14 @@
* Get the key that the result of this input will be set in from the Bundle returned by
* {@link #getResultsFromIntent} when the {@link android.app.PendingIntent} is sent.
*/
- @NonNull
- public String getResultKey() {
+ public @NonNull String getResultKey() {
return mResultKey;
}
/**
* Get the label to display to users when collecting this input.
*/
- @Nullable
- public CharSequence getLabel() {
+ public @Nullable CharSequence getLabel() {
return mLabel;
}
@@ -126,14 +125,12 @@
* Get possible input choices. This can be {@code null} if there are no choices to present.
*/
@SuppressWarnings("NullableCollection") // Look, it's not the best API.
- @Nullable
- public CharSequence[] getChoices() {
+ public CharSequence @Nullable [] getChoices() {
return mChoices;
}
@SuppressWarnings("NullableCollection") // That's just how it was defined.
- @Nullable
- public Set<String> getAllowedDataTypes() {
+ public @Nullable Set<String> getAllowedDataTypes() {
return mAllowedDataTypes;
}
@@ -170,8 +167,7 @@
/**
* Get additional metadata carried around with this remote input.
*/
- @NonNull
- public Bundle getExtras() {
+ public @NonNull Bundle getExtras() {
return mExtras;
}
@@ -206,8 +202,7 @@
* @param label The label to show to users when they input a response
* @return this object for method chaining
*/
- @NonNull
- public Builder setLabel(@Nullable CharSequence label) {
+ public @NonNull Builder setLabel(@Nullable CharSequence label) {
mLabel = label;
return this;
}
@@ -224,8 +219,7 @@
* you disabled free form input using {@link #setAllowFreeFormInput}
* @return this object for method chaining
*/
- @NonNull
- public Builder setChoices(@Nullable CharSequence[] choices) {
+ public @NonNull Builder setChoices(CharSequence @Nullable [] choices) {
mChoices = choices;
return this;
}
@@ -240,8 +234,7 @@
* @param doAllow Whether the mime type should be allowed or not
* @return this object for method chaining
*/
- @NonNull
- public Builder setAllowDataType(@NonNull String mimeType, boolean doAllow) {
+ public @NonNull Builder setAllowDataType(@NonNull String mimeType, boolean doAllow) {
if (doAllow) {
mAllowedDataTypes.add(mimeType);
} else {
@@ -260,8 +253,7 @@
* {@link IllegalArgumentException} is thrown
* @return this object for method chaining
*/
- @NonNull
- public Builder setAllowFreeFormInput(boolean allowFreeFormTextInput) {
+ public @NonNull Builder setAllowFreeFormInput(boolean allowFreeFormTextInput) {
mAllowFreeFormTextInput = allowFreeFormTextInput;
return this;
}
@@ -272,8 +264,7 @@
*
* It cannot be used if {@link #setAllowFreeFormInput} has been set to false.
*/
- @NonNull
- public Builder setEditChoicesBeforeSending(
+ public @NonNull Builder setEditChoicesBeforeSending(
@EditChoicesBeforeSending int editChoicesBeforeSending) {
mEditChoicesBeforeSending = editChoicesBeforeSending;
return this;
@@ -286,8 +277,7 @@
*
* @see RemoteInput#getExtras
*/
- @NonNull
- public Builder addExtras(@NonNull Bundle extras) {
+ public @NonNull Builder addExtras(@NonNull Bundle extras) {
if (extras != null) {
mExtras.putAll(extras);
}
@@ -299,8 +289,7 @@
*
* <p>The returned Bundle is shared with this Builder.
*/
- @NonNull
- public Bundle getExtras() {
+ public @NonNull Bundle getExtras() {
return mExtras;
}
@@ -308,8 +297,7 @@
* Combine all of the options that have been set and return a new {@link
* androidx.core.app.RemoteInput} object.
*/
- @NonNull
- public RemoteInput build() {
+ public @NonNull RemoteInput build() {
return new RemoteInput(
mResultKey,
mLabel,
@@ -338,8 +326,7 @@
* @param remoteInputResultKey The result key for the RemoteInput you want results for.
*/
@SuppressWarnings("NullableCollection") // This is what the platform API does.
- @Nullable
- public static Map<String, Uri> getDataResultsFromIntent(
+ public static @Nullable Map<String, Uri> getDataResultsFromIntent(
@NonNull Intent intent, @NonNull String remoteInputResultKey) {
if (Build.VERSION.SDK_INT >= 26) {
return Api26Impl.getDataResultsFromIntent(intent, remoteInputResultKey);
@@ -378,8 +365,7 @@
*/
// This is on purpose.
@SuppressWarnings({"NullableCollection", "deprecation"})
- @Nullable
- public static Bundle getResultsFromIntent(@NonNull Intent intent) {
+ public static @Nullable Bundle getResultsFromIntent(@NonNull Intent intent) {
if (Build.VERSION.SDK_INT >= 20) {
return Api20Impl.getResultsFromIntent(intent);
} else {
@@ -403,7 +389,7 @@
* {@code remoteInputs} with values being the result per key.
*/
@SuppressWarnings("deprecation")
- public static void addResultsToIntent(@NonNull RemoteInput[] remoteInputs,
+ public static void addResultsToIntent(RemoteInput @NonNull [] remoteInputs,
@NonNull Intent intent, @NonNull Bundle results) {
if (Build.VERSION.SDK_INT >= 26) {
Api20Impl.addResultsToIntent(fromCompat(remoteInputs), intent, results);
diff --git a/core/core/src/main/java/androidx/core/app/ServiceCompat.java b/core/core/src/main/java/androidx/core/app/ServiceCompat.java
index 444e0c0..e1b8891 100644
--- a/core/core/src/main/java/androidx/core/app/ServiceCompat.java
+++ b/core/core/src/main/java/androidx/core/app/ServiceCompat.java
@@ -27,10 +27,11 @@
import android.os.Build;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
diff --git a/core/core/src/main/java/androidx/core/app/ShareCompat.java b/core/core/src/main/java/androidx/core/app/ShareCompat.java
index 818210a..e54c4d8 100644
--- a/core/core/src/main/java/androidx/core/app/ShareCompat.java
+++ b/core/core/src/main/java/androidx/core/app/ShareCompat.java
@@ -37,11 +37,12 @@
import android.widget.ShareActionProvider;
import androidx.annotation.IdRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.content.IntentCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
/**
@@ -126,8 +127,7 @@
* @param calledActivity Current activity that was launched to share content
* @return Name of the calling package
*/
- @Nullable
- public static String getCallingPackage(@NonNull Activity calledActivity) {
+ public static @Nullable String getCallingPackage(@NonNull Activity calledActivity) {
Intent intent = calledActivity.getIntent();
String result = calledActivity.getCallingPackage();
if (result == null && intent != null) {
@@ -149,8 +149,7 @@
* @return Name of the calling package
*/
@SuppressWarnings("WeakerAccess")
- @Nullable
- static String getCallingPackage(@NonNull Intent intent) {
+ static @Nullable String getCallingPackage(@NonNull Intent intent) {
String result = intent.getStringExtra(EXTRA_CALLING_PACKAGE);
if (result == null) {
result = intent.getStringExtra(EXTRA_CALLING_PACKAGE_INTEROP);
@@ -170,8 +169,7 @@
* @param calledActivity Current activity that was launched to share content
* @return ComponentName of the calling activity
*/
- @Nullable
- public static ComponentName getCallingActivity(@NonNull Activity calledActivity) {
+ public static @Nullable ComponentName getCallingActivity(@NonNull Activity calledActivity) {
Intent intent = calledActivity.getIntent();
ComponentName result = calledActivity.getCallingActivity();
if (result == null) {
@@ -193,8 +191,7 @@
* @return ComponentName of the calling activity
*/
@SuppressWarnings({"WeakerAccess", "deprecation"})
- @Nullable
- static ComponentName getCallingActivity(@NonNull Intent intent) {
+ static @Nullable ComponentName getCallingActivity(@NonNull Intent intent) {
ComponentName result = intent.getParcelableExtra(EXTRA_CALLING_ACTIVITY);
if (result == null) {
result = intent.getParcelableExtra(EXTRA_CALLING_ACTIVITY_INTEROP);
@@ -291,9 +288,8 @@
* @return a new IntentBuilder instance
* @deprecated Use the constructor of IntentBuilder
*/
- @NonNull
@Deprecated
- public static IntentBuilder from(@NonNull Activity launchingActivity) {
+ public static @NonNull IntentBuilder from(@NonNull Activity launchingActivity) {
return new IntentBuilder(launchingActivity);
}
@@ -339,8 +335,7 @@
*
* @return The current Intent being configured by this builder
*/
- @NonNull
- public Intent getIntent() {
+ public @NonNull Intent getIntent() {
if (mToAddresses != null) {
combineArrayExtra(Intent.EXTRA_EMAIL, mToAddresses);
mToAddresses = null;
@@ -375,8 +370,7 @@
return mIntent;
}
- @NonNull
- Context getContext() {
+ @NonNull Context getContext() {
return mContext;
}
@@ -391,7 +385,7 @@
mIntent.putExtra(extra, finalAddresses);
}
- private void combineArrayExtra(@Nullable String extra, @NonNull String[] add) {
+ private void combineArrayExtra(@Nullable String extra, String @NonNull [] add) {
// Add any items still pending
Intent intent = getIntent();
String[] old = intent.getStringArrayExtra(extra);
@@ -409,8 +403,7 @@
*
* @return A chooser Intent for the currently configured sharing action
*/
- @NonNull
- public Intent createChooserIntent() {
+ public @NonNull Intent createChooserIntent() {
return Intent.createChooser(getIntent(), mChooserTitle);
}
@@ -427,8 +420,7 @@
* @param title Title string
* @return This IntentBuilder for method chaining
*/
- @NonNull
- public IntentBuilder setChooserTitle(@Nullable CharSequence title) {
+ public @NonNull IntentBuilder setChooserTitle(@Nullable CharSequence title) {
mChooserTitle = title;
return this;
}
@@ -439,8 +431,7 @@
* @param resId Resource ID of the title string to use
* @return This IntentBuilder for method chaining
*/
- @NonNull
- public IntentBuilder setChooserTitle(@StringRes int resId) {
+ public @NonNull IntentBuilder setChooserTitle(@StringRes int resId) {
return setChooserTitle(mContext.getText(resId));
}
@@ -451,8 +442,7 @@
* @return This IntentBuilder for method chaining
* @see Intent#setType(String)
*/
- @NonNull
- public IntentBuilder setType(@Nullable String mimeType) {
+ public @NonNull IntentBuilder setType(@Nullable String mimeType) {
mIntent.setType(mimeType);
return this;
}
@@ -465,8 +455,7 @@
* @return This IntentBuilder for method chaining
* @see Intent#EXTRA_TEXT
*/
- @NonNull
- public IntentBuilder setText(@Nullable CharSequence text) {
+ public @NonNull IntentBuilder setText(@Nullable CharSequence text) {
mIntent.putExtra(Intent.EXTRA_TEXT, text);
return this;
}
@@ -482,8 +471,7 @@
* @return This IntentBuilder for method chaining
* @see #setText(CharSequence)
*/
- @NonNull
- public IntentBuilder setHtmlText(@Nullable String htmlText) {
+ public @NonNull IntentBuilder setHtmlText(@Nullable String htmlText) {
mIntent.putExtra(IntentCompat.EXTRA_HTML_TEXT, htmlText);
if (!mIntent.hasExtra(Intent.EXTRA_TEXT)) {
// Supply a default if EXTRA_TEXT isn't set
@@ -502,8 +490,7 @@
* @return This IntentBuilder for method chaining
* @see Intent#EXTRA_STREAM
*/
- @NonNull
- public IntentBuilder setStream(@Nullable Uri streamUri) {
+ public @NonNull IntentBuilder setStream(@Nullable Uri streamUri) {
mStreams = null;
if (streamUri != null) {
addStream(streamUri);
@@ -522,8 +509,7 @@
* @see Intent#ACTION_SEND
* @see Intent#ACTION_SEND_MULTIPLE
*/
- @NonNull
- public IntentBuilder addStream(@NonNull Uri streamUri) {
+ public @NonNull IntentBuilder addStream(@NonNull Uri streamUri) {
if (mStreams == null) {
mStreams = new ArrayList<>();
}
@@ -539,8 +525,7 @@
* @return This IntentBuilder for method chaining
* @see Intent#EXTRA_EMAIL
*/
- @NonNull
- public IntentBuilder setEmailTo(@Nullable String[] addresses) {
+ public @NonNull IntentBuilder setEmailTo(String @Nullable [] addresses) {
if (mToAddresses != null) {
mToAddresses = null;
}
@@ -555,8 +540,7 @@
* @return This IntentBuilder for method chaining
* @see Intent#EXTRA_EMAIL
*/
- @NonNull
- public IntentBuilder addEmailTo(@NonNull String address) {
+ public @NonNull IntentBuilder addEmailTo(@NonNull String address) {
if (mToAddresses == null) {
mToAddresses = new ArrayList<>();
}
@@ -571,8 +555,7 @@
* @return This IntentBuilder for method chaining
* @see Intent#EXTRA_EMAIL
*/
- @NonNull
- public IntentBuilder addEmailTo(@NonNull String[] addresses) {
+ public @NonNull IntentBuilder addEmailTo(String @NonNull [] addresses) {
combineArrayExtra(Intent.EXTRA_EMAIL, addresses);
return this;
}
@@ -585,8 +568,7 @@
* @return This IntentBuilder for method chaining
* @see Intent#EXTRA_CC
*/
- @NonNull
- public IntentBuilder setEmailCc(@Nullable String[] addresses) {
+ public @NonNull IntentBuilder setEmailCc(String @Nullable [] addresses) {
mIntent.putExtra(Intent.EXTRA_CC, addresses);
return this;
}
@@ -598,8 +580,7 @@
* @return This IntentBuilder for method chaining
* @see Intent#EXTRA_CC
*/
- @NonNull
- public IntentBuilder addEmailCc(@NonNull String address) {
+ public @NonNull IntentBuilder addEmailCc(@NonNull String address) {
if (mCcAddresses == null) {
mCcAddresses = new ArrayList<>();
}
@@ -614,8 +595,7 @@
* @return This IntentBuilder for method chaining
* @see Intent#EXTRA_CC
*/
- @NonNull
- public IntentBuilder addEmailCc(@NonNull String[] addresses) {
+ public @NonNull IntentBuilder addEmailCc(String @NonNull [] addresses) {
combineArrayExtra(Intent.EXTRA_CC, addresses);
return this;
}
@@ -628,8 +608,7 @@
* @return This IntentBuilder for method chaining
* @see Intent#EXTRA_BCC
*/
- @NonNull
- public IntentBuilder setEmailBcc(@Nullable String[] addresses) {
+ public @NonNull IntentBuilder setEmailBcc(String @Nullable [] addresses) {
mIntent.putExtra(Intent.EXTRA_BCC, addresses);
return this;
}
@@ -641,8 +620,7 @@
* @return This IntentBuilder for method chaining
* @see Intent#EXTRA_BCC
*/
- @NonNull
- public IntentBuilder addEmailBcc(@NonNull String address) {
+ public @NonNull IntentBuilder addEmailBcc(@NonNull String address) {
if (mBccAddresses == null) {
mBccAddresses = new ArrayList<>();
}
@@ -657,8 +635,7 @@
* @return This IntentBuilder for method chaining
* @see Intent#EXTRA_BCC
*/
- @NonNull
- public IntentBuilder addEmailBcc(@NonNull String[] addresses) {
+ public @NonNull IntentBuilder addEmailBcc(String @NonNull [] addresses) {
combineArrayExtra(Intent.EXTRA_BCC, addresses);
return this;
}
@@ -670,8 +647,7 @@
* @return This IntentBuilder for method chaining
* @see Intent#EXTRA_SUBJECT
*/
- @NonNull
- public IntentBuilder setSubject(@Nullable String subject) {
+ public @NonNull IntentBuilder setSubject(@Nullable String subject) {
mIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
return this;
}
@@ -712,9 +688,8 @@
* @return IntentReader for parsing sharing data
* @deprecated Use the constructor of IntentReader instead
*/
- @NonNull
@Deprecated
- public static IntentReader from(@NonNull Activity activity) {
+ public static @NonNull IntentReader from(@NonNull Activity activity) {
return new IntentReader(activity);
}
@@ -784,8 +759,7 @@
* @return mimetype of the shared data
* @see Intent#getType()
*/
- @Nullable
- public String getType() {
+ public @Nullable String getType() {
return mIntent.getType();
}
@@ -795,8 +769,7 @@
* @return Literal shared text or null if none was supplied
* @see Intent#EXTRA_TEXT
*/
- @Nullable
- public CharSequence getText() {
+ public @Nullable CharSequence getText() {
return mIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
}
@@ -810,8 +783,7 @@
*
* @return Styled text provided by the sender as HTML.
*/
- @Nullable
- public String getHtmlText() {
+ public @Nullable String getHtmlText() {
String result = mIntent.getStringExtra(IntentCompat.EXTRA_HTML_TEXT);
if (result == null) {
CharSequence text = getText();
@@ -836,8 +808,7 @@
* @see Intent#EXTRA_STREAM
*/
@SuppressWarnings("deprecation")
- @Nullable
- public Uri getStream() {
+ public @Nullable Uri getStream() {
return mIntent.getParcelableExtra(Intent.EXTRA_STREAM);
}
@@ -851,8 +822,7 @@
* @see Intent#ACTION_SEND_MULTIPLE
*/
@SuppressWarnings("deprecation")
- @Nullable
- public Uri getStream(int index) {
+ public @Nullable Uri getStream(int index) {
if (mStreams == null && isMultipleShare()) {
mStreams = mIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
}
@@ -890,8 +860,7 @@
* @return An array of email addresses or null if none were supplied.
* @see Intent#EXTRA_EMAIL
*/
- @Nullable
- public String[] getEmailTo() {
+ public String @Nullable [] getEmailTo() {
return mIntent.getStringArrayExtra(Intent.EXTRA_EMAIL);
}
@@ -901,8 +870,7 @@
* @return An array of email addresses or null if none were supplied.
* @see Intent#EXTRA_CC
*/
- @Nullable
- public String[] getEmailCc() {
+ public String @Nullable [] getEmailCc() {
return mIntent.getStringArrayExtra(Intent.EXTRA_CC);
}
@@ -912,8 +880,7 @@
* @return An array of email addresses or null if none were supplied.
* @see Intent#EXTRA_BCC
*/
- @Nullable
- public String[] getEmailBcc() {
+ public String @Nullable [] getEmailBcc() {
return mIntent.getStringArrayExtra(Intent.EXTRA_BCC);
}
@@ -923,8 +890,7 @@
* @return The subject heading for this share or null if one was not supplied.
* @see Intent#EXTRA_SUBJECT
*/
- @Nullable
- public String getSubject() {
+ public @Nullable String getSubject() {
return mIntent.getStringExtra(Intent.EXTRA_SUBJECT);
}
@@ -942,8 +908,7 @@
* @see ShareCompat#EXTRA_CALLING_PACKAGE
* @see ShareCompat#EXTRA_CALLING_PACKAGE_INTEROP
*/
- @Nullable
- public String getCallingPackage() {
+ public @Nullable String getCallingPackage() {
return mCallingPackage;
}
@@ -961,8 +926,7 @@
* @see ShareCompat#EXTRA_CALLING_ACTIVITY
* @see ShareCompat#EXTRA_CALLING_ACTIVITY_INTEROP
*/
- @Nullable
- public ComponentName getCallingActivity() {
+ public @Nullable ComponentName getCallingActivity() {
return mCallingActivity;
}
@@ -976,8 +940,7 @@
*
* @return The calling Activity's icon or null if unknown
*/
- @Nullable
- public Drawable getCallingActivityIcon() {
+ public @Nullable Drawable getCallingActivityIcon() {
if (mCallingActivity == null) return null;
PackageManager pm = mContext.getPackageManager();
@@ -999,8 +962,7 @@
*
* @return The calling application's icon or null if unknown
*/
- @Nullable
- public Drawable getCallingApplicationIcon() {
+ public @Nullable Drawable getCallingApplicationIcon() {
if (mCallingPackage == null) return null;
PackageManager pm = mContext.getPackageManager();
@@ -1023,8 +985,7 @@
* @return The calling application's label or null if unknown
*/
@SuppressWarnings("deprecation")
- @Nullable
- public CharSequence getCallingApplicationLabel() {
+ public @Nullable CharSequence getCallingApplicationLabel() {
if (mCallingPackage == null) return null;
PackageManager pm = mContext.getPackageManager();
diff --git a/core/core/src/main/java/androidx/core/app/TaskStackBuilder.java b/core/core/src/main/java/androidx/core/app/TaskStackBuilder.java
index b077c0c..ee87201 100644
--- a/core/core/src/main/java/androidx/core/app/TaskStackBuilder.java
+++ b/core/core/src/main/java/androidx/core/app/TaskStackBuilder.java
@@ -25,10 +25,11 @@
import android.os.Bundle;
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.Iterator;
@@ -71,8 +72,7 @@
private static final String TAG = "TaskStackBuilder";
public interface SupportParentable {
- @Nullable
- Intent getSupportParentActivityIntent();
+ @Nullable Intent getSupportParentActivityIntent();
}
private final ArrayList<Intent> mIntents = new ArrayList<>();
@@ -89,8 +89,7 @@
* @param context The context that will launch the new task stack or generate a PendingIntent
* @return A new TaskStackBuilder
*/
- @NonNull
- public static TaskStackBuilder create(@NonNull Context context) {
+ public static @NonNull TaskStackBuilder create(@NonNull Context context) {
return new TaskStackBuilder(context);
}
@@ -115,8 +114,7 @@
* @param nextIntent Intent for the next Activity in the synthesized task stack
* @return This TaskStackBuilder for method chaining
*/
- @NonNull
- public TaskStackBuilder addNextIntent(@NonNull Intent nextIntent) {
+ public @NonNull TaskStackBuilder addNextIntent(@NonNull Intent nextIntent) {
mIntents.add(nextIntent);
return this;
}
@@ -133,8 +131,7 @@
* Its chain of parents as specified in the manifest will be added.
* @return This TaskStackBuilder for method chaining.
*/
- @NonNull
- public TaskStackBuilder addNextIntentWithParentStack(@NonNull Intent nextIntent) {
+ public @NonNull TaskStackBuilder addNextIntentWithParentStack(@NonNull Intent nextIntent) {
ComponentName target = nextIntent.getComponent();
if (target == null) {
target = nextIntent.resolveActivity(mSourceContext.getPackageManager());
@@ -153,8 +150,7 @@
* @param sourceActivity All parents of this activity will be added
* @return This TaskStackBuilder for method chaining
*/
- @NonNull
- public TaskStackBuilder addParentStack(@NonNull Activity sourceActivity) {
+ public @NonNull TaskStackBuilder addParentStack(@NonNull Activity sourceActivity) {
Intent parent = null;
if (sourceActivity instanceof SupportParentable) {
parent = ((SupportParentable) sourceActivity).getSupportParentActivityIntent();
@@ -183,8 +179,7 @@
* @param sourceActivityClass All parents of this activity will be added
* @return This TaskStackBuilder for method chaining
*/
- @NonNull
- public TaskStackBuilder addParentStack(@NonNull Class<?> sourceActivityClass) {
+ public @NonNull TaskStackBuilder addParentStack(@NonNull Class<?> sourceActivityClass) {
return addParentStack(new ComponentName(mSourceContext, sourceActivityClass));
}
@@ -196,8 +191,7 @@
* this activity will be added
* @return This TaskStackBuilder for method chaining
*/
- @NonNull
- public TaskStackBuilder addParentStack(@NonNull ComponentName sourceActivityName) {
+ public @NonNull TaskStackBuilder addParentStack(@NonNull ComponentName sourceActivityName) {
final int insertAt = mIntents.size();
try {
Intent parent = NavUtils.getParentActivityIntent(mSourceContext, sourceActivityName);
@@ -242,18 +236,16 @@
* @param index Index from 0-getIntentCount()
* @return the intent at position index
*/
- @Nullable
- public Intent editIntentAt(int index) {
+ public @Nullable Intent editIntentAt(int index) {
return mIntents.get(index);
}
/**
* @deprecated Use editIntentAt instead
*/
- @NonNull
@Override
@Deprecated
- public Iterator<Intent> iterator() {
+ public @NonNull Iterator<Intent> iterator() {
return mIntents.iterator();
}
@@ -308,8 +300,7 @@
* @return The obtained PendingIntent. May return null only if
* {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
*/
- @Nullable
- public PendingIntent getPendingIntent(int requestCode, int flags) {
+ public @Nullable PendingIntent getPendingIntent(int requestCode, int flags) {
return getPendingIntent(requestCode, flags, null);
}
@@ -319,8 +310,7 @@
* mutability flag. This method combines mutability flag when necessary. See {@link
* TaskStackBuilder#getPendingIntent(int, int)}.
*/
- @Nullable
- public PendingIntent getPendingIntent(
+ public @Nullable PendingIntent getPendingIntent(
int requestCode,
int flags,
boolean isMutable) {
@@ -343,8 +333,8 @@
* @return The obtained PendingIntent. May return null only if
* {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
*/
- @Nullable
- public PendingIntent getPendingIntent(int requestCode, int flags, @Nullable Bundle options) {
+ public @Nullable PendingIntent getPendingIntent(int requestCode, int flags,
+ @Nullable Bundle options) {
if (mIntents.isEmpty()) {
throw new IllegalStateException(
"No intents added to TaskStackBuilder; cannot getPendingIntent");
@@ -363,8 +353,7 @@
* mutability flag. This method combines mutability flag when necessary. See {@link
* TaskStackBuilder#getPendingIntent(int, int, Bundle)}.
*/
- @Nullable
- public PendingIntent getPendingIntent(
+ public @Nullable PendingIntent getPendingIntent(
int requestCode,
int flags,
@Nullable Bundle options,
@@ -382,8 +371,7 @@
*
* @return An array containing the intents added to this builder.
*/
- @NonNull
- public Intent[] getIntents() {
+ public Intent @NonNull [] getIntents() {
Intent[] intents = new Intent[mIntents.size()];
if (intents.length == 0) return intents;
diff --git a/core/core/src/main/java/androidx/core/content/ContentProviderCompat.java b/core/core/src/main/java/androidx/core/content/ContentProviderCompat.java
index 0d4f8b4..5cbbcd7 100644
--- a/core/core/src/main/java/androidx/core/content/ContentProviderCompat.java
+++ b/core/core/src/main/java/androidx/core/content/ContentProviderCompat.java
@@ -19,7 +19,7 @@
import android.content.ContentProvider;
import android.content.Context;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Helper for accessing features in {@link android.content.ContentProvider} in a backwards
@@ -41,7 +41,7 @@
* @return The {@link android.content.Context} object associated with the
* {@link android.content.ContentProvider}.
*/
- public static @NonNull Context requireContext(@NonNull final ContentProvider provider) {
+ public static @NonNull Context requireContext(final @NonNull ContentProvider provider) {
final Context ctx = provider.getContext();
if (ctx == null) {
throw new IllegalStateException("Cannot find context from the provider.");
diff --git a/core/core/src/main/java/androidx/core/content/ContentResolverCompat.java b/core/core/src/main/java/androidx/core/content/ContentResolverCompat.java
index c2fc5b2..2435568 100644
--- a/core/core/src/main/java/androidx/core/content/ContentResolverCompat.java
+++ b/core/core/src/main/java/androidx/core/content/ContentResolverCompat.java
@@ -21,10 +21,11 @@
import android.net.Uri;
import android.os.CancellationSignal;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.os.OperationCanceledException;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Helper for accessing features in {@link ContentResolver} in a backwards
* compatible fashion.
@@ -72,11 +73,10 @@
* {@link #query(ContentResolver, Uri, String[], String, String[], String, CancellationSignal)}
*/
@Deprecated
- @Nullable
- public static Cursor query(@NonNull ContentResolver resolver,
- @NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
- @Nullable String[] selectionArgs, @Nullable String sortOrder,
- @Nullable androidx.core.os.CancellationSignal cancellationSignal) {
+ public static @Nullable Cursor query(@NonNull ContentResolver resolver,
+ @NonNull Uri uri, String @Nullable [] projection, @Nullable String selection,
+ String @Nullable [] selectionArgs, @Nullable String sortOrder,
+ androidx.core.os.@Nullable CancellationSignal cancellationSignal) {
return query(resolver, uri, projection, selection, selectionArgs, sortOrder,
cancellationSignal != null
? (CancellationSignal) cancellationSignal.getCancellationSignalObject() :
@@ -118,10 +118,9 @@
* @return A Cursor object, which is positioned before the first entry, or null
* @see Cursor
*/
- @Nullable
- public static Cursor query(@NonNull ContentResolver resolver,
- @NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
- @Nullable String[] selectionArgs, @Nullable String sortOrder,
+ public static @Nullable Cursor query(@NonNull ContentResolver resolver,
+ @NonNull Uri uri, String @Nullable [] projection, @Nullable String selection,
+ String @Nullable [] selectionArgs, @Nullable String sortOrder,
@Nullable CancellationSignal cancellationSignal) {
try {
return resolver.query(uri, projection, selection, selectionArgs, sortOrder,
diff --git a/core/core/src/main/java/androidx/core/content/ContextCompat.java b/core/core/src/main/java/androidx/core/content/ContextCompat.java
index 076e416..b897682 100644
--- a/core/core/src/main/java/androidx/core/content/ContextCompat.java
+++ b/core/core/src/main/java/androidx/core/content/ContextCompat.java
@@ -145,8 +145,6 @@
import androidx.annotation.DisplayContext;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.app.ActivityOptionsCompat;
@@ -159,6 +157,9 @@
import androidx.core.os.LocaleListCompat;
import androidx.core.util.ObjectsCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -235,7 +236,7 @@
* length-1 will correspond to the top activity on the resulting task stack.
* @return true if the underlying API was available and the call was successful, false otherwise
*/
- public static boolean startActivities(@NonNull Context context, @NonNull Intent[] intents) {
+ public static boolean startActivities(@NonNull Context context, Intent @NonNull [] intents) {
return startActivities(context, intents, null);
}
@@ -266,7 +267,7 @@
* See {@link Context#startActivity(Intent, Bundle)}
* @return true if the underlying API was available and the call was successful, false otherwise
*/
- public static boolean startActivities(@NonNull Context context, @NonNull Intent[] intents,
+ public static boolean startActivities(@NonNull Context context, Intent @NonNull [] intents,
@Nullable Bundle options) {
context.startActivities(intents, options);
return true;
@@ -312,8 +313,7 @@
*
* @see ApplicationInfo#dataDir
*/
- @Nullable
- public static File getDataDir(@NonNull Context context) {
+ public static @Nullable File getDataDir(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= 24) {
return Api24Impl.getDataDir(context);
} else {
@@ -368,8 +368,7 @@
*/
@Deprecated
@androidx.annotation.ReplaceWith(expression = "context.getObbDirs()")
- @NonNull
- public static File[] getObbDirs(@NonNull Context context) {
+ public static File @NonNull [] getObbDirs(@NonNull Context context) {
return context.getObbDirs();
}
@@ -420,8 +419,8 @@
*/
@Deprecated
@androidx.annotation.ReplaceWith(expression = "context.getExternalFilesDirs(type)")
- @NonNull
- public static File[] getExternalFilesDirs(@NonNull Context context, @Nullable String type) {
+ public static File @NonNull [] getExternalFilesDirs(@NonNull Context context,
+ @Nullable String type) {
return context.getExternalFilesDirs(type);
}
@@ -472,8 +471,7 @@
*/
@Deprecated
@androidx.annotation.ReplaceWith(expression = "context.getExternalCacheDirs()")
- @NonNull
- public static File[] getExternalCacheDirs(@NonNull Context context) {
+ public static File @NonNull [] getExternalCacheDirs(@NonNull Context context) {
return context.getExternalCacheDirs();
}
@@ -490,8 +488,7 @@
* @return Drawable An object that can be used to draw this resource.
*/
@SuppressWarnings("deprecation")
- @Nullable
- public static Drawable getDrawable(@NonNull Context context, @DrawableRes int id) {
+ public static @Nullable Drawable getDrawable(@NonNull Context context, @DrawableRes int id) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.getDrawable(context, id);
} else {
@@ -514,8 +511,8 @@
* @throws android.content.res.Resources.NotFoundException if the given ID
* does not exist.
*/
- @Nullable
- public static ColorStateList getColorStateList(@NonNull Context context, @ColorRes int id) {
+ public static @Nullable ColorStateList getColorStateList(@NonNull Context context,
+ @ColorRes int id) {
return ResourcesCompat.getColorStateList(context.getResources(), id, context.getTheme());
}
@@ -576,8 +573,7 @@
* automatically backed up to remote storage.
* @see Context#getFilesDir()
*/
- @Nullable
- public static File getNoBackupFilesDir(@NonNull Context context) {
+ public static @Nullable File getNoBackupFilesDir(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.getNoBackupFilesDir(context);
} else {
@@ -601,8 +597,7 @@
*
* @return The path of the directory holding application code cache files.
*/
- @NonNull
- public static File getCodeCacheDir(@NonNull Context context) {
+ public static @NonNull File getCodeCacheDir(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.getCodeCacheDir(context);
} else {
@@ -661,8 +656,7 @@
*
* @see ContextCompat#isDeviceProtectedStorage(Context)
*/
- @Nullable
- public static Context createDeviceProtectedStorageContext(@NonNull Context context) {
+ public static @Nullable Context createDeviceProtectedStorageContext(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= 24) {
return Api24Impl.createDeviceProtectedStorageContext(context);
} else {
@@ -689,8 +683,7 @@
* thread associated with this context. This is the thread used to dispatch
* calls to application components (activities, services, etc).
*/
- @NonNull
- public static Executor getMainExecutor(@NonNull Context context) {
+ public static @NonNull Executor getMainExecutor(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= 28) {
return Api28Impl.getMainExecutor(context);
}
@@ -729,8 +722,7 @@
* @return The display associated with the Context or the default display if the context
* doesn't associated with any display.
*/
- @NonNull
- public static Display getDisplayOrDefault(@NonNull @DisplayContext Context context) {
+ public static @NonNull Display getDisplayOrDefault(@DisplayContext @NonNull Context context) {
if (Build.VERSION.SDK_INT >= 30) {
return Api30Impl.getDisplayOrDefault(context);
} else {
@@ -749,8 +741,8 @@
* @see Context#getSystemService(Class)
*/
@SuppressWarnings("unchecked")
- @Nullable
- public static <T> T getSystemService(@NonNull Context context, @NonNull Class<T> serviceClass) {
+ public static <T> @Nullable T getSystemService(@NonNull Context context,
+ @NonNull Class<T> serviceClass) {
if (Build.VERSION.SDK_INT >= 23) {
return Api23Impl.getSystemService(context, serviceClass);
}
@@ -774,8 +766,7 @@
* @see Context#registerReceiver(BroadcastReceiver, IntentFilter, int)
* @see https://developer.android.com/develop/background-work/background-tasks/broadcasts#context-registered-receivers
*/
- @Nullable
- public static Intent registerReceiver(@NonNull Context context,
+ public static @Nullable Intent registerReceiver(@NonNull Context context,
@Nullable BroadcastReceiver receiver, @NonNull IntentFilter filter,
@RegisterReceiverFlags int flags) {
return registerReceiver(context, receiver, filter, null, null, flags);
@@ -802,8 +793,7 @@
* @see Context#registerReceiver(BroadcastReceiver, IntentFilter, String, Handler, int)
* @see https://developer.android.com/develop/background-work/background-tasks/broadcasts#context-registered-receivers
*/
- @Nullable
- public static Intent registerReceiver(@NonNull Context context,
+ public static @Nullable Intent registerReceiver(@NonNull Context context,
@Nullable BroadcastReceiver receiver, @NonNull IntentFilter filter,
@Nullable String broadcastPermission,
@Nullable Handler scheduler, @RegisterReceiverFlags int flags) {
@@ -851,8 +841,7 @@
* @return The service name or null if the class is not a supported system service.
* @see Context#getSystemServiceName(Class)
*/
- @Nullable
- public static String getSystemServiceName(@NonNull Context context,
+ public static @Nullable String getSystemServiceName(@NonNull Context context,
@NonNull Class<?> serviceClass) {
if (Build.VERSION.SDK_INT >= 23) {
return Api23Impl.getSystemServiceName(context, serviceClass);
@@ -877,8 +866,7 @@
* </ul>
* </p>
*/
- @NonNull
- public static String getString(@NonNull Context context, int resId) {
+ public static @NonNull String getString(@NonNull Context context, int resId) {
return getContextForLanguage(context).getString(resId);
}
@@ -902,8 +890,7 @@
* </ul>
* </p>
*/
- @NonNull
- public static Context getContextForLanguage(@NonNull Context context) {
+ public static @NonNull Context getContextForLanguage(@NonNull Context context) {
LocaleListCompat locales = LocaleManagerCompat.getApplicationLocales(context);
// The Android framework supports per-app locales on API 33, so we assume the
@@ -932,8 +919,7 @@
*
* @return the attribution tag this context is for or {@code null} if this is the default.
*/
- @Nullable
- public static String getAttributionTag(@NonNull Context context) {
+ public static @Nullable String getAttributionTag(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= 30) {
return Api30Impl.getAttributionTag(context);
}
@@ -957,8 +943,7 @@
* @return A {@link Context} that is tagged for the new attribution
* @see #getAttributionTag(Context)
*/
- @NonNull
- public static Context createAttributionContext(@NonNull Context context,
+ public static @NonNull Context createAttributionContext(@NonNull Context context,
@Nullable String attributionTag) {
if (Build.VERSION.SDK_INT >= 30) {
return Api30Impl.createAttributionContext(context, attributionTag);
@@ -1169,8 +1154,7 @@
}
}
- @NonNull
- static Context createAttributionContext(@NonNull Context context,
+ static @NonNull Context createAttributionContext(@NonNull Context context,
@Nullable String attributionTag) {
return context.createAttributionContext(attributionTag);
}
diff --git a/core/core/src/main/java/androidx/core/content/FileProvider.java b/core/core/src/main/java/androidx/core/content/FileProvider.java
index 123322e..b3b6c92 100644
--- a/core/core/src/main/java/androidx/core/content/FileProvider.java
+++ b/core/core/src/main/java/androidx/core/content/FileProvider.java
@@ -46,13 +46,13 @@
import androidx.annotation.CallSuper;
import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.XmlRes;
import androidx.core.content.res.ResourcesCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
@@ -376,15 +376,13 @@
@GuardedBy("sCache")
private static final HashMap<String, PathStrategy> sCache = new HashMap<>();
- @NonNull
- private final Object mLock = new Object();
+ private final @NonNull Object mLock = new Object();
private final int mResourceId;
@GuardedBy("mLock")
private String mAuthority;
// Do NOT access directly! Use getLocalPathStrategy() instead.
@GuardedBy("mLock")
- @Nullable
- private PathStrategy mLocalPathStrategy;
+ private @Nullable PathStrategy mLocalPathStrategy;
public FileProvider() {
this(ResourcesCompat.ID_NULL);
@@ -443,8 +441,7 @@
* the paths supported by the provider.
*/
@SuppressLint("StreamFiles")
- @NonNull
- public static Uri getUriForFile(@NonNull Context context, @NonNull String authority,
+ public static @NonNull Uri getUriForFile(@NonNull Context context, @NonNull String authority,
@NonNull File file, @NonNull String displayName) {
Uri uri = getUriForFile(context, authority, file);
return uri.buildUpon().appendQueryParameter(DISPLAYNAME_FIELD, displayName).build();
@@ -602,8 +599,7 @@
return result;
}
- @NonNull
- private static String removeTrailingSlash(@NonNull String path) {
+ private static @NonNull String removeTrailingSlash(@NonNull String path) {
if (path.length() > 0 && path.charAt(path.length() - 1) == '/') {
return path.substring(0, path.length() - 1);
} else {
@@ -684,10 +680,10 @@
* the resulting {@link Cursor}.
* @return A {@link Cursor} containing the results of the query.
*/
- @NonNull
@Override
- public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
- @Nullable String[] selectionArgs, @Nullable String sortOrder) {
+ public @NonNull Cursor query(@NonNull Uri uri, String @Nullable [] projection,
+ @Nullable String selection, String @Nullable [] selectionArgs,
+ @Nullable String sortOrder) {
// ContentProvider has already checked granted permissions
final File file = getLocalPathStrategy().getFileForUri(uri);
String displayName = uri.getQueryParameter(DISPLAYNAME_FIELD);
@@ -726,9 +722,8 @@
* @return If the associated file has an extension, the MIME type associated with that
* extension; otherwise <code>application/octet-stream</code>.
*/
- @Nullable
@Override
- public String getType(@NonNull Uri uri) {
+ public @Nullable String getType(@NonNull Uri uri) {
// ContentProvider has already checked granted permissions
final File file = getLocalPathStrategy().getFileForUri(uri);
@@ -750,8 +745,7 @@
*/
//@Override
@SuppressWarnings("MissingOverride")
- @Nullable
- public String getTypeAnonymous(@NonNull Uri uri) {
+ public @Nullable String getTypeAnonymous(@NonNull Uri uri) {
return "application/octet-stream";
}
@@ -770,7 +764,7 @@
*/
@Override
public int update(@NonNull Uri uri, @NonNull ContentValues values, @Nullable String selection,
- @Nullable String[] selectionArgs) {
+ String @Nullable [] selectionArgs) {
throw new UnsupportedOperationException("No external updates");
}
@@ -787,7 +781,7 @@
*/
@Override
public int delete(@NonNull Uri uri, @Nullable String selection,
- @Nullable String[] selectionArgs) {
+ String @Nullable [] selectionArgs) {
// ContentProvider has already checked granted permissions
final File file = getLocalPathStrategy().getFileForUri(uri);
return file.delete() ? 1 : 0;
@@ -820,8 +814,7 @@
}
/** Return the local {@link PathStrategy}, creating it if necessary. */
- @NonNull
- private PathStrategy getLocalPathStrategy() {
+ private @NonNull PathStrategy getLocalPathStrategy() {
synchronized (mLock) {
requireNonNull(mAuthority, "mAuthority is null. Did you override attachInfo and "
+ "did not call super.attachInfo()?");
diff --git a/core/core/src/main/java/androidx/core/content/IntentCompat.java b/core/core/src/main/java/androidx/core/content/IntentCompat.java
index 7730a74..4691d30 100644
--- a/core/core/src/main/java/androidx/core/content/IntentCompat.java
+++ b/core/core/src/main/java/androidx/core/content/IntentCompat.java
@@ -31,10 +31,11 @@
import android.os.Build;
import android.os.Parcelable;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.Serializable;
import java.util.ArrayList;
@@ -117,8 +118,7 @@
* @return Returns a newly created Intent that can be used to launch the
* activity as a main application entry.
*/
- @NonNull
- public static Intent makeMainSelectorActivity(@NonNull String selectorAction,
+ public static @NonNull Intent makeMainSelectorActivity(@NonNull String selectorAction,
@NonNull String selectorCategory) {
return Intent.makeMainSelectorActivity(selectorAction, selectorCategory);
}
@@ -161,8 +161,7 @@
* @return Returns a newly created Intent that can be used to launch an activity where users
* can manage unused app restrictions for a specific app.
*/
- @NonNull
- public static Intent createManageUnusedAppRestrictionsIntent(@NonNull Context context,
+ public static @NonNull Intent createManageUnusedAppRestrictionsIntent(@NonNull Context context,
@NonNull String packageName) {
if (!areUnusedAppRestrictionsAvailable(context.getPackageManager())) {
throw new UnsupportedOperationException(
@@ -215,9 +214,8 @@
*
* @see Intent#putExtra(String, Parcelable)
*/
- @Nullable
@SuppressWarnings({"deprecation", "unchecked"})
- public static <T> T getParcelableExtra(@NonNull Intent in, @Nullable String name,
+ public static <T> @Nullable T getParcelableExtra(@NonNull Intent in, @Nullable String name,
@NonNull Class<T> clazz) {
if (Build.VERSION.SDK_INT >= 34) {
// Don't call this API on SDK 33 due to b/232589966.
@@ -246,11 +244,10 @@
*
* @see Intent#putExtra(String, Parcelable[])
*/
- @Nullable
@SuppressWarnings({"deprecation"})
@SuppressLint({"ArrayReturn", "NullableCollection"})
- public static Parcelable[] getParcelableArrayExtra(@NonNull Intent in, @Nullable String name,
- @NonNull Class<? extends Parcelable> clazz) {
+ public static Parcelable @Nullable [] getParcelableArrayExtra(@NonNull Intent in,
+ @Nullable String name, @NonNull Class<? extends Parcelable> clazz) {
if (Build.VERSION.SDK_INT >= 34) {
// Don't call this API on SDK 33 due to b/232589966.
return Api33Impl.getParcelableArrayExtra(in, name, clazz);
@@ -279,10 +276,9 @@
*
* @see Intent#putParcelableArrayListExtra(String, ArrayList)
*/
- @Nullable
@SuppressWarnings({"deprecation", "unchecked"})
@SuppressLint({"ConcreteCollection", "NullableCollection"})
- public static <T> ArrayList<T> getParcelableArrayListExtra(
+ public static <T> @Nullable ArrayList<T> getParcelableArrayListExtra(
@NonNull Intent in, @Nullable String name, @NonNull Class<? extends T> clazz) {
if (Build.VERSION.SDK_INT >= 34) {
// Don't call this API on SDK 33 due to b/232589966.
@@ -312,8 +308,7 @@
* @return a Serializable value, or {@code null}
*/
@SuppressWarnings({"deprecation", "unchecked"})
- @Nullable
- public static <T extends Serializable> T getSerializableExtra(@NonNull Intent in,
+ public static <T extends Serializable> @Nullable T getSerializableExtra(@NonNull Intent in,
@Nullable String key, @NonNull Class<T> clazz) {
if (Build.VERSION.SDK_INT >= 34) {
// Don't call this API on SDK 33 due to b/232589966.
diff --git a/core/core/src/main/java/androidx/core/content/IntentSanitizer.java b/core/core/src/main/java/androidx/core/content/IntentSanitizer.java
index 9297ccf..2c3071e 100644
--- a/core/core/src/main/java/androidx/core/content/IntentSanitizer.java
+++ b/core/core/src/main/java/androidx/core/content/IntentSanitizer.java
@@ -29,11 +29,12 @@
import android.os.strictmode.UnsafeIntentLaunchViolation;
import android.provider.MediaStore;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.util.Consumer;
import androidx.core.util.Predicate;
+import org.jspecify.annotations.NonNull;
+
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
@@ -87,8 +88,7 @@
* @param in input intent
* @return a copy of the input intent after filtering out unwanted members.
*/
- @NonNull
- public Intent sanitizeByFiltering(@NonNull Intent in) {
+ public @NonNull Intent sanitizeByFiltering(@NonNull Intent in) {
return sanitize(in, msg -> {});
}
@@ -100,8 +100,7 @@
* @return a copy of the input intent if the input intent does not contain any unwanted members.
* @throws SecurityException if the input intent contains any unwanted members.
*/
- @NonNull
- public Intent sanitizeByThrowing(@NonNull Intent in) {
+ public @NonNull Intent sanitizeByThrowing(@NonNull Intent in) {
return sanitize(in, msg -> {
throw new SecurityException(msg);
});
@@ -116,8 +115,7 @@
* @param penalty consumer of the error message if dirty members are found.
* @return a sanitized copy of the given intent.
*/
- @NonNull
- public Intent sanitize(@NonNull Intent in,
+ public @NonNull Intent sanitize(@NonNull Intent in,
@NonNull Consumer<String> penalty) {
Intent intent = new Intent();
@@ -322,8 +320,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowFlags(int flags) {
+ public @NonNull Builder allowFlags(int flags) {
mAllowedFlags |= flags;
return this;
}
@@ -353,8 +350,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowHistoryStackFlags() {
+ public @NonNull Builder allowHistoryStackFlags() {
mAllowedFlags |= HISTORY_STACK_FLAGS;
return this;
}
@@ -372,8 +368,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowReceiverFlags() {
+ public @NonNull Builder allowReceiverFlags() {
mAllowedFlags |= RECEIVER_FLAGS;
return this;
}
@@ -387,8 +382,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowAction(@NonNull String action) {
+ public @NonNull Builder allowAction(@NonNull String action) {
checkNotNull(action);
allowAction(action::equals);
return this;
@@ -403,8 +397,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowAction(@NonNull Predicate<String> filter) {
+ public @NonNull Builder allowAction(@NonNull Predicate<String> filter) {
checkNotNull(filter);
mAllowedActions = mAllowedActions.or(filter);
return this;
@@ -419,8 +412,7 @@
* @return this builder
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowDataWithAuthority(@NonNull String authority) {
+ public @NonNull Builder allowDataWithAuthority(@NonNull String authority) {
checkNotNull(authority);
allowData(v -> authority.equals(v.getAuthority()));
return this;
@@ -434,8 +426,7 @@
* @param filter data filter.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowData(@NonNull Predicate<Uri> filter) {
+ public @NonNull Builder allowData(@NonNull Predicate<Uri> filter) {
checkNotNull(filter);
mAllowedData = mAllowedData.or(filter);
return this;
@@ -452,8 +443,7 @@
* @return this builder
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowType(@NonNull String type) {
+ public @NonNull Builder allowType(@NonNull String type) {
checkNotNull(type);
return allowType(type::equals);
}
@@ -466,8 +456,7 @@
* @param filter the data type filter.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowType(@NonNull Predicate<String> filter) {
+ public @NonNull Builder allowType(@NonNull Predicate<String> filter) {
checkNotNull(filter);
mAllowedTypes = mAllowedTypes.or(filter);
return this;
@@ -482,8 +471,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowCategory(@NonNull String category) {
+ public @NonNull Builder allowCategory(@NonNull String category) {
checkNotNull(category);
return allowCategory(category::equals);
}
@@ -497,8 +485,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowCategory(@NonNull Predicate<String> filter) {
+ public @NonNull Builder allowCategory(@NonNull Predicate<String> filter) {
checkNotNull(filter);
mAllowedCategories = mAllowedCategories.or(filter);
return this;
@@ -514,8 +501,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowPackage(@NonNull String packageName) {
+ public @NonNull Builder allowPackage(@NonNull String packageName) {
checkNotNull(packageName);
return allowPackage(packageName::equals);
}
@@ -531,8 +517,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowPackage(@NonNull Predicate<String> filter) {
+ public @NonNull Builder allowPackage(@NonNull Predicate<String> filter) {
checkNotNull(filter);
mAllowedPackages = mAllowedPackages.or(filter);
return this;
@@ -547,8 +532,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowComponent(@NonNull ComponentName component) {
+ public @NonNull Builder allowComponent(@NonNull ComponentName component) {
checkNotNull(component);
return allowComponent(component::equals);
}
@@ -562,8 +546,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowComponent(@NonNull Predicate<ComponentName> filter) {
+ public @NonNull Builder allowComponent(@NonNull Predicate<ComponentName> filter) {
checkNotNull(filter);
mAllowSomeComponents = true;
mAllowedComponents = mAllowedComponents.or(filter);
@@ -579,8 +562,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowComponentWithPackage(@NonNull String packageName) {
+ public @NonNull Builder allowComponentWithPackage(@NonNull String packageName) {
checkNotNull(packageName);
return allowComponent(v -> packageName.equals(v.getPackageName()));
}
@@ -595,8 +577,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowAnyComponent() {
+ public @NonNull Builder allowAnyComponent() {
mAllowAnyComponent = true;
mAllowedComponents = v -> true;
return this;
@@ -609,8 +590,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowClipDataText() {
+ public @NonNull Builder allowClipDataText() {
mAllowClipDataText = true;
return this;
}
@@ -624,8 +604,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowClipDataUriWithAuthority(@NonNull String authority) {
+ public @NonNull Builder allowClipDataUriWithAuthority(@NonNull String authority) {
checkNotNull(authority);
return allowClipDataUri(v -> authority.equals(v.getAuthority()));
}
@@ -640,8 +619,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowClipDataUri(@NonNull Predicate<Uri> filter) {
+ public @NonNull Builder allowClipDataUri(@NonNull Predicate<Uri> filter) {
checkNotNull(filter);
mAllowedClipDataUri = mAllowedClipDataUri.or(filter);
return this;
@@ -657,8 +635,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowClipData(@NonNull Predicate<ClipData> filter) {
+ public @NonNull Builder allowClipData(@NonNull Predicate<ClipData> filter) {
checkNotNull(filter);
mAllowedClipData = mAllowedClipData.or(filter);
return this;
@@ -674,8 +651,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowExtra(@NonNull String key, @NonNull Class<?> clazz) {
+ public @NonNull Builder allowExtra(@NonNull String key, @NonNull Class<?> clazz) {
return allowExtra(key, clazz, (v) -> true);
}
@@ -692,8 +668,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public <T> Builder allowExtra(@NonNull String key, @NonNull Class<T> clazz,
+ public <T> @NonNull Builder allowExtra(@NonNull String key, @NonNull Class<T> clazz,
@NonNull Predicate<T> valueFilter) {
checkNotNull(key);
checkNotNull(clazz);
@@ -712,8 +687,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowExtra(@NonNull String key, @NonNull Predicate<Object> filter) {
+ public @NonNull Builder allowExtra(@NonNull String key, @NonNull Predicate<Object> filter) {
checkNotNull(key);
checkNotNull(filter);
Predicate<Object> allowedExtra = mAllowedExtras.get(key);
@@ -736,8 +710,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowExtraStreamUriWithAuthority(@NonNull String uriAuthority) {
+ public @NonNull Builder allowExtraStreamUriWithAuthority(@NonNull String uriAuthority) {
checkNotNull(uriAuthority);
allowExtra(Intent.EXTRA_STREAM, Uri.class,
(v) -> uriAuthority.equals(v.getAuthority()));
@@ -757,8 +730,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowExtraStream(@NonNull Predicate<Uri> filter) {
+ public @NonNull Builder allowExtraStream(@NonNull Predicate<Uri> filter) {
allowExtra(Intent.EXTRA_STREAM, Uri.class, filter);
return this;
}
@@ -777,8 +749,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowExtraOutput(@NonNull String uriAuthority) {
+ public @NonNull Builder allowExtraOutput(@NonNull String uriAuthority) {
allowExtra(MediaStore.EXTRA_OUTPUT, Uri.class,
(v) -> uriAuthority.equals(v.getAuthority()));
return this;
@@ -798,8 +769,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowExtraOutput(@NonNull Predicate<Uri> filter) {
+ public @NonNull Builder allowExtraOutput(@NonNull Predicate<Uri> filter) {
allowExtra(MediaStore.EXTRA_OUTPUT, Uri.class, filter);
return this;
}
@@ -810,8 +780,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowIdentifier() {
+ public @NonNull Builder allowIdentifier() {
mAllowIdentifier = true;
return this;
}
@@ -822,8 +791,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowSelector() {
+ public @NonNull Builder allowSelector() {
mAllowSelector = true;
return this;
}
@@ -834,8 +802,7 @@
* @return this builder.
*/
@SuppressLint("BuilderSetStyle")
- @NonNull
- public Builder allowSourceBounds() {
+ public @NonNull Builder allowSourceBounds() {
mAllowSourceBounds = true;
return this;
}
@@ -845,8 +812,7 @@
*
* @return the IntentSanitizer
*/
- @NonNull
- public IntentSanitizer build() {
+ public @NonNull IntentSanitizer build() {
if ((mAllowAnyComponent && mAllowSomeComponents)
|| (!mAllowAnyComponent && !mAllowSomeComponents)) {
throw new SecurityException(
diff --git a/core/core/src/main/java/androidx/core/content/LocusIdCompat.java b/core/core/src/main/java/androidx/core/content/LocusIdCompat.java
index 3349721..fb76614 100644
--- a/core/core/src/main/java/androidx/core/content/LocusIdCompat.java
+++ b/core/core/src/main/java/androidx/core/content/LocusIdCompat.java
@@ -20,11 +20,12 @@
import android.content.LocusId;
import android.os.Build;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* An identifier for an unique state (locus) in the application. Should be stable across reboots and
* backup / restore.
@@ -89,8 +90,7 @@
/**
* Gets the canonical {@code id} associated with the locus.
*/
- @NonNull
- public String getId() {
+ public @NonNull String getId() {
return mId;
}
@@ -103,7 +103,7 @@
}
@Override
- public boolean equals(@Nullable final Object obj) {
+ public boolean equals(final @Nullable Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
@@ -115,34 +115,30 @@
}
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
return "LocusIdCompat[" + getSanitizedId() + "]";
}
/**
* @return {@link LocusId} object from this compat object.
*/
- @NonNull
@RequiresApi(29)
- public LocusId toLocusId() {
+ public @NonNull LocusId toLocusId() {
return mWrapped;
}
/**
* Returns an instance of LocusIdCompat from given {@link LocusId}.
*/
- @NonNull
@RequiresApi(29)
- public static LocusIdCompat toLocusIdCompat(@NonNull final LocusId locusId) {
+ public static @NonNull LocusIdCompat toLocusIdCompat(final @NonNull LocusId locusId) {
Preconditions.checkNotNull(locusId, "locusId cannot be null");
return new LocusIdCompat(Preconditions.checkStringNotEmpty(Api29Impl.getId(locusId),
"id cannot be empty"));
}
- @NonNull
- private String getSanitizedId() {
+ private @NonNull String getSanitizedId() {
final int size = mId.length();
return size + "_chars";
}
@@ -154,16 +150,14 @@
/**
* @return {@link LocusId} object from this compat object.
*/
- @NonNull
- static LocusId create(@NonNull final String id) {
+ static @NonNull LocusId create(final @NonNull String id) {
return new LocusId(id);
}
/**
* @return {@code id} from the LocusId object.
*/
- @NonNull
- static String getId(@NonNull final LocusId obj) {
+ static @NonNull String getId(final @NonNull LocusId obj) {
return obj.getId();
}
}
diff --git a/core/core/src/main/java/androidx/core/content/MimeTypeFilter.java b/core/core/src/main/java/androidx/core/content/MimeTypeFilter.java
index 7075f87..0ad3062 100644
--- a/core/core/src/main/java/androidx/core/content/MimeTypeFilter.java
+++ b/core/core/src/main/java/androidx/core/content/MimeTypeFilter.java
@@ -16,8 +16,8 @@
package androidx.core.content;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
@@ -44,7 +44,7 @@
}
private static boolean mimeTypeAgainstFilter(
- @NonNull String[] mimeTypeParts, @NonNull String[] filterParts) {
+ String @NonNull [] mimeTypeParts, String @NonNull [] filterParts) {
if (filterParts.length != 2) {
throw new IllegalArgumentException(
"Ill-formatted MIME type filter. Must be type/subtype.");
@@ -87,9 +87,8 @@
* Matches one nullable MIME type against an array of MIME type filters.
* @return The first matching filter, or null if nothing matches.
*/
- @Nullable
- public static String matches(
- @Nullable String mimeType, @NonNull String[] filters) {
+ public static @Nullable String matches(
+ @Nullable String mimeType, String @NonNull [] filters) {
if (mimeType == null) {
return null;
}
@@ -109,9 +108,8 @@
* Matches multiple MIME types against an array of MIME type filters.
* @return The first matching MIME type, or null if nothing matches.
*/
- @Nullable
- public static String matches(
- @Nullable String[] mimeTypes, @NonNull String filter) {
+ public static @Nullable String matches(
+ String @Nullable [] mimeTypes, @NonNull String filter) {
if (mimeTypes == null) {
return null;
}
@@ -131,9 +129,8 @@
* Matches multiple MIME types against an array of MIME type filters.
* @return The list of matching MIME types, or empty array if nothing matches.
*/
- @NonNull
- public static String[] matchesMany(
- @Nullable String[] mimeTypes, @NonNull String filter) {
+ public static String @NonNull [] matchesMany(
+ String @Nullable [] mimeTypes, @NonNull String filter) {
if (mimeTypes == null) {
return new String[] {};
}
diff --git a/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java b/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java
index 6bee967..7b95c6f 100644
--- a/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java
@@ -36,8 +36,6 @@
import android.util.Log;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.concurrent.futures.ResolvableFuture;
@@ -45,6 +43,9 @@
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.util.List;
import java.util.concurrent.Executors;
@@ -129,8 +130,7 @@
* not exist yet.
* </ul>
*/
- @NonNull
- public static ListenableFuture<Integer> getUnusedAppRestrictionsStatus(
+ public static @NonNull ListenableFuture<Integer> getUnusedAppRestrictionsStatus(
@NonNull Context context) {
ResolvableFuture<Integer> resultFuture = ResolvableFuture.create();
// If the user is in locked direct boot mode, return error as we cannot access the
@@ -211,10 +211,9 @@
* Verifiers exist, this method will return the first Verifier's package name.
*
*/
- @Nullable
@RestrictTo(LIBRARY)
@SuppressWarnings("deprecation")
- public static String getPermissionRevocationVerifierApp(
+ public static @Nullable String getPermissionRevocationVerifierApp(
@NonNull PackageManager packageManager) {
Intent permissionRevocationSettingsIntent =
new Intent(ACTION_PERMISSION_REVOCATION_SETTINGS)
diff --git a/core/core/src/main/java/androidx/core/content/PermissionChecker.java b/core/core/src/main/java/androidx/core/content/PermissionChecker.java
index 12bec8f..459ac9a 100644
--- a/core/core/src/main/java/androidx/core/content/PermissionChecker.java
+++ b/core/core/src/main/java/androidx/core/content/PermissionChecker.java
@@ -24,12 +24,13 @@
import android.os.Process;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.app.AppOpsManagerCompat;
import androidx.core.util.ObjectsCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
diff --git a/core/core/src/main/java/androidx/core/content/SharedPreferencesCompat.java b/core/core/src/main/java/androidx/core/content/SharedPreferencesCompat.java
index d780f6f..c8db841 100644
--- a/core/core/src/main/java/androidx/core/content/SharedPreferencesCompat.java
+++ b/core/core/src/main/java/androidx/core/content/SharedPreferencesCompat.java
@@ -18,7 +18,7 @@
import android.content.SharedPreferences;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* @deprecated This compatibility class is no longer required. Use {@link SharedPreferences}
@@ -40,7 +40,7 @@
Helper() {
}
- public void apply(@NonNull SharedPreferences.Editor editor) {
+ public void apply(SharedPreferences.@NonNull Editor editor) {
try {
editor.apply();
} catch (AbstractMethodError unused) {
@@ -73,7 +73,7 @@
* {@link SharedPreferences.Editor#apply()} directly.
*/
@Deprecated
- public void apply(@NonNull SharedPreferences.Editor editor) {
+ public void apply(SharedPreferences.@NonNull Editor editor) {
// Note that this redirection is needed to not break the public API chain
// of getInstance().apply() calls. Otherwise this method could (and should)
// be static.
diff --git a/core/core/src/main/java/androidx/core/content/UnusedAppRestrictionsBackportCallback.java b/core/core/src/main/java/androidx/core/content/UnusedAppRestrictionsBackportCallback.java
index 755717e..731fe80 100644
--- a/core/core/src/main/java/androidx/core/content/UnusedAppRestrictionsBackportCallback.java
+++ b/core/core/src/main/java/androidx/core/content/UnusedAppRestrictionsBackportCallback.java
@@ -20,10 +20,11 @@
import android.os.RemoteException;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.core.app.unusedapprestrictions.IUnusedAppRestrictionsBackportCallback;
+import org.jspecify.annotations.NonNull;
+
/**
* Wrapper class for {IUnusedAppRestrictionsBackportCallback}.
*
diff --git a/core/core/src/main/java/androidx/core/content/UnusedAppRestrictionsBackportService.java b/core/core/src/main/java/androidx/core/content/UnusedAppRestrictionsBackportService.java
index 8bf7c83..91d9a2f 100644
--- a/core/core/src/main/java/androidx/core/content/UnusedAppRestrictionsBackportService.java
+++ b/core/core/src/main/java/androidx/core/content/UnusedAppRestrictionsBackportService.java
@@ -22,11 +22,12 @@
import android.os.IBinder;
import android.os.RemoteException;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.app.unusedapprestrictions.IUnusedAppRestrictionsBackportCallback;
import androidx.core.app.unusedapprestrictions.IUnusedAppRestrictionsBackportService;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Wrapper class for {@link IUnusedAppRestrictionsBackportService}.
*
@@ -64,9 +65,8 @@
}
};
- @Nullable
@Override
- public IBinder onBind(@Nullable Intent intent) {
+ public @Nullable IBinder onBind(@Nullable Intent intent) {
return mBinder;
}
diff --git a/core/core/src/main/java/androidx/core/content/UnusedAppRestrictionsBackportServiceConnection.java b/core/core/src/main/java/androidx/core/content/UnusedAppRestrictionsBackportServiceConnection.java
index b58feab..b211856 100644
--- a/core/core/src/main/java/androidx/core/content/UnusedAppRestrictionsBackportServiceConnection.java
+++ b/core/core/src/main/java/androidx/core/content/UnusedAppRestrictionsBackportServiceConnection.java
@@ -30,13 +30,14 @@
import android.os.RemoteException;
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.concurrent.futures.ResolvableFuture;
import androidx.core.app.unusedapprestrictions.IUnusedAppRestrictionsBackportCallback;
import androidx.core.app.unusedapprestrictions.IUnusedAppRestrictionsBackportService;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* {@link ServiceConnection} to use while binding to a
* {@link IUnusedAppRestrictionsBackportService}.
diff --git a/core/core/src/main/java/androidx/core/content/UriMatcherCompat.java b/core/core/src/main/java/androidx/core/content/UriMatcherCompat.java
index e3b33c7..50a5e7d 100644
--- a/core/core/src/main/java/androidx/core/content/UriMatcherCompat.java
+++ b/core/core/src/main/java/androidx/core/content/UriMatcherCompat.java
@@ -19,9 +19,10 @@
import android.content.UriMatcher;
import android.net.Uri;
-import androidx.annotation.NonNull;
import androidx.core.util.Predicate;
+import org.jspecify.annotations.NonNull;
+
/**
* Helper for accessing {@link UriMatcher} to create Uri Predicate.
*/
@@ -35,8 +36,7 @@
* @param matcher the uriMatcher.
* @return the predicate created from the uriMatcher.
*/
- @NonNull
- public static Predicate<Uri> asPredicate(@NonNull UriMatcher matcher) {
+ public static @NonNull Predicate<Uri> asPredicate(@NonNull UriMatcher matcher) {
return v -> matcher.match(v) != UriMatcher.NO_MATCH;
}
}
diff --git a/core/core/src/main/java/androidx/core/content/pm/PackageInfoCompat.java b/core/core/src/main/java/androidx/core/content/pm/PackageInfoCompat.java
index be98811..b3743c7 100644
--- a/core/core/src/main/java/androidx/core/content/pm/PackageInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/content/pm/PackageInfoCompat.java
@@ -23,11 +23,12 @@
import android.content.pm.SigningInfo;
import android.os.Build;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.Size;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
@@ -79,9 +80,8 @@
* @throws PackageManager.NameNotFoundException if the package cannot be found through the
* provided {@param packageManager}
*/
- @NonNull
@SuppressWarnings("deprecation")
- public static List<Signature> getSignatures(@NonNull PackageManager packageManager,
+ public static @NonNull List<Signature> getSignatures(@NonNull PackageManager packageManager,
@NonNull String packageName) throws PackageManager.NameNotFoundException {
Signature[] array;
if (Build.VERSION.SDK_INT >= 28) {
@@ -241,7 +241,7 @@
return false;
}
- private static boolean byteArrayContains(@NonNull byte[][] array, @NonNull byte[] expected) {
+ private static boolean byteArrayContains(byte @NonNull [][] array, byte @NonNull [] expected) {
for (byte[] item : array) {
if (Arrays.equals(expected, item)) {
return true;
@@ -269,7 +269,7 @@
}
static boolean hasSigningCertificate(@NonNull PackageManager packageManager,
- @NonNull String packageName, @NonNull byte[] bytes, int type) {
+ @NonNull String packageName, byte @NonNull [] bytes, int type) {
return packageManager.hasSigningCertificate(packageName, bytes, type);
}
@@ -277,13 +277,12 @@
return signingInfo.hasMultipleSigners();
}
- @Nullable
- static Signature[] getApkContentsSigners(@NonNull SigningInfo signingInfo) {
+ static Signature @Nullable [] getApkContentsSigners(@NonNull SigningInfo signingInfo) {
return signingInfo.getApkContentsSigners();
}
- @Nullable
- static Signature[] getSigningCertificateHistory(@NonNull SigningInfo signingInfo) {
+ static Signature @Nullable [] getSigningCertificateHistory(
+ @NonNull SigningInfo signingInfo) {
return signingInfo.getSigningCertificateHistory();
}
diff --git a/core/core/src/main/java/androidx/core/content/pm/PermissionInfoCompat.java b/core/core/src/main/java/androidx/core/content/pm/PermissionInfoCompat.java
index 8cf8f7e..591d427 100644
--- a/core/core/src/main/java/androidx/core/content/pm/PermissionInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/content/pm/PermissionInfoCompat.java
@@ -21,10 +21,11 @@
import android.os.Build;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
diff --git a/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoChangeListener.java b/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoChangeListener.java
index 06bab24..39bc7b9 100644
--- a/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoChangeListener.java
+++ b/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoChangeListener.java
@@ -19,9 +19,10 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
import androidx.annotation.AnyThread;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
import java.util.List;
/**
diff --git a/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java b/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java
index 271eee3..30cf54a 100644
--- a/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java
@@ -33,8 +33,6 @@
import android.text.TextUtils;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
@@ -45,6 +43,9 @@
import androidx.core.net.UriCompat;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
@@ -93,8 +94,7 @@
Person[] mPersons;
Set<String> mCategories;
- @Nullable
- LocusIdCompat mLocusId;
+ @Nullable LocusIdCompat mLocusId;
// TODO: Support |auto| when the value of mIsLongLived is not set
boolean mIsLongLived;
@@ -221,16 +221,14 @@
* devices so that shortcuts will still be valid when restored on a different device.
* See {@link android.content.pm.ShortcutManager} for details.
*/
- @NonNull
- public String getId() {
+ public @NonNull String getId() {
return mId;
}
/**
* Return the package name of the publisher app.
*/
- @NonNull
- public String getPackage() {
+ public @NonNull String getPackage() {
return mPackageName;
}
@@ -243,8 +241,7 @@
*
* @see Builder#setActivity(ComponentName)
*/
- @Nullable
- public ComponentName getActivity() {
+ public @Nullable ComponentName getActivity() {
return mActivity;
}
@@ -253,8 +250,7 @@
*
* @see Builder#setShortLabel(CharSequence)
*/
- @NonNull
- public CharSequence getShortLabel() {
+ public @NonNull CharSequence getShortLabel() {
return mLabel;
}
@@ -263,8 +259,7 @@
*
* @see Builder#setLongLabel(CharSequence)
*/
- @Nullable
- public CharSequence getLongLabel() {
+ public @Nullable CharSequence getLongLabel() {
return mLongLabel;
}
@@ -274,8 +269,7 @@
*
* @see Builder#setDisabledMessage(CharSequence)
*/
- @Nullable
- public CharSequence getDisabledMessage() {
+ public @Nullable CharSequence getDisabledMessage() {
return mDisabledMessage;
}
@@ -292,8 +286,7 @@
*
* @see Builder#setIntent(Intent)
*/
- @NonNull
- public Intent getIntent() {
+ public @NonNull Intent getIntent() {
return mIntents[mIntents.length - 1];
}
@@ -302,8 +295,7 @@
*
* @see Builder#setIntents(Intent[])
*/
- @NonNull
- public Intent[] getIntents() {
+ public Intent @NonNull [] getIntents() {
return Arrays.copyOf(mIntents, mIntents.length);
}
@@ -312,8 +304,7 @@
*
* @see Builder#setCategories(Set)
*/
- @Nullable
- public Set<String> getCategories() {
+ public @Nullable Set<String> getCategories() {
return mCategories;
}
@@ -324,8 +315,7 @@
* {@link androidx.core.app.NotificationCompat} and
* {@link android.view.contentcapture.ContentCaptureContext}) that are correlated.
*/
- @Nullable
- public LocusIdCompat getLocusId() {
+ public @Nullable LocusIdCompat getLocusId() {
return mLocusId;
}
@@ -350,8 +340,7 @@
@RequiresApi(25)
@RestrictTo(LIBRARY_GROUP_PREFIX)
@VisibleForTesting
- @Nullable
- static Person[] getPersonsFromExtra(@NonNull PersistableBundle bundle) {
+ static Person @Nullable [] getPersonsFromExtra(@NonNull PersistableBundle bundle) {
if (bundle == null || !bundle.containsKey(EXTRA_PERSON_COUNT)) {
return null;
}
@@ -381,8 +370,8 @@
*/
@RequiresApi(25)
@RestrictTo(LIBRARY_GROUP_PREFIX)
- static List<ShortcutInfoCompat> fromShortcuts(@NonNull final Context context,
- @NonNull final List<ShortcutInfo> shortcuts) {
+ static List<ShortcutInfoCompat> fromShortcuts(final @NonNull Context context,
+ final @NonNull List<ShortcutInfo> shortcuts) {
final List<ShortcutInfoCompat> results = new ArrayList<>(shortcuts.size());
for (ShortcutInfo s : shortcuts) {
results.add(new ShortcutInfoCompat.Builder(context, s).build());
@@ -390,8 +379,7 @@
return results;
}
- @Nullable
- public PersistableBundle getExtras() {
+ public @Nullable PersistableBundle getExtras() {
return mExtras;
}
@@ -400,16 +388,14 @@
* shortcut is published.
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @Nullable
- public Bundle getTransientExtras() {
+ public @Nullable Bundle getTransientExtras() {
return mTransientExtras;
}
/**
* {@link UserHandle} on which the publisher created this shortcut.
*/
- @Nullable
- public UserHandle getUserHandle() {
+ public @Nullable UserHandle getUserHandle() {
return mUser;
}
@@ -494,8 +480,7 @@
}
@RequiresApi(25)
- @Nullable
- static LocusIdCompat getLocusId(@NonNull final ShortcutInfo shortcutInfo) {
+ static @Nullable LocusIdCompat getLocusId(final @NonNull ShortcutInfo shortcutInfo) {
if (Build.VERSION.SDK_INT >= 29) {
if (shortcutInfo.getLocusId() == null) return null;
return LocusIdCompat.toLocusIdCompat(shortcutInfo.getLocusId());
@@ -525,8 +510,7 @@
*/
@RequiresApi(25)
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @Nullable
- private static LocusIdCompat getLocusIdFromExtra(@Nullable PersistableBundle bundle) {
+ private static @Nullable LocusIdCompat getLocusIdFromExtra(@Nullable PersistableBundle bundle) {
if (bundle == null) return null;
final String locusId = bundle.getString(EXTRA_LOCUS_ID);
return locusId == null ? null : new LocusIdCompat(locusId);
@@ -638,8 +622,7 @@
*
* <p>The recommended maximum length is 10 characters.
*/
- @NonNull
- public Builder setShortLabel(@NonNull CharSequence shortLabel) {
+ public @NonNull Builder setShortLabel(@NonNull CharSequence shortLabel) {
mInfo.mLabel = shortLabel;
return this;
}
@@ -652,8 +635,7 @@
*
* <p>The recommend maximum length is 25 characters.
*/
- @NonNull
- public Builder setLongLabel(@NonNull CharSequence longLabel) {
+ public @NonNull Builder setLongLabel(@NonNull CharSequence longLabel) {
mInfo.mLongLabel = longLabel;
return this;
}
@@ -664,8 +646,7 @@
*
* @see ShortcutInfo#getDisabledMessage()
*/
- @NonNull
- public Builder setDisabledMessage(@NonNull CharSequence disabledMessage) {
+ public @NonNull Builder setDisabledMessage(@NonNull CharSequence disabledMessage) {
mInfo.mDisabledMessage = disabledMessage;
return this;
}
@@ -679,8 +660,7 @@
* <p>The given {@code intent} can contain extras, but these extras must contain values
* of primitive types in order for the system to persist these values.
*/
- @NonNull
- public Builder setIntent(@NonNull Intent intent) {
+ public @NonNull Builder setIntent(@NonNull Intent intent) {
return setIntents(new Intent[]{intent});
}
@@ -690,8 +670,7 @@
* intents. The last element in the list represents the only intent that doesn't place
* an activity on the back stack.
*/
- @NonNull
- public Builder setIntents(@NonNull Intent[] intents) {
+ public @NonNull Builder setIntents(Intent @NonNull [] intents) {
mInfo.mIntents = intents;
return this;
}
@@ -699,8 +678,7 @@
/**
* Sets an icon of a shortcut.
*/
- @NonNull
- public Builder setIcon(IconCompat icon) {
+ public @NonNull Builder setIcon(IconCompat icon) {
mInfo.mIcon = icon;
return this;
}
@@ -713,8 +691,7 @@
* {@link android.view.contentcapture.ContentCaptureContext}) so the device's intelligence
* services can correlate them.
*/
- @NonNull
- public Builder setLocusId(@Nullable final LocusIdCompat locusId) {
+ public @NonNull Builder setLocusId(final @Nullable LocusIdCompat locusId) {
mInfo.mLocusId = locusId;
return this;
}
@@ -729,8 +706,7 @@
* Additionally, the shortcut will be long-lived.
* @see #setLongLived(boolean)
*/
- @NonNull
- public Builder setIsConversation() {
+ public @NonNull Builder setIsConversation() {
mIsConversation = true;
return this;
}
@@ -742,8 +718,7 @@
* @see ShortcutInfo#getActivity()
* @see ShortcutInfo.Builder#setActivity(ComponentName)
*/
- @NonNull
- public Builder setActivity(@NonNull ComponentName activity) {
+ public @NonNull Builder setActivity(@NonNull ComponentName activity) {
mInfo.mActivity = activity;
return this;
}
@@ -760,8 +735,7 @@
*
* @see #setActivity(ComponentName)
*/
- @NonNull
- public Builder setAlwaysBadged() {
+ public @NonNull Builder setAlwaysBadged() {
mInfo.mIsAlwaysBadged = true;
return this;
}
@@ -774,16 +748,14 @@
*
* @see Person
*/
- @NonNull
- public Builder setPerson(@NonNull Person person) {
+ public @NonNull Builder setPerson(@NonNull Person person) {
return setPersons(new Person[]{person});
}
/**
* Sets multiple persons instead of a single person.
*/
- @NonNull
- public Builder setPersons(@NonNull Person[] persons) {
+ public @NonNull Builder setPersons(Person @NonNull [] persons) {
mInfo.mPersons = persons;
return this;
}
@@ -799,8 +771,7 @@
*
* @see ShortcutInfo#getCategories()
*/
- @NonNull
- public Builder setCategories(@NonNull Set<String> categories) {
+ public @NonNull Builder setCategories(@NonNull Set<String> categories) {
ArraySet<String> set = new ArraySet<>();
set.addAll(categories);
mInfo.mCategories = set;
@@ -811,8 +782,7 @@
* @deprecated Use {@ink #setLongLived(boolean)) instead.
*/
@Deprecated
- @NonNull
- public Builder setLongLived() {
+ public @NonNull Builder setLongLived() {
mInfo.mIsLongLived = true;
return this;
}
@@ -822,8 +792,7 @@
* (as a dynamic or pinned shortcut). If it is long lived, it can be cached by various
* system services even after it has been unpublished as a dynamic shortcut.
*/
- @NonNull
- public Builder setLongLived(boolean longLived) {
+ public @NonNull Builder setLongLived(boolean longLived) {
mInfo.mIsLongLived = longLived;
return this;
}
@@ -842,8 +811,7 @@
* actually sent to {@link ShortcutManager}. These shortcuts might still be made
* available to other surfaces via alternative means.
*/
- @NonNull
- public Builder setExcludedFromSurfaces(final int surfaces) {
+ public @NonNull Builder setExcludedFromSurfaces(final int surfaces) {
mInfo.mExcludedSurfaces = surfaces;
return this;
}
@@ -854,8 +822,7 @@
*
* @see ShortcutInfo#getRank() for details.
*/
- @NonNull
- public Builder setRank(int rank) {
+ public @NonNull Builder setRank(int rank) {
mInfo.mRank = rank;
return this;
}
@@ -868,8 +835,7 @@
*
* @see ShortcutInfo#getExtras
*/
- @NonNull
- public Builder setExtras(@NonNull PersistableBundle extras) {
+ public @NonNull Builder setExtras(@NonNull PersistableBundle extras) {
mInfo.mExtras = extras;
return this;
}
@@ -877,8 +843,7 @@
/**
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @NonNull
- public Builder setTransientExtras(@NonNull final Bundle transientExtras) {
+ public @NonNull Builder setTransientExtras(final @NonNull Bundle transientExtras) {
mInfo.mTransientExtras = Preconditions.checkNotNull(transientExtras);
return this;
}
@@ -894,8 +859,7 @@
* .START_EXERCISE.
*/
@SuppressLint("MissingGetterMatchingBuilder")
- @NonNull
- public Builder addCapabilityBinding(@NonNull String capability) {
+ public @NonNull Builder addCapabilityBinding(@NonNull String capability) {
if (mCapabilityBindings == null) {
mCapabilityBindings = new HashSet<>();
}
@@ -919,8 +883,7 @@
* the shortcut.
*/
@SuppressLint("MissingGetterMatchingBuilder")
- @NonNull
- public Builder addCapabilityBinding(@NonNull String capability,
+ public @NonNull Builder addCapabilityBinding(@NonNull String capability,
@NonNull String parameter, @NonNull List<String> parameterValues) {
addCapabilityBinding(capability);
@@ -942,8 +905,7 @@
* slice, instead of an intent.
*/
@SuppressLint("MissingGetterMatchingBuilder")
- @NonNull
- public Builder setSliceUri(@NonNull Uri sliceUri) {
+ public @NonNull Builder setSliceUri(@NonNull Uri sliceUri) {
mSliceUri = sliceUri;
return this;
}
@@ -951,8 +913,7 @@
/**
* Creates a {@link ShortcutInfoCompat} instance.
*/
- @NonNull
- public ShortcutInfoCompat build() {
+ public @NonNull ShortcutInfoCompat build() {
// Verify the arguments
if (TextUtils.isEmpty(mInfo.mLabel)) {
throw new IllegalArgumentException("Shortcut must have a non-empty label");
@@ -1007,7 +968,7 @@
@RequiresApi(33)
private static class Api33Impl {
- static void setExcludedFromSurfaces(@NonNull final ShortcutInfo.Builder builder,
+ static void setExcludedFromSurfaces(final ShortcutInfo.@NonNull Builder builder,
final int surfaces) {
builder.setExcludedFromSurfaces(surfaces);
}
diff --git a/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java b/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java
index 03e4741..c9c2a65 100644
--- a/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java
@@ -38,8 +38,6 @@
import android.util.DisplayMetrics;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
@@ -48,6 +46,9 @@
import androidx.core.graphics.drawable.IconCompat;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.InputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -186,8 +187,8 @@
* @see IntentSender
* @see android.app.PendingIntent#getIntentSender()
*/
- public static boolean requestPinShortcut(@NonNull final Context context,
- @NonNull ShortcutInfoCompat shortcut, @Nullable final IntentSender callback) {
+ public static boolean requestPinShortcut(final @NonNull Context context,
+ @NonNull ShortcutInfoCompat shortcut, final @Nullable IntentSender callback) {
if (Build.VERSION.SDK_INT <= 32
&& shortcut.isExcludedFromSurfaces(ShortcutInfoCompat.SURFACE_LAUNCHER)) {
// A shortcut that is not frequently used cannot be pinned to WorkSpace.
@@ -234,8 +235,7 @@
*
* @see Intent#ACTION_CREATE_SHORTCUT
*/
- @NonNull
- public static Intent createShortcutResultIntent(@NonNull Context context,
+ public static @NonNull Intent createShortcutResultIntent(@NonNull Context context,
@NonNull ShortcutInfoCompat shortcut) {
Intent result = null;
if (Build.VERSION.SDK_INT >= 26) {
@@ -276,8 +276,7 @@
*
* @throws IllegalStateException when the user is locked.
*/
- @NonNull
- public static List<ShortcutInfoCompat> getShortcuts(@NonNull final Context context,
+ public static @NonNull List<ShortcutInfoCompat> getShortcuts(final @NonNull Context context,
@ShortcutMatchFlags int matchFlags) {
if (Build.VERSION.SDK_INT >= 30) {
final List<ShortcutInfo> shortcuts =
@@ -367,7 +366,7 @@
*
* @throws IllegalStateException when the user is locked.
*/
- public static boolean isRateLimitingActive(@NonNull final Context context) {
+ public static boolean isRateLimitingActive(final @NonNull Context context) {
Preconditions.checkNotNull(context);
if (Build.VERSION.SDK_INT >= 25) {
return context.getSystemService(ShortcutManager.class).isRateLimitingActive();
@@ -387,7 +386,7 @@
* 1 + 2 * {@link android.graphics.drawable.AdaptiveIconDrawable#getExtraInsetFraction()} to
* the returned size.
*/
- public static int getIconMaxWidth(@NonNull final Context context) {
+ public static int getIconMaxWidth(final @NonNull Context context) {
Preconditions.checkNotNull(context);
if (Build.VERSION.SDK_INT >= 25) {
return context.getSystemService(ShortcutManager.class).getIconMaxWidth();
@@ -398,7 +397,7 @@
/**
* Return the max height for icons, in pixels.
*/
- public static int getIconMaxHeight(@NonNull final Context context) {
+ public static int getIconMaxHeight(final @NonNull Context context) {
Preconditions.checkNotNull(context);
if (Build.VERSION.SDK_INT >= 25) {
return context.getSystemService(ShortcutManager.class).getIconMaxHeight();
@@ -423,8 +422,8 @@
* <p>This method is not supported on devices running SDK < 25 since the platform class will
* not be available.
*/
- public static void reportShortcutUsed(@NonNull final Context context,
- @NonNull final String shortcutId) {
+ public static void reportShortcutUsed(final @NonNull Context context,
+ final @NonNull String shortcutId) {
Preconditions.checkNotNull(context);
Preconditions.checkNotNull(shortcutId);
if (Build.VERSION.SDK_INT >= 25) {
@@ -460,8 +459,8 @@
*
* @throws IllegalStateException when the user is locked.
*/
- public static boolean setDynamicShortcuts(@NonNull final Context context,
- @NonNull final List<ShortcutInfoCompat> shortcutInfoList) {
+ public static boolean setDynamicShortcuts(final @NonNull Context context,
+ final @NonNull List<ShortcutInfoCompat> shortcutInfoList) {
Preconditions.checkNotNull(context);
Preconditions.checkNotNull(shortcutInfoList);
final List<ShortcutInfoCompat> clone = removeShortcutsExcludedFromSurface(
@@ -492,8 +491,7 @@
* Re-publishing returned {@link ShortcutInfo}s via APIs such as
* {@link #addDynamicShortcuts(Context, List)} may cause loss of information such as icons.
*/
- @NonNull
- public static List<ShortcutInfoCompat> getDynamicShortcuts(@NonNull Context context) {
+ public static @NonNull List<ShortcutInfoCompat> getDynamicShortcuts(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= 25) {
List<ShortcutInfo> shortcuts = context.getSystemService(
ShortcutManager.class).getDynamicShortcuts();
@@ -552,8 +550,8 @@
}
@VisibleForTesting
- static boolean convertUriIconToBitmapIcon(@NonNull final Context context,
- @NonNull final ShortcutInfoCompat info) {
+ static boolean convertUriIconToBitmapIcon(final @NonNull Context context,
+ final @NonNull ShortcutInfoCompat info) {
if (info.mIcon == null) {
return false;
}
@@ -576,8 +574,8 @@
}
@VisibleForTesting
- static void convertUriIconsToBitmapIcons(@NonNull final Context context,
- @NonNull final List<ShortcutInfoCompat> shortcutInfoList) {
+ static void convertUriIconsToBitmapIcons(final @NonNull Context context,
+ final @NonNull List<ShortcutInfoCompat> shortcutInfoList) {
final List<ShortcutInfoCompat> shortcuts = new ArrayList<>(shortcutInfoList);
for (ShortcutInfoCompat info : shortcuts) {
if (!convertUriIconToBitmapIcon(context, info)) {
@@ -603,8 +601,8 @@
*
* @throws IllegalStateException when the user is locked.
*/
- public static void disableShortcuts(@NonNull final Context context,
- @NonNull final List<String> shortcutIds, @Nullable final CharSequence disabledMessage) {
+ public static void disableShortcuts(final @NonNull Context context,
+ final @NonNull List<String> shortcutIds, final @Nullable CharSequence disabledMessage) {
if (Build.VERSION.SDK_INT >= 25) {
context.getSystemService(ShortcutManager.class)
.disableShortcuts(shortcutIds, disabledMessage);
@@ -632,8 +630,8 @@
*
* @throws IllegalStateException when the user is locked.
*/
- public static void enableShortcuts(@NonNull final Context context,
- @NonNull final List<ShortcutInfoCompat> shortcutInfoList) {
+ public static void enableShortcuts(final @NonNull Context context,
+ final @NonNull List<ShortcutInfoCompat> shortcutInfoList) {
final List<ShortcutInfoCompat> clone = removeShortcutsExcludedFromSurface(
shortcutInfoList, ShortcutInfoCompat.SURFACE_LAUNCHER);
if (Build.VERSION.SDK_INT >= 25) {
@@ -698,8 +696,8 @@
*
* @throws IllegalStateException when the user is locked.
*/
- public static void removeLongLivedShortcuts(@NonNull final Context context,
- @NonNull final List<String> shortcutIds) {
+ public static void removeLongLivedShortcuts(final @NonNull Context context,
+ final @NonNull List<String> shortcutIds) {
if (Build.VERSION.SDK_INT < 30) {
removeDynamicShortcuts(context, shortcutIds);
return;
@@ -745,8 +743,8 @@
*
* @throws IllegalStateException when the user is locked.
*/
- public static boolean pushDynamicShortcut(@NonNull final Context context,
- @NonNull final ShortcutInfoCompat shortcut) {
+ public static boolean pushDynamicShortcut(final @NonNull Context context,
+ final @NonNull ShortcutInfoCompat shortcut) {
Preconditions.checkNotNull(context);
Preconditions.checkNotNull(shortcut);
@@ -800,7 +798,7 @@
}
private static String getShortcutInfoCompatWithLowestRank(
- @NonNull final List<ShortcutInfoCompat> shortcuts) {
+ final @NonNull List<ShortcutInfoCompat> shortcuts) {
int rank = -1;
String target = null;
for (ShortcutInfoCompat s : shortcuts) {
@@ -827,7 +825,7 @@
return sShortcutInfoChangeListeners;
}
- private static int getIconDimensionInternal(@NonNull final Context context,
+ private static int getIconDimensionInternal(final @NonNull Context context,
final boolean isHorizontal) {
final ActivityManager am = (ActivityManager)
context.getSystemService(Context.ACTIVITY_SERVICE);
@@ -905,9 +903,8 @@
return sShortcutInfoChangeListeners;
}
- @NonNull
- private static List<ShortcutInfoCompat> removeShortcutsExcludedFromSurface(
- @NonNull final List<ShortcutInfoCompat> shortcuts, final int surfaces) {
+ private static @NonNull List<ShortcutInfoCompat> removeShortcutsExcludedFromSurface(
+ final @NonNull List<ShortcutInfoCompat> shortcuts, final int surfaces) {
Objects.requireNonNull(shortcuts);
if (Build.VERSION.SDK_INT > 32) return shortcuts;
final List<ShortcutInfoCompat> clone = new ArrayList<>(shortcuts);
@@ -921,7 +918,7 @@
@RequiresApi(25)
private static class Api25Impl {
- static String getShortcutInfoWithLowestRank(@NonNull final List<ShortcutInfo> shortcuts) {
+ static String getShortcutInfoWithLowestRank(final @NonNull List<ShortcutInfo> shortcuts) {
int rank = -1;
String target = null;
for (ShortcutInfo s : shortcuts) {
diff --git a/core/core/src/main/java/androidx/core/content/pm/ShortcutXmlParser.java b/core/core/src/main/java/androidx/core/content/pm/ShortcutXmlParser.java
index 82eeb8f..4b3f45f 100644
--- a/core/core/src/main/java/androidx/core/content/pm/ShortcutXmlParser.java
+++ b/core/core/src/main/java/androidx/core/content/pm/ShortcutXmlParser.java
@@ -27,11 +27,11 @@
import android.os.Bundle;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
+import org.jspecify.annotations.NonNull;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -62,8 +62,7 @@
* Returns a singleton instance of list of ids of static shortcuts parsed from shortcuts.xml
*/
@WorkerThread
- @NonNull
- public static List<String> getShortcutIds(@NonNull final Context context) {
+ public static @NonNull List<String> getShortcutIds(final @NonNull Context context) {
if (sShortcutIds == null) {
synchronized (GET_INSTANCE_LOCK) {
if (sShortcutIds == null) {
@@ -84,9 +83,8 @@
* Calling package is determined by {@link Context#getPackageName}
* Returns a set of string which contains the ids of static shortcuts.
*/
- @NonNull
@SuppressWarnings("deprecation")
- private static Set<String> parseShortcutIds(@NonNull final Context context) {
+ private static @NonNull Set<String> parseShortcutIds(final @NonNull Context context) {
final Set<String> result = new HashSet<>();
final Intent mainIntent = new Intent(Intent.ACTION_MAIN);
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
@@ -115,8 +113,8 @@
return result;
}
- @NonNull
- private static XmlResourceParser getXmlResourceParser(Context context, ActivityInfo info) {
+ private static @NonNull XmlResourceParser getXmlResourceParser(Context context,
+ ActivityInfo info) {
final XmlResourceParser parser = info.loadXmlMetaData(context.getPackageManager(),
META_DATA_APP_SHORTCUTS);
if (parser == null) {
@@ -131,8 +129,7 @@
* Parses the shortcut ids from given XmlPullParser.
*/
@VisibleForTesting
- @NonNull
- public static List<String> parseShortcutIds(@NonNull final XmlPullParser parser)
+ public static @NonNull List<String> parseShortcutIds(final @NonNull XmlPullParser parser)
throws IOException, XmlPullParserException {
final List<String> result = new ArrayList<>(1);
diff --git a/core/core/src/main/java/androidx/core/content/res/CamColor.java b/core/core/src/main/java/androidx/core/content/res/CamColor.java
index 9cd05c0..24a52a3 100644
--- a/core/core/src/main/java/androidx/core/content/res/CamColor.java
+++ b/core/core/src/main/java/androidx/core/content/res/CamColor.java
@@ -18,12 +18,13 @@
import androidx.annotation.ColorInt;
import androidx.annotation.FloatRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.Size;
import androidx.core.graphics.ColorUtils;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* A color appearance model, based on CAM16, extended to use L* as the lightness dimension, and
* coupled to a gamut mapping algorithm. Creates a color system, enables a digital design system.
@@ -158,8 +159,7 @@
*
* The alpha component is ignored, CamColor only represents opaque colors.
*/
- @NonNull
- static CamColor fromColor(@ColorInt int color) {
+ static @NonNull CamColor fromColor(@ColorInt int color) {
float[] outCamColor = new float[7];
float[] outM3HCT = new float[3];
fromColorInViewingConditions(color, ViewingConditions.DEFAULT, outCamColor, outM3HCT);
@@ -187,7 +187,7 @@
* Chroma, Tone).
*/
public static void getM3HCTfromColor(@ColorInt int color,
- @NonNull @Size(3) float[] outM3HCT) {
+ @Size(3) float @NonNull [] outM3HCT) {
fromColorInViewingConditions(color, ViewingConditions.DEFAULT, null, outM3HCT);
outM3HCT[2] = CamUtils.lStarFromInt(color);
}
@@ -197,8 +197,8 @@
* ViewingConditions in which the color was viewed. Prefer Cam.fromColor.
*/
static void fromColorInViewingConditions(@ColorInt int color,
- @NonNull ViewingConditions viewingConditions, @Nullable @Size(7) float[] outCamColor,
- @NonNull @Size(3) float[] outM3HCT) {
+ @NonNull ViewingConditions viewingConditions, @Size(7) float @Nullable [] outCamColor,
+ @Size(3) float @NonNull [] outM3HCT) {
// Transform ARGB int to XYZ, reusing outM3HCT array to avoid a new allocation.
CamUtils.xyzFromInt(color, outM3HCT);
float[] xyz = outM3HCT;
@@ -291,8 +291,7 @@
* Create a CAM from lightness, chroma, and hue coordinates. It is assumed those coordinates
* were measured in the default ViewingConditions.
*/
- @NonNull
- private static CamColor fromJch(@FloatRange(from = 0.0, to = 100.0) float j,
+ private static @NonNull CamColor fromJch(@FloatRange(from = 0.0, to = 100.0) float j,
@FloatRange(from = 0.0, to = Double.POSITIVE_INFINITY, toInclusive = false) float c,
@FloatRange(from = 0.0, to = 360.0) float h) {
return fromJchInFrame(j, c, h, ViewingConditions.DEFAULT);
@@ -302,8 +301,7 @@
* Create a CAM from lightness, chroma, and hue coordinates, and also specify the
* ViewingConditions where the color was seen.
*/
- @NonNull
- private static CamColor fromJchInFrame(@FloatRange(from = 0.0, to = 100.0) float j,
+ private static @NonNull CamColor fromJchInFrame(@FloatRange(from = 0.0, to = 100.0) float j,
@FloatRange(from = 0.0, to = Double.POSITIVE_INFINITY, toInclusive = false) float c,
@FloatRange(from = 0.0, to = 360.0) float h, ViewingConditions viewingConditions) {
float q =
@@ -520,8 +518,7 @@
// color space.
//
// Returns null if no J could be found that generated a color with L* `lstar`.
- @Nullable
- private static CamColor findCamByJ(@FloatRange(from = 0.0, to = 360.0) float hue,
+ private static @Nullable CamColor findCamByJ(@FloatRange(from = 0.0, to = 360.0) float hue,
@FloatRange(from = 0.0, to = Double.POSITIVE_INFINITY, toInclusive = false)
float chroma,
@FloatRange(from = 0.0, to = 100.0) float lstar) {
diff --git a/core/core/src/main/java/androidx/core/content/res/CamUtils.java b/core/core/src/main/java/androidx/core/content/res/CamUtils.java
index 55435fd..5a0e459 100644
--- a/core/core/src/main/java/androidx/core/content/res/CamUtils.java
+++ b/core/core/src/main/java/androidx/core/content/res/CamUtils.java
@@ -18,9 +18,10 @@
import android.graphics.Color;
-import androidx.annotation.NonNull;
import androidx.core.graphics.ColorUtils;
+import org.jspecify.annotations.NonNull;
+
/**
* Collection of methods for transforming between color spaces.
*
@@ -132,7 +133,7 @@
return y;
}
- static void xyzFromInt(int argb, @NonNull float[] outXYZ) {
+ static void xyzFromInt(int argb, float @NonNull [] outXYZ) {
final float r = linearized(Color.red(argb));
final float g = linearized(Color.green(argb));
final float b = linearized(Color.blue(argb));
diff --git a/core/core/src/main/java/androidx/core/content/res/ColorStateListInflaterCompat.java b/core/core/src/main/java/androidx/core/content/res/ColorStateListInflaterCompat.java
index 6ab7be4..c4a87cc 100644
--- a/core/core/src/main/java/androidx/core/content/res/ColorStateListInflaterCompat.java
+++ b/core/core/src/main/java/androidx/core/content/res/ColorStateListInflaterCompat.java
@@ -32,13 +32,13 @@
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.FloatRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.XmlRes;
import androidx.core.R;
import androidx.core.math.MathUtils;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -63,9 +63,8 @@
* @param theme Optional theme to apply to the color, may be {@code null}.
* @return A new color state list.
*/
- @Nullable
- public static ColorStateList inflate(@NonNull Resources resources, @XmlRes int resId,
- @Nullable Resources.Theme theme) {
+ public static @Nullable ColorStateList inflate(@NonNull Resources resources, @XmlRes int resId,
+ Resources.@Nullable Theme theme) {
try {
XmlPullParser parser = resources.getXml(resId);
return createFromXml(resources, parser, theme);
@@ -85,9 +84,9 @@
* {@code null}.
* @return A new color state list.
*/
- @NonNull
- public static ColorStateList createFromXml(@NonNull Resources r, @NonNull XmlPullParser parser,
- @Nullable Resources.Theme theme) throws XmlPullParserException, IOException {
+ public static @NonNull ColorStateList createFromXml(@NonNull Resources r,
+ @NonNull XmlPullParser parser, Resources.@Nullable Theme theme)
+ throws XmlPullParserException, IOException {
final AttributeSet attrs = Xml.asAttributeSet(parser);
int type;
@@ -110,10 +109,9 @@
* @return A new color state list for the current tag.
* @throws XmlPullParserException if the current tag is not <selector>
*/
- @NonNull
- public static ColorStateList createFromXmlInner(@NonNull Resources r,
+ public static @NonNull ColorStateList createFromXmlInner(@NonNull Resources r,
@NonNull XmlPullParser parser, @NonNull AttributeSet attrs,
- @Nullable Resources.Theme theme)
+ Resources.@Nullable Theme theme)
throws XmlPullParserException, IOException {
final String name = parser.getName();
if (!name.equals("selector")) {
@@ -128,7 +126,7 @@
* Fill in this object based on the contents of an XML "selector" element.
*/
private static ColorStateList inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
- @NonNull AttributeSet attrs, @Nullable Resources.Theme theme)
+ @NonNull AttributeSet attrs, Resources.@Nullable Theme theme)
throws XmlPullParserException, IOException {
final int innerDepth = parser.getDepth() + 1;
int depth;
@@ -218,8 +216,7 @@
&& value.type <= TypedValue.TYPE_LAST_COLOR_INT;
}
- @NonNull
- private static TypedValue getTypedValue() {
+ private static @NonNull TypedValue getTypedValue() {
TypedValue tv = sTempTypedValue.get();
if (tv == null) {
tv = new TypedValue();
diff --git a/core/core/src/main/java/androidx/core/content/res/ComplexColorCompat.java b/core/core/src/main/java/androidx/core/content/res/ComplexColorCompat.java
index ed22996..2e480fb 100644
--- a/core/core/src/main/java/androidx/core/content/res/ComplexColorCompat.java
+++ b/core/core/src/main/java/androidx/core/content/res/ComplexColorCompat.java
@@ -30,10 +30,10 @@
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -75,8 +75,7 @@
return new ComplexColorCompat(null, null, color);
}
- @Nullable
- public Shader getShader() {
+ public @Nullable Shader getShader() {
return mShader;
}
@@ -132,9 +131,8 @@
* @param theme Optional theme to apply to the color, may be {@code null}.
* @return A new color.
*/
- @Nullable
- public static ComplexColorCompat inflate(@NonNull Resources resources, @ColorRes int resId,
- @Nullable Resources.Theme theme) {
+ public static @Nullable ComplexColorCompat inflate(@NonNull Resources resources,
+ @ColorRes int resId, Resources.@Nullable Theme theme) {
try {
return createFromXml(resources, resId, theme);
} catch (Exception e) {
@@ -143,9 +141,8 @@
return null;
}
- @NonNull
- private static ComplexColorCompat createFromXml(@NonNull Resources resources,
- @ColorRes int resId, @Nullable Resources.Theme theme)
+ private static @NonNull ComplexColorCompat createFromXml(@NonNull Resources resources,
+ @ColorRes int resId, Resources.@Nullable Theme theme)
throws IOException, XmlPullParserException {
@SuppressLint("ResourceType")
XmlPullParser parser = resources.getXml(resId);
diff --git a/core/core/src/main/java/androidx/core/content/res/ConfigurationHelper.java b/core/core/src/main/java/androidx/core/content/res/ConfigurationHelper.java
index dcfb64c..becebe4 100644
--- a/core/core/src/main/java/androidx/core/content/res/ConfigurationHelper.java
+++ b/core/core/src/main/java/androidx/core/content/res/ConfigurationHelper.java
@@ -19,7 +19,7 @@
import android.content.res.Configuration;
import android.content.res.Resources;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Helper class which allows access to properties of {@link Configuration} in
diff --git a/core/core/src/main/java/androidx/core/content/res/FontResourcesParserCompat.java b/core/core/src/main/java/androidx/core/content/res/FontResourcesParserCompat.java
index 55b813c..9582516 100644
--- a/core/core/src/main/java/androidx/core/content/res/FontResourcesParserCompat.java
+++ b/core/core/src/main/java/androidx/core/content/res/FontResourcesParserCompat.java
@@ -29,13 +29,13 @@
import androidx.annotation.ArrayRes;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.R;
import androidx.core.provider.FontRequest;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -101,8 +101,7 @@
return mRequest;
}
- @Nullable
- public FontRequest getFallbackRequest() {
+ public @Nullable FontRequest getFallbackRequest() {
return mFallbackRequest;
}
@@ -170,13 +169,13 @@
* A class that represents a file based font-family element in an xml font file.
*/
public static final class FontFamilyFilesResourceEntry implements FamilyResourceEntry {
- private final @NonNull FontFileResourceEntry[] mEntries;
+ private final FontFileResourceEntry @NonNull [] mEntries;
- public FontFamilyFilesResourceEntry(@NonNull FontFileResourceEntry[] entries) {
+ public FontFamilyFilesResourceEntry(FontFileResourceEntry @NonNull [] entries) {
mEntries = entries;
}
- public @NonNull FontFileResourceEntry[] getEntries() {
+ public FontFileResourceEntry @NonNull [] getEntries() {
return mEntries;
}
}
@@ -281,9 +280,8 @@
*
* Provider cert entry must be cert string array or array of cert string array.
*/
- @NonNull
@SuppressWarnings("MixedMutabilityReturnType")
- public static List<List<byte[]>> readCerts(@NonNull Resources resources,
+ public static @NonNull List<List<byte[]>> readCerts(@NonNull Resources resources,
@ArrayRes int certsId) {
if (certsId == 0) {
return Collections.emptyList();
diff --git a/core/core/src/main/java/androidx/core/content/res/GradientColorInflaterCompat.java b/core/core/src/main/java/androidx/core/content/res/GradientColorInflaterCompat.java
index 37fcafd..773cf40 100644
--- a/core/core/src/main/java/androidx/core/content/res/GradientColorInflaterCompat.java
+++ b/core/core/src/main/java/androidx/core/content/res/GradientColorInflaterCompat.java
@@ -34,11 +34,11 @@
import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.R;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -66,7 +66,7 @@
}
static Shader createFromXml(@NonNull Resources resources, @NonNull XmlPullParser parser,
- @Nullable Resources.Theme theme) throws XmlPullParserException, IOException {
+ Resources.@Nullable Theme theme) throws XmlPullParserException, IOException {
final AttributeSet attrs = Xml.asAttributeSet(parser);
int type;
@@ -84,7 +84,7 @@
static Shader createFromXmlInner(@NonNull Resources resources,
@NonNull XmlPullParser parser, @NonNull AttributeSet attrs,
- @Nullable Resources.Theme theme)
+ Resources.@Nullable Theme theme)
throws IOException, XmlPullParserException {
final String name = parser.getName();
if (!name.equals("gradient")) {
@@ -144,7 +144,7 @@
private static ColorStops inflateChildElements(@NonNull Resources resources,
@NonNull XmlPullParser parser, @NonNull AttributeSet attrs,
- @Nullable Resources.Theme theme)
+ Resources.@Nullable Theme theme)
throws XmlPullParserException, IOException {
final int innerDepth = parser.getDepth() + 1;
int type;
diff --git a/core/core/src/main/java/androidx/core/content/res/ResourcesCompat.java b/core/core/src/main/java/androidx/core/content/res/ResourcesCompat.java
index 9ba0cc71..29f37eb 100644
--- a/core/core/src/main/java/androidx/core/content/res/ResourcesCompat.java
+++ b/core/core/src/main/java/androidx/core/content/res/ResourcesCompat.java
@@ -45,8 +45,6 @@
import androidx.annotation.DrawableRes;
import androidx.annotation.FontRes;
import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.content.res.FontResourcesParserCompat.FamilyResourceEntry;
@@ -56,6 +54,8 @@
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -129,9 +129,8 @@
* @throws NotFoundException Throws NotFoundException if the given ID does
* not exist.
*/
- @Nullable
@SuppressWarnings("deprecation")
- public static Drawable getDrawable(@NonNull Resources res, @DrawableRes int id,
+ public static @Nullable Drawable getDrawable(@NonNull Resources res, @DrawableRes int id,
@Nullable Theme theme) throws NotFoundException {
if (SDK_INT >= 21) {
return Api21Impl.getDrawable(res, id, theme);
@@ -163,10 +162,9 @@
* @throws NotFoundException Throws NotFoundException if the given ID does
* not exist.
*/
- @Nullable
@SuppressWarnings("deprecation")
- public static Drawable getDrawableForDensity(@NonNull Resources res, @DrawableRes int id,
- int density, @Nullable Theme theme) throws NotFoundException {
+ public static @Nullable Drawable getDrawableForDensity(@NonNull Resources res,
+ @DrawableRes int id, int density, @Nullable Theme theme) throws NotFoundException {
if (SDK_INT >= 21) {
return Api21Impl.getDrawableForDensity(res, id, density, theme);
} else {
@@ -220,10 +218,9 @@
* @throws NotFoundException Throws NotFoundException if the given ID does
* not exist.
*/
- @Nullable
@SuppressWarnings("deprecation")
- public static ColorStateList getColorStateList(@NonNull Resources res, @ColorRes int id,
- @Nullable Theme theme) throws NotFoundException {
+ public static @Nullable ColorStateList getColorStateList(@NonNull Resources res,
+ @ColorRes int id, @Nullable Theme theme) throws NotFoundException {
// We explicitly do not attempt to use the platform Resources impl on S+
// in case the CSL is using only app:lStar
@@ -251,8 +248,7 @@
/**
* Inflates a {@link ColorStateList} from resources, honouring theme attributes.
*/
- @Nullable
- private static ColorStateList inflateColorStateList(Resources resources, int resId,
+ private static @Nullable ColorStateList inflateColorStateList(Resources resources, int resId,
@Nullable Theme theme) {
if (isColorInt(resources, resId)) {
// The resource is a color int, we can't handle it so return null
@@ -267,9 +263,8 @@
return null;
}
- @Nullable
- private static ColorStateList getCachedColorStateList(@NonNull ColorStateListCacheKey key,
- @ColorRes int resId) {
+ private static @Nullable ColorStateList getCachedColorStateList(
+ @NonNull ColorStateListCacheKey key, @ColorRes int resId) {
synchronized (sColorStateCacheLock) {
final SparseArray<ColorStateListCacheEntry> entries = sColorStateCaches.get(key);
if (entries != null && entries.size() > 0) {
@@ -312,8 +307,7 @@
&& value.type <= TypedValue.TYPE_LAST_COLOR_INT;
}
- @NonNull
- private static TypedValue getTypedValue() {
+ private static @NonNull TypedValue getTypedValue() {
TypedValue tv = sTempTypedValue.get();
if (tv == null) {
tv = new TypedValue();
@@ -404,8 +398,7 @@
* @throws NotFoundException Throws NotFoundException if the given ID does not exist.
* @see #getFont(Context, int, FontCallback, Handler)
*/
- @Nullable
- public static Typeface getFont(@NonNull Context context, @FontRes int id)
+ public static @Nullable Typeface getFont(@NonNull Context context, @FontRes int id)
throws NotFoundException {
if (context.isRestricted()) {
return null;
@@ -432,8 +425,7 @@
* @throws NotFoundException Throws NotFoundException if the given ID does not exist.
* @see #getFont(Context, int, FontCallback, Handler)
*/
- @Nullable
- public static Typeface getCachedFont(@NonNull Context context, @FontRes int id)
+ public static @Nullable Typeface getCachedFont(@NonNull Context context, @FontRes int id)
throws NotFoundException {
if (context.isRestricted()) {
return null;
@@ -491,8 +483,7 @@
}
@RestrictTo(LIBRARY)
- @NonNull
- public static Handler getHandler(@Nullable Handler handler) {
+ public static @NonNull Handler getHandler(@Nullable Handler handler) {
return handler == null ? new Handler(Looper.getMainLooper()) : handler;
}
}
@@ -533,9 +524,8 @@
* Used by TintTypedArray.
*
*/
- @Nullable
@RestrictTo(LIBRARY_GROUP_PREFIX)
- public static Typeface getFont(@NonNull Context context, @FontRes int id,
+ public static @Nullable Typeface getFont(@NonNull Context context, @FontRes int id,
@NonNull TypedValue value, int style, @Nullable FontCallback fontCallback)
throws NotFoundException {
if (context.isRestricted()) {
@@ -678,8 +668,7 @@
// This class is not instantiable.
}
- @NonNull
- static ColorStateList getColorStateList(@NonNull Resources res, @ColorRes int id,
+ static @NonNull ColorStateList getColorStateList(@NonNull Resources res, @ColorRes int id,
@Nullable Theme theme) {
return res.getColorStateList(id, theme);
}
diff --git a/core/core/src/main/java/androidx/core/content/res/TypedArrayUtils.java b/core/core/src/main/java/androidx/core/content/res/TypedArrayUtils.java
index 4ce9005..aed89e1 100644
--- a/core/core/src/main/java/androidx/core/content/res/TypedArrayUtils.java
+++ b/core/core/src/main/java/androidx/core/content/res/TypedArrayUtils.java
@@ -27,11 +27,11 @@
import androidx.annotation.AnyRes;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.StyleableRes;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.xmlpull.v1.XmlPullParser;
/**
@@ -134,7 +134,7 @@
* or containing {@code defaultValue} if it does not exist.
*/
public static ComplexColorCompat getNamedComplexColor(@NonNull TypedArray a,
- @NonNull XmlPullParser parser, @Nullable Resources.Theme theme,
+ @NonNull XmlPullParser parser, Resources.@Nullable Theme theme,
@NonNull String attrName, @StyleableRes int resId, @ColorInt int defaultValue) {
if (hasAttribute(parser, attrName)) {
// first check if is a simple color
@@ -160,9 +160,8 @@
* @return a color state list object form the {@link TypedArray} with the specified
* {@code resId}, or null if it does not exist.
*/
- @Nullable
- public static ColorStateList getNamedColorStateList(@NonNull TypedArray a,
- @NonNull XmlPullParser parser, @Nullable Resources.Theme theme,
+ public static @Nullable ColorStateList getNamedColorStateList(@NonNull TypedArray a,
+ @NonNull XmlPullParser parser, Resources.@Nullable Theme theme,
@NonNull String attrName, @StyleableRes int resId) {
if (hasAttribute(parser, attrName)) {
final TypedValue value = new TypedValue();
@@ -181,8 +180,8 @@
return null;
}
- @NonNull
- private static ColorStateList getNamedColorStateListFromInt(@NonNull TypedValue value) {
+ private static @NonNull ColorStateList getNamedColorStateListFromInt(
+ @NonNull TypedValue value) {
// This is copied from ResourcesImpl#getNamedColorStateListFromInt in the platform, but the
// ComplexColor caching mechanism has been removed. The practical implication of this is
// minimal, since platform caching is only used by Zygote-preloaded resources.
@@ -214,9 +213,8 @@
* @return a string value in the {@link TypedArray} with the specified {@code resId}, or
* null if it does not exist.
*/
- @Nullable
- public static String getNamedString(@NonNull TypedArray a, @NonNull XmlPullParser parser,
- @NonNull String attrName, @StyleableRes int resId) {
+ public static @Nullable String getNamedString(@NonNull TypedArray a,
+ @NonNull XmlPullParser parser, @NonNull String attrName, @StyleableRes int resId) {
final boolean hasAttr = hasAttribute(parser, attrName);
if (!hasAttr) {
return null;
@@ -230,9 +228,8 @@
* and return a temporary object holding its data. This object is only
* valid until the next call on to {@link TypedArray}.
*/
- @Nullable
- public static TypedValue peekNamedValue(@NonNull TypedArray a, @NonNull XmlPullParser parser,
- @NonNull String attrName, int resId) {
+ public static @Nullable TypedValue peekNamedValue(@NonNull TypedArray a,
+ @NonNull XmlPullParser parser, @NonNull String attrName, int resId) {
final boolean hasAttr = hasAttribute(parser, attrName);
if (!hasAttr) {
return null;
@@ -245,9 +242,8 @@
* Obtains styled attributes from the theme, if available, or unstyled
* resources if the theme is null.
*/
- @NonNull
- public static TypedArray obtainAttributes(@NonNull Resources res,
- @Nullable Resources.Theme theme, @NonNull AttributeSet set, @NonNull int[] attrs) {
+ public static @NonNull TypedArray obtainAttributes(@NonNull Resources res,
+ Resources.@Nullable Theme theme, @NonNull AttributeSet set, int @NonNull [] attrs) {
if (theme == null) {
return res.obtainAttributes(set, attrs);
}
@@ -268,8 +264,7 @@
* @return a drawable value of {@code index}. If it does not exist, a drawable value of
* {@code fallbackIndex}. If it still does not exist, {@code null}.
*/
- @Nullable
- public static Drawable getDrawable(@NonNull TypedArray a, @StyleableRes int index,
+ public static @Nullable Drawable getDrawable(@NonNull TypedArray a, @StyleableRes int index,
@StyleableRes int fallbackIndex) {
Drawable val = a.getDrawable(index);
if (val == null) {
@@ -303,8 +298,7 @@
* @return a string value of {@code index}. If it does not exist, a string value of
* {@code fallbackIndex}. If it still does not exist, {@code null}.
*/
- @Nullable
- public static String getString(@NonNull TypedArray a, @StyleableRes int index,
+ public static @Nullable String getString(@NonNull TypedArray a, @StyleableRes int index,
@StyleableRes int fallbackIndex) {
String val = a.getString(index);
if (val == null) {
@@ -319,8 +313,7 @@
* @return a text value of {@code index}. If it does not exist, a text value of
* {@code fallbackIndex}. If it still does not exist, {@code null}.
*/
- @Nullable
- public static CharSequence getText(@NonNull TypedArray a, @StyleableRes int index,
+ public static @Nullable CharSequence getText(@NonNull TypedArray a, @StyleableRes int index,
@StyleableRes int fallbackIndex) {
CharSequence val = a.getText(index);
if (val == null) {
@@ -335,9 +328,8 @@
* @return a string array value of {@code index}. If it does not exist, a string array value
* of {@code fallbackIndex}. If it still does not exist, {@code null}.
*/
- @Nullable
- public static CharSequence[] getTextArray(@NonNull TypedArray a, @StyleableRes int index,
- @StyleableRes int fallbackIndex) {
+ public static CharSequence @Nullable [] getTextArray(@NonNull TypedArray a,
+ @StyleableRes int index, @StyleableRes int fallbackIndex) {
CharSequence[] val = a.getTextArray(index);
if (val == null) {
val = a.getTextArray(fallbackIndex);
diff --git a/core/core/src/main/java/androidx/core/content/res/ViewingConditions.java b/core/core/src/main/java/androidx/core/content/res/ViewingConditions.java
index 1768c07..7b2c4ae 100644
--- a/core/core/src/main/java/androidx/core/content/res/ViewingConditions.java
+++ b/core/core/src/main/java/androidx/core/content/res/ViewingConditions.java
@@ -16,7 +16,7 @@
package androidx.core.content.res;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Viewing conditions define parameters of where a color was seen. Used, along with a color, to
@@ -91,8 +91,7 @@
return mNc;
}
- @NonNull
- float[] getRgbD() {
+ float @NonNull [] getRgbD() {
return mRgbD;
}
@@ -123,8 +122,7 @@
}
/** Create a custom camFrame. */
- @NonNull
- static ViewingConditions make(@NonNull float[] whitepoint, float adaptingLuminance,
+ static @NonNull ViewingConditions make(float @NonNull [] whitepoint, float adaptingLuminance,
float backgroundLstar, float surround, boolean discountingIlluminant) {
// Transform white point XYZ to 'cone'/'rgb' responses
float[][] matrix = CamUtils.XYZ_TO_CAM16RGB;
diff --git a/core/core/src/main/java/androidx/core/database/CursorWindowCompat.java b/core/core/src/main/java/androidx/core/database/CursorWindowCompat.java
index 04202eb..433aca7 100644
--- a/core/core/src/main/java/androidx/core/database/CursorWindowCompat.java
+++ b/core/core/src/main/java/androidx/core/database/CursorWindowCompat.java
@@ -19,10 +19,11 @@
import android.database.CursorWindow;
import android.os.Build;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Helper for accessing features in {@link CursorWindow}
*/
@@ -38,8 +39,7 @@
* Prior to Android P, this method will return a CursorWindow of size defined by the platform.
*/
@SuppressWarnings("deprecation")
- @NonNull
- public static CursorWindow create(@Nullable String name, long windowSizeBytes) {
+ public static @NonNull CursorWindow create(@Nullable String name, long windowSizeBytes) {
if (Build.VERSION.SDK_INT >= 28) {
return Api28Impl.createCursorWindow(name, windowSizeBytes);
} else {
diff --git a/core/core/src/main/java/androidx/core/database/sqlite/SQLiteCursorCompat.java b/core/core/src/main/java/androidx/core/database/sqlite/SQLiteCursorCompat.java
index 00746f4..95466bb 100644
--- a/core/core/src/main/java/androidx/core/database/sqlite/SQLiteCursorCompat.java
+++ b/core/core/src/main/java/androidx/core/database/sqlite/SQLiteCursorCompat.java
@@ -20,9 +20,10 @@
import android.database.sqlite.SQLiteCursor;
import android.os.Build;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
/**
* Helper for accessing features in {@link AbstractWindowedCursor}
*/
diff --git a/core/core/src/main/java/androidx/core/graphics/BitmapCompat.java b/core/core/src/main/java/androidx/core/graphics/BitmapCompat.java
index ea4f04a..432e006 100644
--- a/core/core/src/main/java/androidx/core/graphics/BitmapCompat.java
+++ b/core/core/src/main/java/androidx/core/graphics/BitmapCompat.java
@@ -26,11 +26,12 @@
import android.hardware.HardwareBuffer;
import android.os.Build;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Helper for accessing features in {@link Bitmap}.
*/
@@ -128,8 +129,7 @@
* level 26 and earlier, this parameter has no effect).
* @return A new bitmap in the requested size.
*/
- public static @NonNull
- Bitmap createScaledBitmap(@NonNull Bitmap srcBm, int dstW,
+ public static @NonNull Bitmap createScaledBitmap(@NonNull Bitmap srcBm, int dstW,
int dstH, @Nullable Rect srcRect, boolean scaleInLinearSpace) {
if (dstW <= 0 || dstH <= 0) {
throw new IllegalArgumentException("dstW and dstH must be > 0!");
diff --git a/core/core/src/main/java/androidx/core/graphics/BlendModeColorFilterCompat.java b/core/core/src/main/java/androidx/core/graphics/BlendModeColorFilterCompat.java
index f12da61..0eaf8e9 100644
--- a/core/core/src/main/java/androidx/core/graphics/BlendModeColorFilterCompat.java
+++ b/core/core/src/main/java/androidx/core/graphics/BlendModeColorFilterCompat.java
@@ -23,10 +23,11 @@
import android.graphics.PorterDuffColorFilter;
import android.os.Build;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Helper for accessing ColorFilter APIs on various API levels of the platform
*/
diff --git a/core/core/src/main/java/androidx/core/graphics/BlendModeUtils.java b/core/core/src/main/java/androidx/core/graphics/BlendModeUtils.java
index f610f9a..5d54947 100644
--- a/core/core/src/main/java/androidx/core/graphics/BlendModeUtils.java
+++ b/core/core/src/main/java/androidx/core/graphics/BlendModeUtils.java
@@ -19,10 +19,11 @@
import android.graphics.BlendMode;
import android.graphics.PorterDuff;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Utility class used to map BlendModeCompat parameters to the corresponding
* PorterDuff mode or BlendMode depending on the API level of the platform
@@ -38,8 +39,8 @@
// This class is not instantiable.
}
- @Nullable
- static Object obtainBlendModeFromCompat(@NonNull BlendModeCompat blendModeCompat) {
+ static @Nullable Object obtainBlendModeFromCompat(
+ @NonNull BlendModeCompat blendModeCompat) {
switch (blendModeCompat) {
case CLEAR:
return BlendMode.CLEAR;
@@ -105,7 +106,7 @@
}
}
- static @Nullable PorterDuff.Mode obtainPorterDuffFromCompat(
+ static PorterDuff.@Nullable Mode obtainPorterDuffFromCompat(
@Nullable BlendModeCompat blendModeCompat) {
if (blendModeCompat == null) {
return null;
diff --git a/core/core/src/main/java/androidx/core/graphics/ColorUtils.java b/core/core/src/main/java/androidx/core/graphics/ColorUtils.java
index a7b93c7..a04bdef 100644
--- a/core/core/src/main/java/androidx/core/graphics/ColorUtils.java
+++ b/core/core/src/main/java/androidx/core/graphics/ColorUtils.java
@@ -22,12 +22,13 @@
import androidx.annotation.ColorInt;
import androidx.annotation.FloatRange;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.Size;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.res.CamColor;
+import org.jspecify.annotations.NonNull;
+
import java.util.Objects;
/**
@@ -89,8 +90,8 @@
* {@linkplain android.graphics.Color#getModel models} of the colors do not match
*/
@RequiresApi(26)
- @NonNull
- public static Color compositeColors(@NonNull Color foreground, @NonNull Color background) {
+ public static @NonNull Color compositeColors(@NonNull Color foreground,
+ @NonNull Color background) {
return Api26Impl.compositeColors(foreground, background);
}
@@ -250,7 +251,7 @@
*/
public static void RGBToHSL(@IntRange(from = 0x0, to = 0xFF) int r,
@IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
- @NonNull float[] outHsl) {
+ float @NonNull [] outHsl) {
final float rf = r / 255f;
final float gf = g / 255f;
final float bf = b / 255f;
@@ -298,7 +299,7 @@
* @param color the ARGB color to convert. The alpha component is ignored
* @param outHsl 3-element array which holds the resulting HSL components
*/
- public static void colorToHSL(@ColorInt int color, @NonNull float[] outHsl) {
+ public static void colorToHSL(@ColorInt int color, float @NonNull [] outHsl) {
RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), outHsl);
}
@@ -315,7 +316,7 @@
* @return the resulting RGB color
*/
@ColorInt
- public static int HSLToColor(@NonNull float[] hsl) {
+ public static int HSLToColor(float @NonNull [] hsl) {
final float h = hsl[0];
final float s = hsl[1];
final float l = hsl[2];
@@ -387,7 +388,7 @@
* @param color the ARGB color to convert. The alpha component is ignored
* @param outLab 3-element array which holds the resulting LAB components
*/
- public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) {
+ public static void colorToLAB(@ColorInt int color, double @NonNull [] outLab) {
RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab);
}
@@ -407,7 +408,7 @@
*/
public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r,
@IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
- @NonNull double[] outLab) {
+ double @NonNull [] outLab) {
// First we convert RGB to XYZ
RGBToXYZ(r, g, b, outLab);
// outLab now contains XYZ
@@ -430,7 +431,7 @@
* @param color the ARGB color to convert. The alpha component is ignored
* @param outXyz 3-element array which holds the resulting LAB components
*/
- public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) {
+ public static void colorToXYZ(@ColorInt int color, double @NonNull [] outXyz) {
RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz);
}
@@ -453,7 +454,7 @@
*/
public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r,
@IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
- @NonNull double[] outXyz) {
+ double @NonNull [] outXyz) {
if (outXyz.length != 3) {
throw new IllegalArgumentException("outXyz must have a length of 3.");
}
@@ -490,7 +491,7 @@
public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z,
- @NonNull double[] outLab) {
+ double @NonNull [] outLab) {
if (outLab.length != 3) {
throw new IllegalArgumentException("outLab must have a length of 3.");
}
@@ -522,7 +523,7 @@
public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l,
@FloatRange(from = -128, to = 127) final double a,
@FloatRange(from = -128, to = 127) final double b,
- @NonNull double[] outXyz) {
+ double @NonNull [] outXyz) {
final double fy = (l + 16) / 116;
final double fx = a / 500 + fy;
final double fz = fy - b / 200;
@@ -589,7 +590,7 @@
* Returns the euclidean distance between two LAB colors.
*/
@SuppressWarnings("unused")
- public static double distanceEuclidean(@NonNull double[] labX, @NonNull double[] labY) {
+ public static double distanceEuclidean(double @NonNull [] labX, double @NonNull [] labY) {
return Math.sqrt(Math.pow(labX[0] - labY[0], 2)
+ Math.pow(labX[1] - labY[1], 2)
+ Math.pow(labX[2] - labY[2], 2));
@@ -646,8 +647,8 @@
* @param outResult 3-element array which holds the resulting HSL components
*/
@SuppressWarnings("unused")
- public static void blendHSL(@NonNull float[] hsl1, @NonNull float[] hsl2,
- @FloatRange(from = 0.0, to = 1.0) float ratio, @NonNull float[] outResult) {
+ public static void blendHSL(float @NonNull [] hsl1, float @NonNull [] hsl2,
+ @FloatRange(from = 0.0, to = 1.0) float ratio, float @NonNull [] outResult) {
if (outResult.length != 3) {
throw new IllegalArgumentException("result must have a length of 3.");
}
@@ -670,8 +671,8 @@
* @param outResult 3-element array which holds the resulting LAB components
*/
@SuppressWarnings("unused")
- public static void blendLAB(@NonNull double[] lab1, @NonNull double[] lab2,
- @FloatRange(from = 0.0, to = 1.0) double ratio, @NonNull double[] outResult) {
+ public static void blendLAB(double @NonNull [] lab1, double @NonNull [] lab2,
+ @FloatRange(from = 0.0, to = 1.0) double ratio, double @NonNull [] outResult) {
if (outResult.length != 3) {
throw new IllegalArgumentException("outResult must have a length of 3.");
}
@@ -720,7 +721,7 @@
* Tone).
*/
@SuppressWarnings("AcronymName")
- public static void colorToM3HCT(@ColorInt int color, @NonNull @Size(3) float[] outM3HCT) {
+ public static void colorToM3HCT(@ColorInt int color, @Size(3) float @NonNull [] outM3HCT) {
CamColor.getM3HCTfromColor(color, outM3HCT);
}
diff --git a/core/core/src/main/java/androidx/core/graphics/Insets.java b/core/core/src/main/java/androidx/core/graphics/Insets.java
index f88bff3..7642e34 100644
--- a/core/core/src/main/java/androidx/core/graphics/Insets.java
+++ b/core/core/src/main/java/androidx/core/graphics/Insets.java
@@ -20,10 +20,11 @@
import android.graphics.Rect;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
/**
* An Insets instance holds four integer offsets which describe changes to the four
* edges of a Rectangle. By convention, positive values move edges towards the
@@ -32,8 +33,7 @@
* Insets are immutable so may be treated as values.
*/
public final class Insets {
- @NonNull
- public static final Insets NONE = new Insets(0, 0, 0, 0);
+ public static final @NonNull Insets NONE = new Insets(0, 0, 0, 0);
public final int left;
public final int top;
@@ -58,8 +58,7 @@
* @param bottom the bottom inset
* @return Insets instance with the appropriate values
*/
- @NonNull
- public static Insets of(int left, int top, int right, int bottom) {
+ public static @NonNull Insets of(int left, int top, int right, int bottom) {
if (left == 0 && top == 0 && right == 0 && bottom == 0) {
return NONE;
}
@@ -72,8 +71,7 @@
* @param r the rectangle from which to take the values
* @return an Insets instance with the appropriate values
*/
- @NonNull
- public static Insets of(@NonNull Rect r) {
+ public static @NonNull Insets of(@NonNull Rect r) {
return of(r.left, r.top, r.right, r.bottom);
}
@@ -84,8 +82,7 @@
* @param b The second Insets to add.
* @return a + b, i. e. all insets on every side are added together.
*/
- @NonNull
- public static Insets add(@NonNull Insets a, @NonNull Insets b) {
+ public static @NonNull Insets add(@NonNull Insets a, @NonNull Insets b) {
return Insets.of(a.left + b.left, a.top + b.top, a.right + b.right, a.bottom + b.bottom);
}
@@ -97,8 +94,7 @@
* @return a - b, i. e. all insets on every side are subtracted from each other.
*/
@SuppressWarnings("unused")
- @NonNull
- public static Insets subtract(@NonNull Insets a, @NonNull Insets b) {
+ public static @NonNull Insets subtract(@NonNull Insets a, @NonNull Insets b) {
return Insets.of(a.left - b.left, a.top - b.top, a.right - b.right, a.bottom - b.bottom);
}
@@ -110,8 +106,7 @@
* @return an {@code Insets} instance where the inset on each side is the larger of
* the insets on that side from {@code a} and {@code b}.
*/
- @NonNull
- public static Insets max(@NonNull Insets a, @NonNull Insets b) {
+ public static @NonNull Insets max(@NonNull Insets a, @NonNull Insets b) {
return Insets.of(Math.max(a.left, b.left), Math.max(a.top, b.top),
Math.max(a.right, b.right), Math.max(a.bottom, b.bottom));
}
@@ -124,8 +119,7 @@
* @return an {@code Insets} instance where the inset on each side is the smaller of
* the insets on that side from {@code a} and {@code b}.
*/
- @NonNull
- public static Insets min(@NonNull Insets a, @NonNull Insets b) {
+ public static @NonNull Insets min(@NonNull Insets a, @NonNull Insets b) {
return Insets.of(Math.min(a.left, b.left), Math.min(a.top, b.top),
Math.min(a.right, b.right), Math.min(a.bottom, b.bottom));
}
@@ -162,9 +156,8 @@
return result;
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
return "Insets{left=" + left + ", top=" + top
+ ", right=" + right + ", bottom=" + bottom + '}';
}
@@ -173,10 +166,9 @@
* @deprecated Use {@link #toCompatInsets(android.graphics.Insets)} instead.
*/
@RequiresApi(api = 29)
- @NonNull
@Deprecated
@RestrictTo(LIBRARY_GROUP_PREFIX)
- public static Insets wrap(@NonNull android.graphics.Insets insets) {
+ public static @NonNull Insets wrap(android.graphics.@NonNull Insets insets) {
return toCompatInsets(insets);
}
@@ -185,8 +177,7 @@
* {@link Insets} instance from AndroidX.
*/
@RequiresApi(api = 29)
- @NonNull
- public static Insets toCompatInsets(@NonNull android.graphics.Insets insets) {
+ public static @NonNull Insets toCompatInsets(android.graphics.@NonNull Insets insets) {
return Insets.of(insets.left, insets.top, insets.right, insets.bottom);
}
@@ -195,8 +186,7 @@
* from the platform.
*/
@RequiresApi(29)
- @NonNull
- public android.graphics.Insets toPlatformInsets() {
+ public android.graphics.@NonNull Insets toPlatformInsets() {
return Api29Impl.of(left, top, right, bottom);
}
diff --git a/core/core/src/main/java/androidx/core/graphics/PaintCompat.java b/core/core/src/main/java/androidx/core/graphics/PaintCompat.java
index e2e51b4..cc7ae63 100644
--- a/core/core/src/main/java/androidx/core/graphics/PaintCompat.java
+++ b/core/core/src/main/java/androidx/core/graphics/PaintCompat.java
@@ -25,11 +25,12 @@
import android.graphics.Rect;
import android.os.Build;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.util.Pair;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Helper for accessing features in {@link Paint}.
*/
diff --git a/core/core/src/main/java/androidx/core/graphics/PathParser.java b/core/core/src/main/java/androidx/core/graphics/PathParser.java
index 9f77d78..7de28da 100644
--- a/core/core/src/main/java/androidx/core/graphics/PathParser.java
+++ b/core/core/src/main/java/androidx/core/graphics/PathParser.java
@@ -22,10 +22,11 @@
import android.graphics.Path;
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
// This class is a duplicate from the PathParser.java of frameworks/base, with slight
@@ -73,8 +74,7 @@
* @param pathData The string representing a path, the same as "d" string in svg file.
* @return the generated Path object.
*/
- @NonNull
- public static Path createPathFromPathData(@NonNull String pathData) {
+ public static @NonNull Path createPathFromPathData(@NonNull String pathData) {
Path path = new Path();
PathDataNode[] nodes = createNodesFromPathData(pathData);
try {
@@ -90,8 +90,7 @@
* @return an array of the PathDataNode.
*/
@SuppressWarnings("ArrayReturn")
- @NonNull
- public static PathDataNode[] createNodesFromPathData(@NonNull String pathData) {
+ public static PathDataNode @NonNull [] createNodesFromPathData(@NonNull String pathData) {
int start = 0;
int end = 1;
@@ -118,9 +117,8 @@
* @return a deep copy of the <code>source</code>.
*/
@SuppressWarnings("ArrayReturn")
- @NonNull
- public static PathDataNode[] deepCopyNodes(
- @SuppressWarnings("ArrayReturn") @NonNull PathDataNode[] source
+ public static PathDataNode @NonNull [] deepCopyNodes(
+ @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] source
) {
PathDataNode[] copy = new PathParser.PathDataNode[source.length];
for (int i = 0; i < source.length; i++) {
@@ -136,8 +134,8 @@
*/
@SuppressWarnings("ArrayReturn")
public static boolean canMorph(
- @SuppressWarnings("ArrayReturn") @Nullable PathDataNode[] nodesFrom,
- @SuppressWarnings("ArrayReturn") @Nullable PathDataNode[] nodesTo
+ @SuppressWarnings("ArrayReturn") PathDataNode @Nullable [] nodesFrom,
+ @SuppressWarnings("ArrayReturn") PathDataNode @Nullable [] nodesTo
) {
if (nodesFrom == null || nodesTo == null) {
return false;
@@ -164,8 +162,8 @@
* @param source The source path represented in an array of PathDataNode
*/
public static void updateNodes(
- @SuppressWarnings("ArrayReturn") @NonNull PathDataNode[] target,
- @SuppressWarnings("ArrayReturn") @NonNull PathDataNode[] source
+ @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] target,
+ @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] source
) {
for (int i = 0; i < source.length; i++) {
target[i].mType = source[i].mType;
@@ -318,10 +316,10 @@
* @see #canMorph(PathDataNode[], PathDataNode[])
*/
public static void interpolatePathDataNodes(
- @SuppressWarnings("ArrayReturn") @NonNull PathDataNode[] target,
+ @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] target,
float fraction,
- @SuppressWarnings("ArrayReturn") @NonNull PathDataNode[] from,
- @SuppressWarnings("ArrayReturn") @NonNull PathDataNode[] to
+ @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] from,
+ @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] to
) {
if (!interpolatePathDataNodes(target, from, to, fraction)) {
throw new IllegalArgumentException(
@@ -347,9 +345,9 @@
@Deprecated
@RestrictTo(LIBRARY_GROUP_PREFIX)
public static boolean interpolatePathDataNodes(
- @SuppressWarnings("ArrayReturn") @NonNull PathDataNode[] target,
- @SuppressWarnings("ArrayReturn") @NonNull PathDataNode[] from,
- @SuppressWarnings("ArrayReturn") @NonNull PathDataNode[] to,
+ @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] target,
+ @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] from,
+ @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] to,
float fraction
) {
if (target.length != from.length || from.length != to.length) {
@@ -375,7 +373,7 @@
*/
@SuppressWarnings("ArrayReturn")
public static void nodesToPath(
- @SuppressWarnings("ArrayReturn") @NonNull PathDataNode[] node,
+ @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] node,
@NonNull Path path
) {
float[] current = new float[6];
@@ -406,8 +404,7 @@
return mType;
}
- @NonNull
- public float[] getParams() {
+ public float @NonNull [] getParams() {
return mParams;
}
@@ -432,7 +429,7 @@
@RestrictTo(LIBRARY_GROUP_PREFIX)
@SuppressWarnings("ArrayReturn")
public static void nodesToPath(
- @SuppressWarnings("ArrayReturn") @NonNull PathDataNode[] node,
+ @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] node,
@NonNull Path path
) {
PathParser.nodesToPath(node, path);
diff --git a/core/core/src/main/java/androidx/core/graphics/PathSegment.java b/core/core/src/main/java/androidx/core/graphics/PathSegment.java
index a5fe5cb..216d4d3 100644
--- a/core/core/src/main/java/androidx/core/graphics/PathSegment.java
+++ b/core/core/src/main/java/androidx/core/graphics/PathSegment.java
@@ -21,7 +21,7 @@
import android.graphics.Path;
import android.graphics.PointF;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* A line segment that represents an approximate fraction of a {@link Path} after
@@ -42,8 +42,7 @@
}
/** The start point of the line segment. */
- @NonNull
- public PointF getStart() {
+ public @NonNull PointF getStart() {
return mStart;
}
@@ -55,8 +54,7 @@
}
/** The end point of the line segment. */
- @NonNull
- public PointF getEnd() {
+ public @NonNull PointF getEnd() {
return mEnd;
}
diff --git a/core/core/src/main/java/androidx/core/graphics/PathUtils.java b/core/core/src/main/java/androidx/core/graphics/PathUtils.java
index 043f1c4..e4c5f16 100644
--- a/core/core/src/main/java/androidx/core/graphics/PathUtils.java
+++ b/core/core/src/main/java/androidx/core/graphics/PathUtils.java
@@ -20,9 +20,10 @@
import android.graphics.PointF;
import androidx.annotation.FloatRange;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -40,8 +41,7 @@
* @see #flatten(Path, float)
*/
@RequiresApi(26)
- @NonNull
- public static Collection<PathSegment> flatten(@NonNull Path path) {
+ public static @NonNull Collection<PathSegment> flatten(@NonNull Path path) {
return flatten(path, 0.5f);
}
@@ -57,8 +57,7 @@
* @see Path#approximate
*/
@RequiresApi(26)
- @NonNull
- public static Collection<PathSegment> flatten(@NonNull final Path path,
+ public static @NonNull Collection<PathSegment> flatten(final @NonNull Path path,
@FloatRange(from = 0) final float error) {
float[] pathData = Api26Impl.approximate(path, error);
int pointCount = pathData.length / 3;
diff --git a/core/core/src/main/java/androidx/core/graphics/TypefaceCompat.java b/core/core/src/main/java/androidx/core/graphics/TypefaceCompat.java
index fcf65ad..c7163d5 100644
--- a/core/core/src/main/java/androidx/core/graphics/TypefaceCompat.java
+++ b/core/core/src/main/java/androidx/core/graphics/TypefaceCompat.java
@@ -27,8 +27,6 @@
import android.os.Handler;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
@@ -44,6 +42,9 @@
import androidx.core.util.Preconditions;
import androidx.tracing.Trace;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.List;
/**
@@ -90,9 +91,8 @@
*
* @return null if not found.
*/
- @Nullable
@RestrictTo(LIBRARY)
- public static Typeface findFromCache(@NonNull Resources resources, int id,
+ public static @Nullable Typeface findFromCache(@NonNull Resources resources, int id,
@Nullable String path, int cookie, int style) {
return sTypefaceCache.get(createResourceUid(resources, id, path, cookie, style));
}
@@ -103,10 +103,10 @@
* @return null if not found.
* @deprecated Use {@link #findFromCache(Resources, int, String, int, int)} method
*/
- @Nullable
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Deprecated
- public static Typeface findFromCache(@NonNull Resources resources, int id, int style) {
+ public static @Nullable Typeface findFromCache(@NonNull Resources resources, int id,
+ int style) {
return findFromCache(resources, id, null, 0, style);
}
@@ -150,13 +150,12 @@
*
* @return null if failed to create.
*/
- @Nullable
@RestrictTo(LIBRARY)
- public static Typeface createFromResourcesFamilyXml(
+ public static @Nullable Typeface createFromResourcesFamilyXml(
@NonNull Context context, @NonNull FamilyResourceEntry entry,
@NonNull Resources resources, int id, @Nullable String path,
int cookie, int style,
- @Nullable ResourcesCompat.FontCallback fontCallback, @Nullable Handler handler,
+ ResourcesCompat.@Nullable FontCallback fontCallback, @Nullable Handler handler,
boolean isRequestFromLayoutInflator) {
Typeface typeface;
if (entry instanceof ProviderResourceEntry) {
@@ -215,13 +214,12 @@
* @deprecated Use {@link #createFromResourcesFamilyXml(Context, FamilyResourceEntry,
* Resources, int, String, int, int, ResourcesCompat.FontCallback, Handler, boolean)} method
*/
- @Nullable
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Deprecated
- public static Typeface createFromResourcesFamilyXml(
+ public static @Nullable Typeface createFromResourcesFamilyXml(
@NonNull Context context, @NonNull FamilyResourceEntry entry,
@NonNull Resources resources, int id, int style,
- @Nullable ResourcesCompat.FontCallback fontCallback, @Nullable Handler handler,
+ ResourcesCompat.@Nullable FontCallback fontCallback, @Nullable Handler handler,
boolean isRequestFromLayoutInflator) {
return createFromResourcesFamilyXml(context, entry, resources, id, null, 0, style,
fontCallback, handler, isRequestFromLayoutInflator);
@@ -230,9 +228,8 @@
/**
* Used by Resources to load a font resource of type font file.
*/
- @Nullable
@RestrictTo(LIBRARY)
- public static Typeface createFromResourcesFontFile(
+ public static @Nullable Typeface createFromResourcesFontFile(
@NonNull Context context, @NonNull Resources resources, int id, String path, int cookie,
int style) {
Typeface typeface = sTypefaceCompatImpl.createFromResourcesFontFile(
@@ -249,10 +246,9 @@
* @deprecated Use {@link #createFromResourcesFontFile(Context, Resources, int, String,
* int, int)} method
*/
- @Nullable
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Deprecated
- public static Typeface createFromResourcesFontFile(
+ public static @Nullable Typeface createFromResourcesFontFile(
@NonNull Context context, @NonNull Resources resources, int id, String path,
int style) {
return createFromResourcesFontFile(context, resources, id, path, 0, style);
@@ -261,10 +257,10 @@
/**
* Create a Typeface from a given FontInfo list and a map that matches them to ByteBuffers.
*/
- @Nullable
@RestrictTo(LIBRARY_GROUP_PREFIX)
- public static Typeface createFromFontInfo(@NonNull Context context,
- @Nullable CancellationSignal cancellationSignal, @NonNull FontInfo[] fonts, int style) {
+ public static @Nullable Typeface createFromFontInfo(@NonNull Context context,
+ @Nullable CancellationSignal cancellationSignal, FontInfo @NonNull [] fonts,
+ int style) {
if (TypefaceCompat.DOWNLOADABLE_FONT_TRACING) {
Trace.beginSection("TypefaceCompat.createFromFontInfo");
}
@@ -283,10 +279,9 @@
* <p>
* This currently throws an exception if used below API 29.
*/
- @Nullable
@RestrictTo(LIBRARY)
@RequiresApi(29)
- public static Typeface createFromFontInfoWithFallback(@NonNull Context context,
+ public static @Nullable Typeface createFromFontInfoWithFallback(@NonNull Context context,
@Nullable CancellationSignal cancellationSignal, @NonNull List<FontInfo[]> fonts,
int style) {
if (TypefaceCompat.DOWNLOADABLE_FONT_TRACING) {
@@ -306,9 +301,8 @@
/**
* Retrieves the best matching font from the family specified by the {@link Typeface} object
*/
- @Nullable
- private static Typeface getBestFontFromFamily(final Context context, final Typeface typeface,
- final int style) {
+ private static @Nullable Typeface getBestFontFromFamily(final Context context,
+ final Typeface typeface, final int style) {
final FontFamilyFilesResourceEntry families = sTypefaceCompatImpl.getFontFamily(typeface);
if (families == null) {
return null;
@@ -327,9 +321,8 @@
* @param context The context used to retrieve the font.
* @return The best matching typeface.
*/
- @NonNull
- public static Typeface create(@NonNull final Context context, @Nullable final Typeface family,
- final int style) {
+ public static @NonNull Typeface create(final @NonNull Context context,
+ final @Nullable Typeface family, final int style) {
if (context == null) {
throw new IllegalArgumentException("Context cannot be null");
}
@@ -381,8 +374,7 @@
* @see Typeface#getWeight()
* @see Typeface#isItalic()
*/
- @NonNull
- public static Typeface create(@NonNull Context context, @Nullable Typeface family,
+ public static @NonNull Typeface create(@NonNull Context context, @Nullable Typeface family,
@IntRange(from = 1, to = 1000) int weight, boolean italic) {
if (context == null) {
throw new IllegalArgumentException("Context cannot be null");
@@ -412,10 +404,9 @@
*/
@RestrictTo(LIBRARY)
public static class ResourcesCallbackAdapter extends FontsContractCompat.FontRequestCallback {
- @Nullable
- private ResourcesCompat.FontCallback mFontCallback;
+ private ResourcesCompat.@Nullable FontCallback mFontCallback;
- public ResourcesCallbackAdapter(@Nullable ResourcesCompat.FontCallback fontCallback) {
+ public ResourcesCallbackAdapter(ResourcesCompat.@Nullable FontCallback fontCallback) {
mFontCallback = fontCallback;
}
diff --git a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi21Impl.java b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi21Impl.java
index 0a7a7bb..dcd0f4939 100644
--- a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi21Impl.java
+++ b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi21Impl.java
@@ -29,13 +29,14 @@
import android.system.OsConstants;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.content.res.FontResourcesParserCompat.FontFamilyFilesResourceEntry;
import androidx.core.content.res.FontResourcesParserCompat.FontFileResourceEntry;
import androidx.core.provider.FontsContractCompat.FontInfo;
+import org.jspecify.annotations.NonNull;
+
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@@ -44,7 +45,6 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
-
/**
* Implementation of the Typeface compat methods for API 21 and above.
*/
@@ -144,7 +144,7 @@
@Override
public Typeface createFromFontInfo(Context context, CancellationSignal cancellationSignal,
- @NonNull FontInfo[] fonts, int style) {
+ FontInfo @NonNull [] fonts, int style) {
if (fonts.length < 1) {
return null;
}
@@ -198,9 +198,8 @@
return createFromFamiliesWithDefault(family);
}
- @NonNull
@Override
- Typeface createWeightStyle(@NonNull Context context,
+ @NonNull Typeface createWeightStyle(@NonNull Context context,
@NonNull Typeface base, int weight, boolean italic) {
Typeface out = null;
try {
diff --git a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi24Impl.java b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi24Impl.java
index 7e1b656..0e37d61 100644
--- a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi24Impl.java
+++ b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi24Impl.java
@@ -25,8 +25,6 @@
import android.os.CancellationSignal;
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.collection.SimpleArrayMap;
@@ -34,6 +32,9 @@
import androidx.core.content.res.FontResourcesParserCompat.FontFileResourceEntry;
import androidx.core.provider.FontsContractCompat.FontInfo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
@@ -41,7 +42,6 @@
import java.nio.ByteBuffer;
import java.util.List;
-
/**
* Implementation of the Typeface compat methods for API 24 and above.
*/
@@ -128,9 +128,9 @@
}
@Override
- @Nullable
- public Typeface createFromFontInfo(Context context,
- @Nullable CancellationSignal cancellationSignal, @NonNull FontInfo[] fonts, int style) {
+ public @Nullable Typeface createFromFontInfo(Context context,
+ @Nullable CancellationSignal cancellationSignal, FontInfo @NonNull [] fonts,
+ int style) {
Object family = newFamily();
if (family == null) {
return null;
@@ -160,8 +160,7 @@
}
@Override
- @Nullable
- public Typeface createFromFontFamilyFilesResourceEntry(Context context,
+ public @Nullable Typeface createFromFontFamilyFilesResourceEntry(Context context,
FontFamilyFilesResourceEntry entry, Resources resources, int style) {
Object family = newFamily();
if (family == null) {
@@ -180,9 +179,8 @@
return createFromFamiliesWithDefault(family);
}
- @NonNull
@Override
- Typeface createWeightStyle(@NonNull Context context,
+ @NonNull Typeface createWeightStyle(@NonNull Context context,
@NonNull Typeface base, int weight, boolean italic) {
Typeface out = null;
try {
diff --git a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi26Impl.java b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi26Impl.java
index b1353688..955ef6f 100644
--- a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi26Impl.java
+++ b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi26Impl.java
@@ -29,14 +29,15 @@
import android.os.ParcelFileDescriptor;
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.content.res.FontResourcesParserCompat;
import androidx.core.content.res.FontResourcesParserCompat.FontFileResourceEntry;
import androidx.core.provider.FontsContractCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
@@ -120,8 +121,7 @@
/**
* Create a new FontFamily instance
*/
- @Nullable
- private Object newFamily() {
+ private @Nullable Object newFamily() {
try {
return mFontFamilyCtor.newInstance();
} catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
@@ -134,7 +134,7 @@
* boolean isAsset, int ttcIndex, int weight, int isItalic, FontVariationAxis[] axes)
*/
private boolean addFontFromAssetManager(Context context, Object family, String fileName,
- int ttcIndex, int weight, int style, @Nullable FontVariationAxis[] axes) {
+ int ttcIndex, int weight, int style, FontVariationAxis @Nullable [] axes) {
try {
return (Boolean) mAddFontFromAssetManager.invoke(family,
context.getAssets(), fileName, 0 /* cookie */, false /* isAsset */, ttcIndex,
@@ -162,8 +162,7 @@
* Call method Typeface#createFromFamiliesWithDefault(
* FontFamily[] families, int weight, int italic)
*/
- @Nullable
- protected Typeface createFromFamiliesWithDefault(Object family) {
+ protected @Nullable Typeface createFromFamiliesWithDefault(Object family) {
try {
Object familyArray = Array.newInstance(mFontFamily, 1);
Array.set(familyArray, 0, family);
@@ -195,8 +194,7 @@
}
@Override
- @Nullable
- public Typeface createFromFontFamilyFilesResourceEntry(Context context,
+ public @Nullable Typeface createFromFontFamilyFilesResourceEntry(Context context,
FontResourcesParserCompat.FontFamilyFilesResourceEntry entry, Resources resources,
int style) {
if (!isFontFamilyPrivateAPIAvailable()) {
@@ -221,10 +219,9 @@
}
@Override
- @Nullable
- public Typeface createFromFontInfo(Context context,
+ public @Nullable Typeface createFromFontInfo(Context context,
@Nullable CancellationSignal cancellationSignal,
- @NonNull FontsContractCompat.FontInfo[] fonts, int style) {
+ FontsContractCompat.FontInfo @NonNull [] fonts, int style) {
if (fonts.length < 1) {
return null;
}
@@ -283,9 +280,8 @@
/**
* Used by Resources to load a font resource of type font file.
*/
- @Nullable
@Override
- public Typeface createFromResourcesFontFile(
+ public @Nullable Typeface createFromResourcesFontFile(
Context context, Resources resources, int id, String path, int style) {
if (!isFontFamilyPrivateAPIAvailable()) {
return super.createFromResourcesFontFile(context, resources, id, path, style);
@@ -351,9 +347,8 @@
return m;
}
- @NonNull
@Override
- Typeface createWeightStyle(@NonNull Context context,
+ @NonNull Typeface createWeightStyle(@NonNull Context context,
@NonNull Typeface base, int weight, boolean italic) {
Typeface out = null;
try {
diff --git a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi28Impl.java b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi28Impl.java
index 18f4d17..c4d90cf 100644
--- a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi28Impl.java
+++ b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi28Impl.java
@@ -21,10 +21,11 @@
import android.content.Context;
import android.graphics.Typeface;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -66,9 +67,8 @@
return m;
}
- @NonNull
@Override
- Typeface createWeightStyle(@NonNull Context context,
+ @NonNull Typeface createWeightStyle(@NonNull Context context,
@NonNull Typeface base, int weight, boolean italic) {
return Typeface.create(base, weight, italic);
}
diff --git a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi29Impl.java b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi29Impl.java
index ff8d800..5990c9e 100644
--- a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi29Impl.java
+++ b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi29Impl.java
@@ -29,13 +29,14 @@
import android.os.ParcelFileDescriptor;
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.content.res.FontResourcesParserCompat;
import androidx.core.provider.FontsContractCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
@@ -83,11 +84,10 @@
throw new RuntimeException("Do not use this function in API 29 or later.");
}
- @Nullable
@Override
- public Typeface createFromFontInfo(Context context,
+ public @Nullable Typeface createFromFontInfo(Context context,
@Nullable CancellationSignal cancellationSignal,
- @NonNull FontsContractCompat.FontInfo[] fonts, int style) {
+ FontsContractCompat.FontInfo @NonNull [] fonts, int style) {
final ContentResolver resolver = context.getContentResolver();
try {
final FontFamily family = getFontFamily(cancellationSignal, fonts, resolver);
@@ -103,7 +103,7 @@
private static @Nullable FontFamily getFontFamily(
@Nullable CancellationSignal cancellationSignal,
- @NonNull FontsContractCompat.FontInfo[] fonts, ContentResolver resolver) {
+ FontsContractCompat.FontInfo @NonNull [] fonts, ContentResolver resolver) {
FontFamily.Builder familyBuilder = null;
for (FontsContractCompat.FontInfo font : fonts) {
try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(font.getUri(), "r",
@@ -134,9 +134,8 @@
return family;
}
- @Nullable
@Override
- public Typeface createFromFontInfoWithFallback(@NonNull Context context,
+ public @Nullable Typeface createFromFontInfoWithFallback(@NonNull Context context,
@Nullable CancellationSignal cancellationSignal,
@NonNull List<FontsContractCompat.FontInfo[]> fonts, int style) {
final ContentResolver resolver = context.getContentResolver();
@@ -163,9 +162,8 @@
}
}
- @Nullable
@Override
- public Typeface createFromFontFamilyFilesResourceEntry(Context context,
+ public @Nullable Typeface createFromFontFamilyFilesResourceEntry(Context context,
FontResourcesParserCompat.FontFamilyFilesResourceEntry familyEntry, Resources resources,
int style) {
try {
@@ -204,9 +202,8 @@
/**
* Used by Resources to load a font resource of type font file.
*/
- @Nullable
@Override
- public Typeface createFromResourcesFontFile(
+ public @Nullable Typeface createFromResourcesFontFile(
Context context, Resources resources, int id, String path, int style) {
FontFamily family = null;
Font font = null;
@@ -223,9 +220,8 @@
}
}
- @NonNull
@Override
- Typeface createWeightStyle(@NonNull Context context,
+ @NonNull Typeface createWeightStyle(@NonNull Context context,
@NonNull Typeface base, int weight, boolean italic) {
return Typeface.create(base, weight, italic);
}
diff --git a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatBaseImpl.java b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatBaseImpl.java
index 15e4e20..af49e70 100644
--- a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatBaseImpl.java
+++ b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatBaseImpl.java
@@ -25,14 +25,15 @@
import android.os.CancellationSignal;
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.content.res.FontResourcesParserCompat.FontFamilyFilesResourceEntry;
import androidx.core.content.res.FontResourcesParserCompat.FontFileResourceEntry;
import androidx.core.provider.FontsContractCompat.FontInfo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -83,7 +84,7 @@
return best;
}
- private static long getUniqueKey(@Nullable final Typeface typeface) {
+ private static long getUniqueKey(final @Nullable Typeface typeface) {
if (typeface == null) {
return INVALID_KEY;
}
@@ -137,9 +138,9 @@
}
}
- @Nullable
- public Typeface createFromFontInfo(Context context,
- @Nullable CancellationSignal cancellationSignal, @NonNull FontInfo[] fonts, int style) {
+ public @Nullable Typeface createFromFontInfo(Context context,
+ @Nullable CancellationSignal cancellationSignal, FontInfo @NonNull [] fonts,
+ int style) {
// When we load from file, we can only load one font so just take the first one.
if (fonts.length < 1) {
return null;
@@ -156,9 +157,8 @@
}
}
- @Nullable
@RequiresApi(29)
- public Typeface createFromFontInfoWithFallback(@NonNull Context context,
+ public @Nullable Typeface createFromFontInfoWithFallback(@NonNull Context context,
@Nullable CancellationSignal cancellationSignal,
@NonNull List<FontInfo[]> fonts, int style) {
throw new IllegalStateException(
@@ -195,8 +195,7 @@
});
}
- @Nullable
- public Typeface createFromFontFamilyFilesResourceEntry(Context context,
+ public @Nullable Typeface createFromFontFamilyFilesResourceEntry(Context context,
FontFamilyFilesResourceEntry entry, Resources resources, int style) {
FontFileResourceEntry best = findBestEntry(entry, style);
if (best == null) {
@@ -210,8 +209,7 @@
return typeface;
}
- @Nullable
- Typeface createFromFontFamilyFilesResourceEntry(Context context,
+ @Nullable Typeface createFromFontFamilyFilesResourceEntry(Context context,
FontFamilyFilesResourceEntry entry, Resources resources, int weight, boolean italic) {
FontFileResourceEntry best = findBestEntry(entry, weight, italic);
if (best == null) {
@@ -228,8 +226,7 @@
/**
* Used by Resources to load a font resource of type font file.
*/
- @Nullable
- public Typeface createFromResourcesFontFile(
+ public @Nullable Typeface createFromResourcesFontFile(
Context context, Resources resources, int id, String path, int style) {
final File tmpFile = TypefaceCompatUtil.getTempFile(context);
if (tmpFile == null) {
@@ -250,8 +247,7 @@
}
}
- @NonNull
- Typeface createWeightStyle(@NonNull Context context, @NonNull Typeface base,
+ @NonNull Typeface createWeightStyle(@NonNull Context context, @NonNull Typeface base,
int weight, boolean italic) {
Typeface out = null;
try {
@@ -268,8 +264,7 @@
/**
* Retrieves the font family resource entries given a unique identifier for a Typeface
*/
- @Nullable
- FontFamilyFilesResourceEntry getFontFamily(final Typeface typeface) {
+ @Nullable FontFamilyFilesResourceEntry getFontFamily(final Typeface typeface) {
final long key = getUniqueKey(typeface);
if (key == INVALID_KEY) {
return null;
diff --git a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatUtil.java b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatUtil.java
index 710ef80f..d66858b 100644
--- a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatUtil.java
+++ b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatUtil.java
@@ -29,11 +29,12 @@
import android.os.StrictMode;
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.provider.FontsContractCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
@@ -62,8 +63,7 @@
*
* Returns null if failed to create temp file.
*/
- @Nullable
- public static File getTempFile(@NonNull Context context) {
+ public static @Nullable File getTempFile(@NonNull Context context) {
File cacheDir = context.getCacheDir();
if (cacheDir == null) {
return null;
@@ -86,8 +86,7 @@
/**
* Copy the file contents to the direct byte buffer.
*/
- @Nullable
- private static ByteBuffer mmap(File file) {
+ private static @Nullable ByteBuffer mmap(File file) {
try (FileInputStream fis = new FileInputStream(file)) {
FileChannel channel = fis.getChannel();
final long size = channel.size();
@@ -100,8 +99,7 @@
/**
* Copy the file contents to the direct byte buffer.
*/
- @Nullable
- public static ByteBuffer mmap(@NonNull Context context,
+ public static @Nullable ByteBuffer mmap(@NonNull Context context,
@Nullable CancellationSignal cancellationSignal, @NonNull Uri uri) {
final ContentResolver resolver = context.getContentResolver();
try {
@@ -125,9 +123,8 @@
* Copy the resource contents to the direct byte buffer.
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
- @Nullable
- public static ByteBuffer copyToDirectBuffer(@NonNull Context context, @NonNull Resources res,
- int id) {
+ public static @Nullable ByteBuffer copyToDirectBuffer(@NonNull Context context,
+ @NonNull Resources res, int id) {
File tmpFile = getTempFile(context);
if (tmpFile == null) {
return null;
@@ -205,10 +202,9 @@
* @return A map from {@link Uri} to {@link ByteBuffer}.
*/
@RestrictTo(LIBRARY)
- @NonNull
- public static Map<Uri, ByteBuffer> readFontInfoIntoByteBuffer(
+ public static @NonNull Map<Uri, ByteBuffer> readFontInfoIntoByteBuffer(
@NonNull Context context,
- @NonNull FontsContractCompat.FontInfo[] fonts,
+ FontsContractCompat.FontInfo @NonNull [] fonts,
@Nullable CancellationSignal cancellationSignal
) {
final HashMap<Uri, ByteBuffer> out = new HashMap<>();
diff --git a/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi14.java b/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi14.java
index 90298ca..01606cb 100644
--- a/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi14.java
+++ b/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi14.java
@@ -24,12 +24,13 @@
import android.util.SparseArray;
import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.collection.LongSparseArray;
import androidx.core.content.res.FontResourcesParserCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.reflect.Field;
/**
@@ -74,8 +75,7 @@
/**
* @return Valid typeface, or {@code null} if private API is not available
*/
- @Nullable
- static Typeface createWeightStyle(@NonNull TypefaceCompatBaseImpl compat,
+ static @Nullable Typeface createWeightStyle(@NonNull TypefaceCompatBaseImpl compat,
@NonNull Context context, @NonNull Typeface base, int weight, boolean italic) {
if (!isPrivateApiAvailable()) {
return null;
@@ -128,8 +128,7 @@
/**
* @see {@code TypefaceCompat#getBestFontFromFamily(Context, Typeface, int)}
*/
- @Nullable
- private static Typeface getBestFontFromFamily(@NonNull TypefaceCompatBaseImpl compat,
+ private static @Nullable Typeface getBestFontFromFamily(@NonNull TypefaceCompatBaseImpl compat,
@NonNull Context context, @NonNull Typeface base, int weight, boolean italic) {
final FontResourcesParserCompat.FontFamilyFilesResourceEntry family =
compat.getFontFamily(base);
diff --git a/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi21.java b/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi21.java
index b07f0ae..6eb24a6 100644
--- a/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi21.java
+++ b/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi21.java
@@ -24,12 +24,13 @@
import android.util.SparseArray;
import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.collection.LongSparseArray;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
@@ -99,8 +100,8 @@
/**
* @return Valid typeface, or {@code null} if private API is not available
*/
- @Nullable
- static Typeface createWeightStyle(@NonNull Typeface base, int weight, boolean italic) {
+ static @Nullable Typeface createWeightStyle(@NonNull Typeface base, int weight,
+ boolean italic) {
if (!isPrivateApiAvailable()) {
return null;
}
@@ -170,8 +171,7 @@
}
}
- @Nullable
- private static Typeface create(long nativeInstance) {
+ private static @Nullable Typeface create(long nativeInstance) {
try {
return sConstructor.newInstance(nativeInstance);
} catch (IllegalAccessException e) {
diff --git a/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi26.java b/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi26.java
index 2a8d51f..3602562 100644
--- a/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi26.java
+++ b/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi26.java
@@ -24,12 +24,13 @@
import android.util.SparseArray;
import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.collection.LongSparseArray;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
@@ -93,8 +94,8 @@
/**
* @return Valid typeface, or {@code null} if private API is not available
*/
- @Nullable
- static Typeface createWeightStyle(@NonNull Typeface base, int weight, boolean italic) {
+ static @Nullable Typeface createWeightStyle(@NonNull Typeface base, int weight,
+ boolean italic) {
if (!isPrivateApiAvailable()) {
return null;
}
@@ -144,8 +145,7 @@
}
}
- @Nullable
- private static Typeface create(long nativeInstance) {
+ private static @Nullable Typeface create(long nativeInstance) {
try {
return sConstructor.newInstance(nativeInstance);
} catch (IllegalAccessException e) {
diff --git a/core/core/src/main/java/androidx/core/graphics/drawable/DrawableCompat.java b/core/core/src/main/java/androidx/core/graphics/drawable/DrawableCompat.java
index 4d4ad3b..67b7675 100644
--- a/core/core/src/main/java/androidx/core/graphics/drawable/DrawableCompat.java
+++ b/core/core/src/main/java/androidx/core/graphics/drawable/DrawableCompat.java
@@ -29,11 +29,11 @@
import android.view.View;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.view.ViewCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -166,7 +166,7 @@
* @param drawable The Drawable against which to invoke the method.
* @param tintMode A Porter-Duff blending mode
*/
- public static void setTintMode(@NonNull Drawable drawable, @Nullable PorterDuff.Mode tintMode) {
+ public static void setTintMode(@NonNull Drawable drawable, PorterDuff.@Nullable Mode tintMode) {
if (Build.VERSION.SDK_INT >= 21) {
Api21Impl.setTintMode(drawable, tintMode);
} else if (drawable instanceof TintAwareDrawable) {
@@ -192,7 +192,7 @@
* Applies the specified theme to this Drawable and its children.
*/
@SuppressWarnings("unused")
- public static void applyTheme(@NonNull Drawable drawable, @NonNull Resources.Theme theme) {
+ public static void applyTheme(@NonNull Drawable drawable, Resources.@NonNull Theme theme) {
if (Build.VERSION.SDK_INT >= 21) {
Api21Impl.applyTheme(drawable, theme);
}
@@ -216,8 +216,7 @@
* @return the current color filter, or {@code null} if none set
*/
@SuppressWarnings("unused")
- @Nullable
- public static ColorFilter getColorFilter(@NonNull Drawable drawable) {
+ public static @Nullable ColorFilter getColorFilter(@NonNull Drawable drawable) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.getColorFilter(drawable);
} else {
@@ -276,7 +275,7 @@
*/
public static void inflate(@NonNull Drawable drawable, @NonNull Resources res,
@NonNull XmlPullParser parser, @NonNull AttributeSet attrs,
- @Nullable Resources.Theme theme)
+ Resources.@Nullable Theme theme)
throws XmlPullParserException, IOException {
if (Build.VERSION.SDK_INT >= 21) {
Api21Impl.inflate(drawable, res, parser, attrs, theme);
@@ -316,8 +315,7 @@
* @see #setTintMode(Drawable, PorterDuff.Mode)
* @see #unwrap(Drawable)
*/
- @NonNull
- public static Drawable wrap(@NonNull Drawable drawable) {
+ public static @NonNull Drawable wrap(@NonNull Drawable drawable) {
if (Build.VERSION.SDK_INT >= 23) {
return drawable;
} else if (Build.VERSION.SDK_INT >= 21) {
diff --git a/core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java b/core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java
index 881d8ec..bc9d297 100644
--- a/core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java
+++ b/core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java
@@ -52,8 +52,6 @@
import androidx.annotation.DrawableRes;
import androidx.annotation.IdRes;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
@@ -66,6 +64,9 @@
import androidx.versionedparcelable.ParcelField;
import androidx.versionedparcelable.VersionedParcelize;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
@@ -169,16 +170,14 @@
/**
*/
- @Nullable
@RestrictTo(LIBRARY)
@ParcelField(value = 2, defaultValue = "null")
- public byte[] mData = null;
+ public byte @Nullable [] mData = null;
/**
*/
- @Nullable
@RestrictTo(LIBRARY)
@ParcelField(value = 3, defaultValue = "null")
- public Parcelable mParcelable = null;
+ public @Nullable Parcelable mParcelable = null;
// TYPE_RESOURCE: resId
// TYPE_DATA: data offset
@@ -197,27 +196,24 @@
/**
*/
- @Nullable
@RestrictTo(LIBRARY)
@ParcelField(value = 6, defaultValue = "null")
- public ColorStateList mTintList = null;
+ public @Nullable ColorStateList mTintList = null;
static final PorterDuff.Mode DEFAULT_TINT_MODE = PorterDuff.Mode.SRC_IN; // SRC_IN
@NonParcelField
PorterDuff.Mode mTintMode = DEFAULT_TINT_MODE;
/**
*/
- @Nullable
@RestrictTo(LIBRARY)
@ParcelField(value = 7, defaultValue = "null")
- public String mTintModeStr = null;
+ public @Nullable String mTintModeStr = null;
/**
*/
- @Nullable
@RestrictTo(LIBRARY)
@ParcelField(value = 8, defaultValue = "null")
- public String mString1;
+ public @Nullable String mString1;
/**
* Create an Icon pointing to a drawable resource.
@@ -226,17 +222,17 @@
* @param resId ID of the drawable resource
* @see android.graphics.drawable.Icon#createWithResource(Context, int)
*/
- @NonNull
- public static IconCompat createWithResource(@NonNull Context context, @DrawableRes int resId) {
+ public static @NonNull IconCompat createWithResource(@NonNull Context context,
+ @DrawableRes int resId) {
ObjectsCompat.requireNonNull(context);
return createWithResource(context.getResources(), context.getPackageName(), resId);
}
/**
*/
- @NonNull
@RestrictTo(LIBRARY_GROUP_PREFIX)
- public static IconCompat createWithResource(@Nullable Resources r, @NonNull String pkg,
+ public static @NonNull IconCompat createWithResource(@Nullable Resources r,
+ @NonNull String pkg,
@DrawableRes int resId) {
ObjectsCompat.requireNonNull(pkg);
if (resId == 0) {
@@ -262,8 +258,7 @@
* @param bits A valid {@link android.graphics.Bitmap} object
* @see android.graphics.drawable.Icon#createWithBitmap(Bitmap)
*/
- @NonNull
- public static IconCompat createWithBitmap(@NonNull Bitmap bits) {
+ public static @NonNull IconCompat createWithBitmap(@NonNull Bitmap bits) {
ObjectsCompat.requireNonNull(bits);
final IconCompat rep = new IconCompat(TYPE_BITMAP);
rep.mObj1 = bits;
@@ -276,8 +271,7 @@
* @param bits A valid {@link android.graphics.Bitmap} object
* @see android.graphics.drawable.Icon#createWithAdaptiveBitmap(Bitmap)
*/
- @NonNull
- public static IconCompat createWithAdaptiveBitmap(@NonNull Bitmap bits) {
+ public static @NonNull IconCompat createWithAdaptiveBitmap(@NonNull Bitmap bits) {
ObjectsCompat.requireNonNull(bits);
final IconCompat rep = new IconCompat(TYPE_ADAPTIVE_BITMAP);
rep.mObj1 = bits;
@@ -293,8 +287,8 @@
* @param length Length of the bitmap data
* @see android.graphics.drawable.Icon#createWithData(byte[], int, int)
*/
- @NonNull
- public static IconCompat createWithData(@NonNull byte[] data, int offset, int length) {
+ public static @NonNull IconCompat createWithData(byte @NonNull [] data, int offset,
+ int length) {
ObjectsCompat.requireNonNull(data);
final IconCompat rep = new IconCompat(TYPE_DATA);
rep.mObj1 = data;
@@ -309,8 +303,7 @@
* @param uri A uri referring to local content:// or file:// image data.
* @see android.graphics.drawable.Icon#createWithContentUri(String)
*/
- @NonNull
- public static IconCompat createWithContentUri(@NonNull String uri) {
+ public static @NonNull IconCompat createWithContentUri(@NonNull String uri) {
ObjectsCompat.requireNonNull(uri);
final IconCompat rep = new IconCompat(TYPE_URI);
rep.mObj1 = uri;
@@ -323,8 +316,7 @@
* @param uri A uri referring to local content:// or file:// image data.
* @see android.graphics.drawable.Icon#createWithContentUri(String)
*/
- @NonNull
- public static IconCompat createWithContentUri(@NonNull Uri uri) {
+ public static @NonNull IconCompat createWithContentUri(@NonNull Uri uri) {
ObjectsCompat.requireNonNull(uri);
return createWithContentUri(uri.toString());
}
@@ -336,8 +328,7 @@
* @param uri A uri referring to local content:// or file:// image data.
* @see android.graphics.drawable.Icon#createWithAdaptiveBitmapContentUri(String)
*/
- @NonNull
- public static IconCompat createWithAdaptiveBitmapContentUri(@NonNull String uri) {
+ public static @NonNull IconCompat createWithAdaptiveBitmapContentUri(@NonNull String uri) {
ObjectsCompat.requireNonNull(uri);
final IconCompat rep = new IconCompat(TYPE_URI_ADAPTIVE_BITMAP);
rep.mObj1 = uri;
@@ -351,8 +342,7 @@
* @param uri A uri referring to local content:// or file:// image data.
* @see android.graphics.drawable.Icon#createWithAdaptiveBitmapContentUri(String)
*/
- @NonNull
- public static IconCompat createWithAdaptiveBitmapContentUri(@NonNull Uri uri) {
+ public static @NonNull IconCompat createWithAdaptiveBitmapContentUri(@NonNull Uri uri) {
ObjectsCompat.requireNonNull(uri);
return createWithAdaptiveBitmapContentUri(uri.toString());
}
@@ -389,8 +379,7 @@
* Note: This package may not be available if referenced in the future, and it is
* up to the caller to ensure safety if this package is re-used and/or persisted.
*/
- @NonNull
- public String getResPackage() {
+ public @NonNull String getResPackage() {
if (mType == TYPE_UNKNOWN && Build.VERSION.SDK_INT >= 23) {
return Api23Impl.getResPackage(mObj1);
}
@@ -436,8 +425,7 @@
*
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @Nullable
- public Bitmap getBitmap() {
+ public @Nullable Bitmap getBitmap() {
if (mType == TYPE_UNKNOWN && Build.VERSION.SDK_INT >= 23) {
if (mObj1 instanceof Bitmap) {
return (Bitmap) mObj1;
@@ -460,8 +448,7 @@
* Note: This uri may not be available in the future, and it is
* up to the caller to ensure safety if this uri is re-used and/or persisted.
*/
- @NonNull
- public Uri getUri() {
+ public @NonNull Uri getUri() {
if (mType == TYPE_UNKNOWN && Build.VERSION.SDK_INT >= 23) {
return Api23Impl.getUri(mObj1);
}
@@ -477,8 +464,7 @@
* @param tint a color, as in {@link Drawable#setTint(int)}
* @return this same object, for use in chained construction
*/
- @NonNull
- public IconCompat setTint(@ColorInt int tint) {
+ public @NonNull IconCompat setTint(@ColorInt int tint) {
return setTintList(ColorStateList.valueOf(tint));
}
@@ -488,8 +474,7 @@
* @param tintList as in {@link Drawable#setTintList(ColorStateList)}, null to remove tint
* @return this same object, for use in chained construction
*/
- @NonNull
- public IconCompat setTintList(@Nullable ColorStateList tintList) {
+ public @NonNull IconCompat setTintList(@Nullable ColorStateList tintList) {
mTintList = tintList;
return this;
}
@@ -500,8 +485,7 @@
* @param mode a blending mode, as in {@link Drawable#setTintMode(PorterDuff.Mode)}, may be null
* @return this same object, for use in chained construction
*/
- @NonNull
- public IconCompat setTintMode(@Nullable PorterDuff.Mode mode) {
+ public @NonNull IconCompat setTintMode(PorterDuff.@Nullable Mode mode) {
mTintMode = mode;
return this;
}
@@ -511,8 +495,7 @@
*/
@RequiresApi(23)
@Deprecated
- @NonNull
- public Icon toIcon() {
+ public @NonNull Icon toIcon() {
return toIcon(null);
}
@@ -522,8 +505,7 @@
* @return {@link Icon} object
*/
@RequiresApi(23)
- @NonNull
- public Icon toIcon(@Nullable Context context) {
+ public @NonNull Icon toIcon(@Nullable Context context) {
if (Build.VERSION.SDK_INT >= 23) {
return Api23Impl.toIcon(this, context);
} else {
@@ -570,8 +552,7 @@
* to access {@link android.content.res.Resources Resources}, for example.
* @return A fresh instance of a drawable for this image, yours to keep.
*/
- @Nullable
- public Drawable loadDrawable(@NonNull Context context) {
+ public @Nullable Drawable loadDrawable(@NonNull Context context) {
checkResource(context);
if (Build.VERSION.SDK_INT >= 23) {
return Api23Impl.loadDrawable(toIcon(context), context);
@@ -645,9 +626,8 @@
* Create an input stream for bitmap by resolving corresponding content uri.
*
*/
- @Nullable
@RestrictTo(LIBRARY_GROUP)
- public InputStream getUriInputStream(@NonNull Context context) {
+ public @Nullable InputStream getUriInputStream(@NonNull Context context) {
final Uri uri = getUri();
final String scheme = uri.getScheme();
if (ContentResolver.SCHEME_CONTENT.equals(scheme)
@@ -751,8 +731,7 @@
* Adds this Icon to a Bundle that can be read back with the same parameters
* to {@link #createFromBundle(Bundle)}.
*/
- @NonNull
- public Bundle toBundle() {
+ public @NonNull Bundle toBundle() {
Bundle bundle = new Bundle();
switch (mType) {
case TYPE_BITMAP:
@@ -787,9 +766,8 @@
return bundle;
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
if (mType == TYPE_UNKNOWN) {
return String.valueOf(mObj1);
}
@@ -964,8 +942,8 @@
* Creates an IconCompat from an Icon.
*/
@RequiresApi(23)
- @Nullable
- public static IconCompat createFromIcon(@NonNull Context context, @NonNull Icon icon) {
+ public static @Nullable IconCompat createFromIcon(@NonNull Context context,
+ @NonNull Icon icon) {
Preconditions.checkNotNull(icon);
return Api23Impl.createFromIcon(context, icon);
}
@@ -975,8 +953,7 @@
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@RequiresApi(23)
- @Nullable
- public static IconCompat createFromIcon(@NonNull Icon icon) {
+ public static @Nullable IconCompat createFromIcon(@NonNull Icon icon) {
return Api23Impl.createFromIconInner(icon);
}
@@ -986,8 +963,7 @@
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@RequiresApi(23)
- @Nullable
- public static IconCompat createFromIconOrNullIfZeroResId(@NonNull Icon icon) {
+ public static @Nullable IconCompat createFromIconOrNullIfZeroResId(@NonNull Icon icon) {
if (Api23Impl.getType(icon) == TYPE_RESOURCE && Api23Impl.getResId(icon) == 0) {
return null;
}
@@ -1098,8 +1074,7 @@
// This class is not instantiable.
}
- @Nullable
- static IconCompat createFromIcon(@NonNull Context context, @NonNull Icon icon) {
+ static @Nullable IconCompat createFromIcon(@NonNull Context context, @NonNull Icon icon) {
switch (getType(icon)) {
case TYPE_RESOURCE:
String resPackage = getResPackage(icon);
@@ -1154,8 +1129,7 @@
* up to the caller to ensure safety if this package is re-used and/or persisted.
* Returns {@code null} when the value cannot be gotten.
*/
- @Nullable
- static String getResPackage(@NonNull Object icon) {
+ static @Nullable String getResPackage(@NonNull Object icon) {
if (Build.VERSION.SDK_INT >= 28) {
return Api28Impl.getResPackage(icon);
} else {
@@ -1229,8 +1203,7 @@
* up to the caller to ensure safety if this uri is re-used and/or persisted.
* Returns {@code null} if the uri cannot be gotten.
*/
- @Nullable
- static Uri getUri(@NonNull Object icon) {
+ static @Nullable Uri getUri(@NonNull Object icon) {
if (Build.VERSION.SDK_INT >= 28) {
return Api28Impl.getUri(icon);
} else {
diff --git a/core/core/src/main/java/androidx/core/graphics/drawable/RoundedBitmapDrawable.java b/core/core/src/main/java/androidx/core/graphics/drawable/RoundedBitmapDrawable.java
index 4c9f959..148a7aa 100644
--- a/core/core/src/main/java/androidx/core/graphics/drawable/RoundedBitmapDrawable.java
+++ b/core/core/src/main/java/androidx/core/graphics/drawable/RoundedBitmapDrawable.java
@@ -30,8 +30,8 @@
import android.util.DisplayMetrics;
import android.view.Gravity;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* A Drawable that wraps a bitmap and can be drawn with rounded corners. You can create a
@@ -67,16 +67,14 @@
/**
* Returns the paint used to render this drawable.
*/
- @NonNull
- public final Paint getPaint() {
+ public final @NonNull Paint getPaint() {
return mPaint;
}
/**
* Returns the bitmap used by this drawable to render. May be null.
*/
- @Nullable
- public final Bitmap getBitmap() {
+ public final @Nullable Bitmap getBitmap() {
return mBitmap;
}
diff --git a/core/core/src/main/java/androidx/core/graphics/drawable/RoundedBitmapDrawable21.java b/core/core/src/main/java/androidx/core/graphics/drawable/RoundedBitmapDrawable21.java
index 4a32004..41f179a 100644
--- a/core/core/src/main/java/androidx/core/graphics/drawable/RoundedBitmapDrawable21.java
+++ b/core/core/src/main/java/androidx/core/graphics/drawable/RoundedBitmapDrawable21.java
@@ -23,9 +23,10 @@
import android.view.Gravity;
import android.view.View;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
@RequiresApi(21)
class RoundedBitmapDrawable21 extends RoundedBitmapDrawable {
protected RoundedBitmapDrawable21(Resources res, Bitmap bitmap) {
diff --git a/core/core/src/main/java/androidx/core/graphics/drawable/RoundedBitmapDrawableFactory.java b/core/core/src/main/java/androidx/core/graphics/drawable/RoundedBitmapDrawableFactory.java
index c9c3cd3..4187efd 100644
--- a/core/core/src/main/java/androidx/core/graphics/drawable/RoundedBitmapDrawableFactory.java
+++ b/core/core/src/main/java/androidx/core/graphics/drawable/RoundedBitmapDrawableFactory.java
@@ -24,11 +24,12 @@
import android.util.Log;
import android.view.View;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.graphics.BitmapCompat;
import androidx.core.view.GravityCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.InputStream;
/**
@@ -68,8 +69,8 @@
* Returns a new drawable by creating it from a bitmap, setting initial target density based on
* the display metrics of the resources.
*/
- @NonNull
- public static RoundedBitmapDrawable create(@NonNull Resources res, @Nullable Bitmap bitmap) {
+ public static @NonNull RoundedBitmapDrawable create(@NonNull Resources res,
+ @Nullable Bitmap bitmap) {
if (Build.VERSION.SDK_INT >= 21) {
return new RoundedBitmapDrawable21(res, bitmap);
}
@@ -79,8 +80,8 @@
/**
* Returns a new drawable, creating it by opening a given file path and decoding the bitmap.
*/
- @NonNull
- public static RoundedBitmapDrawable create(@NonNull Resources res, @NonNull String filepath) {
+ public static @NonNull RoundedBitmapDrawable create(@NonNull Resources res,
+ @NonNull String filepath) {
final RoundedBitmapDrawable drawable = create(res, BitmapFactory.decodeFile(filepath));
if (drawable.getBitmap() == null) {
Log.w(TAG, "RoundedBitmapDrawable cannot decode " + filepath);
@@ -92,8 +93,8 @@
/**
* Returns a new drawable, creating it by decoding a bitmap from the given input stream.
*/
- @NonNull
- public static RoundedBitmapDrawable create(@NonNull Resources res, @NonNull InputStream is) {
+ public static @NonNull RoundedBitmapDrawable create(@NonNull Resources res,
+ @NonNull InputStream is) {
final RoundedBitmapDrawable drawable = create(res, BitmapFactory.decodeStream(is));
if (drawable.getBitmap() == null) {
Log.w(TAG, "RoundedBitmapDrawable cannot decode " + is);
diff --git a/core/core/src/main/java/androidx/core/graphics/drawable/WrappedDrawableApi14.java b/core/core/src/main/java/androidx/core/graphics/drawable/WrappedDrawableApi14.java
index e84153e..675df09b 100644
--- a/core/core/src/main/java/androidx/core/graphics/drawable/WrappedDrawableApi14.java
+++ b/core/core/src/main/java/androidx/core/graphics/drawable/WrappedDrawableApi14.java
@@ -25,10 +25,11 @@
import android.graphics.Region;
import android.graphics.drawable.Drawable;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Drawable which delegates all calls to its wrapped {@link Drawable}.
* <p/>
@@ -134,21 +135,19 @@
}
@Override
- public boolean setState(@NonNull int[] stateSet) {
+ public boolean setState(int @NonNull [] stateSet) {
boolean handled = mDrawable.setState(stateSet);
handled = updateTint(stateSet) || handled;
return handled;
}
- @NonNull
@Override
- public int[] getState() {
+ public int @NonNull [] getState() {
return mDrawable.getState();
}
- @NonNull
@Override
- public Drawable getCurrent() {
+ public @NonNull Drawable getCurrent() {
return mDrawable.getCurrent();
}
@@ -215,8 +214,7 @@
}
@Override
- @Nullable
- public ConstantState getConstantState() {
+ public @Nullable ConstantState getConstantState() {
if (mState != null && mState.canConstantState()) {
mState.mChangingConfigurations = getChangingConfigurations();
return mState;
@@ -224,9 +222,8 @@
return null;
}
- @NonNull
@Override
- public Drawable mutate() {
+ public @NonNull Drawable mutate() {
if (!mMutated && super.mutate() == this) {
mState = mutateConstantState();
if (mDrawable != null) {
@@ -248,8 +245,7 @@
*
* @return the new state
*/
- @NonNull
- private WrappedDrawableState mutateConstantState() {
+ private @NonNull WrappedDrawableState mutateConstantState() {
return new WrappedDrawableState(mState);
}
@@ -294,7 +290,7 @@
}
@Override
- public void setTintMode(@NonNull PorterDuff.Mode tintMode) {
+ public void setTintMode(PorterDuff.@NonNull Mode tintMode) {
mState.mTintMode = tintMode;
updateTint(getState());
}
diff --git a/core/core/src/main/java/androidx/core/graphics/drawable/WrappedDrawableApi21.java b/core/core/src/main/java/androidx/core/graphics/drawable/WrappedDrawableApi21.java
index e75bf07..bd6f040 100644
--- a/core/core/src/main/java/androidx/core/graphics/drawable/WrappedDrawableApi21.java
+++ b/core/core/src/main/java/androidx/core/graphics/drawable/WrappedDrawableApi21.java
@@ -29,9 +29,10 @@
import android.os.Build;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
import java.lang.reflect.Method;
@RequiresApi(21)
@@ -64,9 +65,8 @@
mDrawable.getOutline(outline);
}
- @NonNull
@Override
- public Rect getDirtyBounds() {
+ public @NonNull Rect getDirtyBounds() {
return mDrawable.getDirtyBounds();
}
@@ -89,7 +89,7 @@
}
@Override
- public void setTintMode(@NonNull PorterDuff.Mode tintMode) {
+ public void setTintMode(PorterDuff.@NonNull Mode tintMode) {
if (isCompatTintEnabled()) {
super.setTintMode(tintMode);
} else {
@@ -98,7 +98,7 @@
}
@Override
- public boolean setState(@NonNull int[] stateSet) {
+ public boolean setState(int @NonNull [] stateSet) {
if (super.setState(stateSet)) {
// Manually invalidate because the framework doesn't currently force an invalidation
// on a state change
diff --git a/core/core/src/main/java/androidx/core/graphics/drawable/WrappedDrawableState.java b/core/core/src/main/java/androidx/core/graphics/drawable/WrappedDrawableState.java
index d9a5149..be35b25 100644
--- a/core/core/src/main/java/androidx/core/graphics/drawable/WrappedDrawableState.java
+++ b/core/core/src/main/java/androidx/core/graphics/drawable/WrappedDrawableState.java
@@ -22,8 +22,8 @@
import android.graphics.drawable.Drawable;
import android.os.Build;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
final class WrappedDrawableState extends Drawable.ConstantState {
int mChangingConfigurations;
@@ -41,15 +41,13 @@
}
}
- @NonNull
@Override
- public Drawable newDrawable() {
+ public @NonNull Drawable newDrawable() {
return newDrawable(null);
}
- @NonNull
@Override
- public Drawable newDrawable(@Nullable Resources res) {
+ public @NonNull Drawable newDrawable(@Nullable Resources res) {
if (Build.VERSION.SDK_INT >= 21) {
return new WrappedDrawableApi21(this, res);
}
diff --git a/core/core/src/main/java/androidx/core/hardware/display/DisplayManagerCompat.java b/core/core/src/main/java/androidx/core/hardware/display/DisplayManagerCompat.java
index 396cadf..49a4630 100644
--- a/core/core/src/main/java/androidx/core/hardware/display/DisplayManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/hardware/display/DisplayManagerCompat.java
@@ -20,8 +20,8 @@
import android.hardware.display.DisplayManager;
import android.view.Display;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* Helper for accessing features in {@link android.hardware.display.DisplayManager}.
@@ -52,8 +52,7 @@
/**
* Gets an instance of the display manager given the context.
*/
- @NonNull
- public static DisplayManagerCompat getInstance(@NonNull Context context) {
+ public static @NonNull DisplayManagerCompat getInstance(@NonNull Context context) {
return new DisplayManagerCompat(context);
}
@@ -66,9 +65,8 @@
* @param displayId The logical display id.
* @return The display object, or null if there is no valid display with the given id.
*/
- @Nullable
@SuppressWarnings("deprecation")
- public Display getDisplay(int displayId) {
+ public @Nullable Display getDisplay(int displayId) {
DisplayManager displayManager =
(DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE);
return displayManager.getDisplay(displayId);
@@ -80,8 +78,7 @@
* @return An array containing all displays.
*/
@SuppressWarnings("deprecation")
- @NonNull
- public Display[] getDisplays() {
+ public Display @NonNull [] getDisplays() {
return ((DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE)).getDisplays();
}
@@ -101,9 +98,8 @@
*
* @see #DISPLAY_CATEGORY_PRESENTATION
*/
- @NonNull
@SuppressWarnings("deprecation")
- public Display[] getDisplays(@Nullable String category) {
+ public Display @NonNull [] getDisplays(@Nullable String category) {
return ((DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE)).getDisplays();
}
}
diff --git a/core/core/src/main/java/androidx/core/hardware/fingerprint/FingerprintManagerCompat.java b/core/core/src/main/java/androidx/core/hardware/fingerprint/FingerprintManagerCompat.java
index 0244fbc..a18a952 100644
--- a/core/core/src/main/java/androidx/core/hardware/fingerprint/FingerprintManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/hardware/fingerprint/FingerprintManagerCompat.java
@@ -24,12 +24,13 @@
import android.os.CancellationSignal;
import android.os.Handler;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.security.Signature;
import javax.crypto.Cipher;
@@ -51,8 +52,7 @@
private final Context mContext;
/** Get a {@link FingerprintManagerCompat} instance for a provided context. */
- @NonNull
- public static FingerprintManagerCompat from(@NonNull Context context) {
+ public static @NonNull FingerprintManagerCompat from(@NonNull Context context) {
return new FingerprintManagerCompat(context);
}
@@ -110,7 +110,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@RequiresPermission(Manifest.permission.USE_FINGERPRINT)
public void authenticate(@Nullable CryptoObject crypto, int flags,
- @Nullable androidx.core.os.CancellationSignal cancel,
+ androidx.core.os.@Nullable CancellationSignal cancel,
@NonNull AuthenticationCallback callback,
@Nullable Handler handler) {
authenticate(crypto, flags,
@@ -145,9 +145,9 @@
}
}
- @Nullable
@RequiresApi(23)
- private static FingerprintManager getFingerprintManagerOrNull(@NonNull Context context) {
+ private static @Nullable FingerprintManager getFingerprintManagerOrNull(
+ @NonNull Context context) {
return Api23Impl.getFingerprintManagerOrNull(context);
}
@@ -221,22 +221,19 @@
* Get {@link Signature} object.
* @return {@link Signature} object or null if this doesn't contain one.
*/
- @Nullable
- public Signature getSignature() { return mSignature; }
+ public @Nullable Signature getSignature() { return mSignature; }
/**
* Get {@link Cipher} object.
* @return {@link Cipher} object or null if this doesn't contain one.
*/
- @Nullable
- public Cipher getCipher() { return mCipher; }
+ public @Nullable Cipher getCipher() { return mCipher; }
/**
* Get {@link Mac} object.
* @return {@link Mac} object or null if this doesn't contain one.
*/
- @Nullable
- public Mac getMac() { return mMac; }
+ public @Nullable Mac getMac() { return mMac; }
}
/**
@@ -255,8 +252,7 @@
* @return crypto object provided to {@link FingerprintManagerCompat#authenticate(
* CryptoObject, int, CancellationSignal, AuthenticationCallback, Handler)}.
*/
- @NonNull
- public CryptoObject getCryptoObject() { return mCryptoObject; }
+ public @NonNull CryptoObject getCryptoObject() { return mCryptoObject; }
}
/**
diff --git a/core/core/src/main/java/androidx/core/internal/package-info.java b/core/core/src/main/java/androidx/core/internal/package-info.java
index 65a385c..dfc15fe 100644
--- a/core/core/src/main/java/androidx/core/internal/package-info.java
+++ b/core/core/src/main/java/androidx/core/internal/package-info.java
@@ -5,4 +5,4 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
-import androidx.annotation.RestrictTo;
\ No newline at end of file
+import androidx.annotation.RestrictTo;
diff --git a/core/core/src/main/java/androidx/core/internal/view/SupportMenuItem.java b/core/core/src/main/java/androidx/core/internal/view/SupportMenuItem.java
index 54c0454..e04f94f 100644
--- a/core/core/src/main/java/androidx/core/internal/view/SupportMenuItem.java
+++ b/core/core/src/main/java/androidx/core/internal/view/SupportMenuItem.java
@@ -24,11 +24,12 @@
import android.view.MenuItem;
import android.view.View;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.view.ActionProvider;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Interface for direct access to a previously created menu item.
*
@@ -104,9 +105,8 @@
* @see android.app.ActionBar
* @see #setActionView(View)
*/
- @NonNull
@Override
- MenuItem setShowAsActionFlags(int actionEnum);
+ @NonNull MenuItem setShowAsActionFlags(int actionEnum);
/**
* Set an action view for this menu item. An action view will be displayed in place
@@ -120,9 +120,8 @@
* @return This Item so additional setters can be called.
* @see #setShowAsAction(int)
*/
- @NonNull
@Override
- MenuItem setActionView(@Nullable View view);
+ @NonNull MenuItem setActionView(@Nullable View view);
/**
* Set an action view for this menu item. An action view will be displayed in place
@@ -136,9 +135,8 @@
* @return This Item so additional setters can be called.
* @see #setShowAsAction(int)
*/
- @NonNull
@Override
- MenuItem setActionView(int resId);
+ @NonNull MenuItem setActionView(int resId);
/**
* Returns the currently set action view for this menu item.
@@ -147,9 +145,8 @@
* @see #setActionView(View)
* @see #setShowAsAction(int)
*/
- @Nullable
@Override
- View getActionView();
+ @Nullable View getActionView();
/**
* Sets the {@link ActionProvider} responsible for creating an action view if
@@ -164,8 +161,7 @@
* @return This Item so additional setters can be called.
* @see ActionProvider
*/
- @NonNull
- SupportMenuItem setSupportActionProvider(@Nullable ActionProvider actionProvider);
+ @NonNull SupportMenuItem setSupportActionProvider(@Nullable ActionProvider actionProvider);
/**
* Gets the {@link ActionProvider}.
@@ -174,8 +170,7 @@
* @see ActionProvider
* @see #setSupportActionProvider(ActionProvider)
*/
- @Nullable
- ActionProvider getSupportActionProvider();
+ @Nullable ActionProvider getSupportActionProvider();
/**
* Expand the action view associated with this menu item. The menu item must have an action view
@@ -223,18 +218,16 @@
* @param contentDescription The new content description.
* @return This menu item instance for call chaining.
*/
- @NonNull
@Override
- SupportMenuItem setContentDescription(@Nullable CharSequence contentDescription);
+ @NonNull SupportMenuItem setContentDescription(@Nullable CharSequence contentDescription);
/**
* Retrieve the content description associated with this menu item.
*
* @return The content description.
*/
- @Nullable
@Override
- CharSequence getContentDescription();
+ @Nullable CharSequence getContentDescription();
/**
* Change the tooltip text associated with this menu item.
@@ -242,18 +235,16 @@
* @param tooltipText The new tooltip text.
* @return This menu item instance for call chaining.
*/
- @NonNull
@Override
- SupportMenuItem setTooltipText(@Nullable CharSequence tooltipText);
+ @NonNull SupportMenuItem setTooltipText(@Nullable CharSequence tooltipText);
/**
* Retrieve the tooltip text associated with this menu item.
*
* @return The tooltip text.
*/
- @Nullable
@Override
- CharSequence getTooltipText();
+ @Nullable CharSequence getTooltipText();
/**
* Change both the numeric and alphabetic shortcut associated with this
@@ -278,9 +269,8 @@
* {@link KeyEvent#META_SYM_ON}, {@link KeyEvent#META_FUNCTION_ON}.
* @return This Item so additional setters can be called.
*/
- @NonNull
@Override
- MenuItem setShortcut(char numericChar, char alphaChar, int numericModifiers,
+ @NonNull MenuItem setShortcut(char numericChar, char alphaChar, int numericModifiers,
int alphaModifiers);
/**
@@ -296,9 +286,8 @@
* {@link KeyEvent#META_SYM_ON}, {@link KeyEvent#META_FUNCTION_ON}.
* @return This Item so additional setters can be called.
*/
- @NonNull
@Override
- MenuItem setNumericShortcut(char numericChar, int numericModifiers);
+ @NonNull MenuItem setNumericShortcut(char numericChar, int numericModifiers);
/**
* Return the modifiers for this menu item's numeric (12-key) shortcut.
@@ -331,9 +320,8 @@
* {@link KeyEvent#META_SYM_ON}, {@link KeyEvent#META_FUNCTION_ON}.
* @return This Item so additional setters can be called.
*/
- @NonNull
@Override
- MenuItem setAlphabeticShortcut(char alphaChar, int alphaModifiers);
+ @NonNull MenuItem setAlphabeticShortcut(char alphaChar, int alphaModifiers);
/**
* Return the modifier for this menu item's alphabetic shortcut.
@@ -360,17 +348,15 @@
*
* @see #getIconTintList()
*/
- @NonNull
@Override
- MenuItem setIconTintList(@Nullable ColorStateList tint);
+ @NonNull MenuItem setIconTintList(@Nullable ColorStateList tint);
/**
* @return the tint applied to this item's icon
* @see #setIconTintList(ColorStateList)
*/
- @Nullable
@Override
- ColorStateList getIconTintList();
+ @Nullable ColorStateList getIconTintList();
/**
* Specifies the blending mode used to apply the tint specified by
@@ -381,9 +367,8 @@
* {@code null} to clear tint
* @see #setIconTintList(ColorStateList)
*/
- @NonNull
@Override
- MenuItem setIconTintMode(@Nullable PorterDuff.Mode tintMode);
+ @NonNull MenuItem setIconTintMode(PorterDuff.@Nullable Mode tintMode);
/**
* Returns the blending mode used to apply the tint to this item's icon, if specified.
@@ -391,9 +376,8 @@
* @return the blending mode used to apply the tint to this item's icon
* @see #setIconTintMode(PorterDuff.Mode)
*/
- @Nullable
@Override
- PorterDuff.Mode getIconTintMode();
+ PorterDuff.@Nullable Mode getIconTintMode();
/**
* Returns true if {@link #setShowAsAction(int)} was set to {@link #SHOW_AS_ACTION_ALWAYS}.
diff --git a/core/core/src/main/java/androidx/core/location/GnssStatusCompat.java b/core/core/src/main/java/androidx/core/location/GnssStatusCompat.java
index a547b44..85dfca6 100644
--- a/core/core/src/main/java/androidx/core/location/GnssStatusCompat.java
+++ b/core/core/src/main/java/androidx/core/location/GnssStatusCompat.java
@@ -28,10 +28,11 @@
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -111,8 +112,7 @@
* Wraps the given {@link GnssStatus} as GnssStatusCompat.
*/
@RequiresApi(VERSION_CODES.N)
- @NonNull
- public static GnssStatusCompat wrap(@NonNull GnssStatus gnssStatus) {
+ public static @NonNull GnssStatusCompat wrap(@NonNull GnssStatus gnssStatus) {
return new GnssStatusWrapper(gnssStatus);
}
@@ -120,8 +120,7 @@
* Wraps the given {@link GpsStatus} as GnssStatusCompat.
*/
@SuppressLint("ReferencesDeprecated")
- @NonNull
- public static GnssStatusCompat wrap(@NonNull GpsStatus gpsStatus) {
+ public static @NonNull GnssStatusCompat wrap(@NonNull GpsStatus gpsStatus) {
return new GpsStatusWrapper(gpsStatus);
}
diff --git a/core/core/src/main/java/androidx/core/location/LocationCompat.java b/core/core/src/main/java/androidx/core/location/LocationCompat.java
index 49c4c79..8c4d3c2 100644
--- a/core/core/src/main/java/androidx/core/location/LocationCompat.java
+++ b/core/core/src/main/java/androidx/core/location/LocationCompat.java
@@ -24,10 +24,11 @@
import android.os.Bundle;
import androidx.annotation.FloatRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -76,16 +77,11 @@
public static final String EXTRA_MSL_ALTITUDE_ACCURACY =
"androidx.core.location.extra.MSL_ALTITUDE_ACCURACY";
- @Nullable
- private static Method sSetIsFromMockProviderMethod;
- @Nullable
- private static Field sFieldsMaskField;
- @Nullable
- private static Integer sHasSpeedAccuracyMask;
- @Nullable
- private static Integer sHasBearingAccuracyMask;
- @Nullable
- private static Integer sHasVerticalAccuracyMask;
+ private static @Nullable Method sSetIsFromMockProviderMethod;
+ private static @Nullable Field sFieldsMaskField;
+ private static @Nullable Integer sHasSpeedAccuracyMask;
+ private static @Nullable Integer sHasBearingAccuracyMask;
+ private static @Nullable Integer sHasVerticalAccuracyMask;
private LocationCompat() {
}
diff --git a/core/core/src/main/java/androidx/core/location/LocationListenerCompat.java b/core/core/src/main/java/androidx/core/location/LocationListenerCompat.java
index 070815b..3c5ef5e 100644
--- a/core/core/src/main/java/androidx/core/location/LocationListenerCompat.java
+++ b/core/core/src/main/java/androidx/core/location/LocationListenerCompat.java
@@ -20,8 +20,8 @@
import android.location.LocationListener;
import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import java.util.List;
diff --git a/core/core/src/main/java/androidx/core/location/LocationManagerCompat.java b/core/core/src/main/java/androidx/core/location/LocationManagerCompat.java
index f735336..44e85f9 100644
--- a/core/core/src/main/java/androidx/core/location/LocationManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/location/LocationManagerCompat.java
@@ -47,8 +47,6 @@
import android.text.TextUtils;
import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission;
import androidx.collection.SimpleArrayMap;
@@ -57,6 +55,9 @@
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
@@ -186,8 +187,8 @@
@RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
public static void getCurrentLocation(@NonNull LocationManager locationManager,
@NonNull String provider,
- @Nullable androidx.core.os.CancellationSignal cancellationSignal,
- @NonNull Executor executor, @NonNull final Consumer<Location> consumer) {
+ androidx.core.os.@Nullable CancellationSignal cancellationSignal,
+ @NonNull Executor executor, final @NonNull Consumer<Location> consumer) {
getCurrentLocation(locationManager, provider, cancellationSignal != null
? (CancellationSignal) cancellationSignal.getCancellationSignalObject() :
null,
@@ -215,7 +216,7 @@
@RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
public static void getCurrentLocation(@NonNull LocationManager locationManager,
@NonNull String provider, @Nullable CancellationSignal cancellationSignal,
- @NonNull Executor executor, @NonNull final Consumer<Location> consumer) {
+ @NonNull Executor executor, final @NonNull Consumer<Location> consumer) {
if (VERSION.SDK_INT >= 30) {
Api30Impl.getCurrentLocation(locationManager, provider, cancellationSignal, executor,
consumer);
@@ -380,8 +381,8 @@
*
* <p>No device-specific serial number or ID is returned from this API.
*/
- @Nullable
- public static String getGnssHardwareModelName(@NonNull LocationManager locationManager) {
+ public static @Nullable String getGnssHardwareModelName(
+ @NonNull LocationManager locationManager) {
if (VERSION.SDK_INT >= 28) {
return Api28Impl.getGnssHardwareModelName(locationManager);
} else {
@@ -423,7 +424,7 @@
@RequiresApi(VERSION_CODES.N)
@RequiresPermission(ACCESS_FINE_LOCATION)
public static boolean registerGnssMeasurementsCallback(@NonNull LocationManager locationManager,
- @NonNull GnssMeasurementsEvent.Callback callback, @NonNull Handler handler) {
+ GnssMeasurementsEvent.@NonNull Callback callback, @NonNull Handler handler) {
if (VERSION.SDK_INT > VERSION_CODES.R) {
return Api24Impl.registerGnssMeasurementsCallback(locationManager, callback, handler);
} else if (VERSION.SDK_INT == VERSION_CODES.R) {
@@ -457,7 +458,7 @@
@RequiresApi(VERSION_CODES.N)
@RequiresPermission(ACCESS_FINE_LOCATION)
public static boolean registerGnssMeasurementsCallback(@NonNull LocationManager locationManager,
- @NonNull Executor executor, @NonNull GnssMeasurementsEvent.Callback callback) {
+ @NonNull Executor executor, GnssMeasurementsEvent.@NonNull Callback callback) {
if (VERSION.SDK_INT > VERSION_CODES.R) {
return Api31Impl.registerGnssMeasurementsCallback(locationManager, executor, callback);
} else if (VERSION.SDK_INT == VERSION_CODES.R) {
@@ -485,7 +486,7 @@
*/
@RequiresApi(VERSION_CODES.N)
public static void unregisterGnssMeasurementsCallback(@NonNull LocationManager locationManager,
- @NonNull GnssMeasurementsEvent.Callback callback) {
+ GnssMeasurementsEvent.@NonNull Callback callback) {
if (VERSION.SDK_INT >= VERSION_CODES.R) {
Api24Impl.unregisterGnssMeasurementsCallback(locationManager, callback);
} else {
@@ -507,7 +508,7 @@
@RequiresApi(VERSION_CODES.R)
private static boolean registerGnssMeasurementsCallbackOnR(
@NonNull LocationManager locationManager, @NonNull Executor executor,
- @NonNull GnssMeasurementsEvent.Callback callback) {
+ GnssMeasurementsEvent.@NonNull Callback callback) {
if (VERSION.SDK_INT == VERSION_CODES.R) {
try {
if (sGnssRequestBuilderClass == null) {
@@ -551,7 +552,7 @@
*/
@RequiresPermission(ACCESS_FINE_LOCATION)
public static boolean registerGnssStatusCallback(@NonNull LocationManager locationManager,
- @NonNull GnssStatusCompat.Callback callback, @NonNull Handler handler) {
+ GnssStatusCompat.@NonNull Callback callback, @NonNull Handler handler) {
if (VERSION.SDK_INT >= VERSION_CODES.R) {
return registerGnssStatusCallback(locationManager, ExecutorCompat.create(handler),
callback);
@@ -581,7 +582,7 @@
*/
@RequiresPermission(ACCESS_FINE_LOCATION)
public static boolean registerGnssStatusCallback(@NonNull LocationManager locationManager,
- @NonNull Executor executor, @NonNull GnssStatusCompat.Callback callback) {
+ @NonNull Executor executor, GnssStatusCompat.@NonNull Callback callback) {
if (VERSION.SDK_INT >= VERSION_CODES.R) {
return registerGnssStatusCallback(locationManager, null, executor, callback);
} else {
@@ -670,7 +671,7 @@
* and {@link LocationManager#unregisterGnssStatusCallback(GnssStatus.Callback)}.
*/
public static void unregisterGnssStatusCallback(@NonNull LocationManager locationManager,
- @NonNull GnssStatusCompat.Callback callback) {
+ GnssStatusCompat.@NonNull Callback callback) {
if (VERSION.SDK_INT >= 24) {
synchronized (GnssListenersHolder.sGnssStatusListeners) {
Object transport = GnssListenersHolder.sGnssStatusListeners.remove(callback);
@@ -721,7 +722,7 @@
private static class LocationListenerTransport implements LocationListener {
- @Nullable volatile LocationListenerKey mKey;
+ volatile @Nullable LocationListenerKey mKey;
final Executor mExecutor;
LocationListenerTransport(@Nullable LocationListenerKey key, Executor executor) {
@@ -832,9 +833,9 @@
private static class GnssMeasurementsTransport extends GnssMeasurementsEvent.Callback {
final GnssMeasurementsEvent.Callback mCallback;
- @Nullable volatile Executor mExecutor;
+ volatile @Nullable Executor mExecutor;
- GnssMeasurementsTransport(@NonNull GnssMeasurementsEvent.Callback callback,
+ GnssMeasurementsTransport(GnssMeasurementsEvent.@NonNull Callback callback,
@NonNull Executor executor) {
mCallback = callback;
mExecutor = executor;
@@ -911,7 +912,7 @@
final GnssStatusCompat.Callback mCallback;
- @Nullable volatile Executor mExecutor;
+ volatile @Nullable Executor mExecutor;
PreRGnssStatusTransport(GnssStatusCompat.Callback callback) {
Preconditions.checkArgument(callback != null, "invalid null callback");
@@ -994,7 +995,7 @@
private final LocationManager mLocationManager;
final GnssStatusCompat.Callback mCallback;
- @Nullable volatile Executor mExecutor;
+ volatile @Nullable Executor mExecutor;
GpsStatusTransport(LocationManager locationManager,
GnssStatusCompat.Callback callback) {
@@ -1078,8 +1079,7 @@
@GuardedBy("this")
private boolean mTriggered;
- @Nullable
- Runnable mTimeoutRunnable;
+ @Nullable Runnable mTimeoutRunnable;
CancellableLocationListener(LocationManager locationManager,
Executor executor, Consumer<Location> consumer) {
@@ -1134,7 +1134,7 @@
@RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
@Override
- public void onLocationChanged(@Nullable final Location location) {
+ public void onLocationChanged(final @Nullable Location location) {
synchronized (this) {
if (mTriggered) {
return;
@@ -1207,7 +1207,7 @@
@RequiresPermission(ACCESS_FINE_LOCATION)
static boolean registerGnssMeasurementsCallback(@NonNull LocationManager locationManager,
- @NonNull Executor executor, @NonNull GnssMeasurementsEvent.Callback callback) {
+ @NonNull Executor executor, GnssMeasurementsEvent.@NonNull Callback callback) {
return locationManager.registerGnssMeasurementsCallback(executor, callback);
}
}
@@ -1398,18 +1398,18 @@
@RequiresPermission(ACCESS_FINE_LOCATION)
static boolean registerGnssMeasurementsCallback(@NonNull LocationManager locationManager,
- @NonNull GnssMeasurementsEvent.Callback callback) {
+ GnssMeasurementsEvent.@NonNull Callback callback) {
return locationManager.registerGnssMeasurementsCallback(callback);
}
@RequiresPermission(ACCESS_FINE_LOCATION)
static boolean registerGnssMeasurementsCallback(@NonNull LocationManager locationManager,
- @NonNull GnssMeasurementsEvent.Callback callback, @NonNull Handler handler) {
+ GnssMeasurementsEvent.@NonNull Callback callback, @NonNull Handler handler) {
return locationManager.registerGnssMeasurementsCallback(callback, handler);
}
static void unregisterGnssMeasurementsCallback(@NonNull LocationManager locationManager,
- @NonNull GnssMeasurementsEvent.Callback callback) {
+ GnssMeasurementsEvent.@NonNull Callback callback) {
locationManager.unregisterGnssMeasurementsCallback(callback);
}
diff --git a/core/core/src/main/java/androidx/core/location/LocationRequestCompat.java b/core/core/src/main/java/androidx/core/location/LocationRequestCompat.java
index 2e0872b..f7f00c8 100644
--- a/core/core/src/main/java/androidx/core/location/LocationRequestCompat.java
+++ b/core/core/src/main/java/androidx/core/location/LocationRequestCompat.java
@@ -27,13 +27,14 @@
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.util.Preconditions;
import androidx.core.util.TimeUtils;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.InvocationTargetException;
@@ -211,8 +212,7 @@
* @see LocationRequest
*/
@RequiresApi(31)
- @NonNull
- public LocationRequest toLocationRequest() {
+ public @NonNull LocationRequest toLocationRequest() {
return Api31Impl.toLocationRequest(this);
}
@@ -227,8 +227,7 @@
* @see LocationRequest
*/
@SuppressLint("NewApi")
- @Nullable
- public LocationRequest toLocationRequest(@NonNull String provider) {
+ public @Nullable LocationRequest toLocationRequest(@NonNull String provider) {
if (VERSION.SDK_INT >= 31) {
return toLocationRequest();
} else {
@@ -263,8 +262,7 @@
}
@Override
- @NonNull
- public String toString() {
+ public @NonNull String toString() {
StringBuilder s = new StringBuilder();
s.append("Request[");
if (mIntervalMillis != PASSIVE_INTERVAL) {
diff --git a/core/core/src/main/java/androidx/core/net/ConnectivityManagerCompat.java b/core/core/src/main/java/androidx/core/net/ConnectivityManagerCompat.java
index b1eba5e..c23c303 100644
--- a/core/core/src/main/java/androidx/core/net/ConnectivityManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/net/ConnectivityManagerCompat.java
@@ -26,12 +26,13 @@
import android.os.Build;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -104,9 +105,8 @@
*/
@SuppressWarnings("deprecation")
@SuppressLint("ReferencesDeprecated")
- @Nullable
@RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
- public static NetworkInfo getNetworkInfoFromBroadcast(@NonNull ConnectivityManager cm,
+ public static @Nullable NetworkInfo getNetworkInfoFromBroadcast(@NonNull ConnectivityManager cm,
@NonNull Intent intent) {
final NetworkInfo info = intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
if (info != null) {
diff --git a/core/core/src/main/java/androidx/core/net/MailTo.java b/core/core/src/main/java/androidx/core/net/MailTo.java
index 797fbdd..8bd9f3f 100644
--- a/core/core/src/main/java/androidx/core/net/MailTo.java
+++ b/core/core/src/main/java/androidx/core/net/MailTo.java
@@ -18,10 +18,11 @@
import android.net.Uri;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@@ -102,8 +103,7 @@
* @return MailTo object
* @exception ParseException if the scheme is not a mailto URI
*/
- @NonNull
- public static MailTo parse(@NonNull String uri) throws ParseException {
+ public static @NonNull MailTo parse(@NonNull String uri) throws ParseException {
Preconditions.checkNotNull(uri);
if (!isMailTo(uri)) {
@@ -173,8 +173,7 @@
* @return MailTo object
* @exception ParseException if the scheme is not a mailto URI
*/
- @NonNull
- public static MailTo parse(@NonNull Uri uri) throws ParseException {
+ public static @NonNull MailTo parse(@NonNull Uri uri) throws ParseException {
return parse(uri.toString());
}
@@ -184,8 +183,7 @@
* If no To line was specified, then null is return
* @return comma delimited email addresses or null
*/
- @Nullable
- public String getTo() {
+ public @Nullable String getTo() {
return mHeaders.get(TO);
}
@@ -195,8 +193,7 @@
* If no CC line was specified, then null is return
* @return comma delimited email addresses or null
*/
- @Nullable
- public String getCc() {
+ public @Nullable String getCc() {
return mHeaders.get(CC);
}
@@ -206,8 +203,7 @@
* If no BCC line was specified, then null is return
* @return comma delimited email addresses or null
*/
- @Nullable
- public String getBcc() {
+ public @Nullable String getBcc() {
return mHeaders.get(BCC);
}
@@ -216,8 +212,7 @@
* If no subject line was specified, then null is return
* @return subject or null
*/
- @Nullable
- public String getSubject() {
+ public @Nullable String getSubject() {
return mHeaders.get(SUBJECT);
}
@@ -226,8 +221,7 @@
* If no body line was specified, then null is return
* @return body or null
*/
- @Nullable
- public String getBody() {
+ public @Nullable String getBody() {
return mHeaders.get(BODY);
}
@@ -235,14 +229,12 @@
* Retrieve all the parsed email headers from the mailto URI
* @return map containing all parsed values
*/
- @Nullable
- public Map<String, String> getHeaders() {
+ public @Nullable Map<String, String> getHeaders() {
return mHeaders;
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
StringBuilder sb = new StringBuilder(MAILTO_SCHEME);
sb.append('?');
for (Map.Entry<String, String> header : mHeaders.entrySet()) {
diff --git a/core/core/src/main/java/androidx/core/net/ParseException.java b/core/core/src/main/java/androidx/core/net/ParseException.java
index 796733e..10ec5a2 100644
--- a/core/core/src/main/java/androidx/core/net/ParseException.java
+++ b/core/core/src/main/java/androidx/core/net/ParseException.java
@@ -16,14 +16,13 @@
package androidx.core.net;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Thrown when parsing a mailto URI has failed.
*/
public class ParseException extends RuntimeException {
- @NonNull
- public final String response;
+ public final @NonNull String response;
ParseException(@NonNull String response) {
super(response);
diff --git a/core/core/src/main/java/androidx/core/net/TrafficStatsCompat.java b/core/core/src/main/java/androidx/core/net/TrafficStatsCompat.java
index ee3f6b7..35dfab0 100644
--- a/core/core/src/main/java/androidx/core/net/TrafficStatsCompat.java
+++ b/core/core/src/main/java/androidx/core/net/TrafficStatsCompat.java
@@ -20,9 +20,10 @@
import android.os.Build;
import android.os.ParcelFileDescriptor;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
import java.net.DatagramSocket;
import java.net.Socket;
import java.net.SocketException;
diff --git a/core/core/src/main/java/androidx/core/net/UriCompat.java b/core/core/src/main/java/androidx/core/net/UriCompat.java
index 77834b7..825cfd2 100644
--- a/core/core/src/main/java/androidx/core/net/UriCompat.java
+++ b/core/core/src/main/java/androidx/core/net/UriCompat.java
@@ -18,7 +18,7 @@
import android.net.Uri;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Helper for accessing function in {@link Uri} in a backwards compatible fashion.
diff --git a/core/core/src/main/java/androidx/core/os/BundleCompat.java b/core/core/src/main/java/androidx/core/os/BundleCompat.java
index 4e47124..ec80c10 100644
--- a/core/core/src/main/java/androidx/core/os/BundleCompat.java
+++ b/core/core/src/main/java/androidx/core/os/BundleCompat.java
@@ -23,10 +23,11 @@
import android.os.Parcelable;
import android.util.SparseArray;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.Serializable;
import java.util.ArrayList;
@@ -62,9 +63,8 @@
* @param clazz The type of the object expected
* @return a Parcelable value, or {@code null}
*/
- @Nullable
@SuppressWarnings({"deprecation", "unchecked"})
- public static <T> T getParcelable(@NonNull Bundle in, @Nullable String key,
+ public static <T> @Nullable T getParcelable(@NonNull Bundle in, @Nullable String key,
@NonNull Class<T> clazz) {
// Even though API was introduced in 33, we use 34 as 33 is bugged in some scenarios.
if (Build.VERSION.SDK_INT >= 34) {
@@ -98,11 +98,10 @@
* @param clazz The type of the items inside the array. This is only verified when unparceling.
* @return a Parcelable[] value, or {@code null}
*/
- @Nullable
@SuppressWarnings({"deprecation"})
@SuppressLint({"ArrayReturn", "NullableCollection"})
- public static Parcelable[] getParcelableArray(@NonNull Bundle in, @Nullable String key,
- @NonNull Class<? extends Parcelable> clazz) {
+ public static Parcelable @Nullable [] getParcelableArray(@NonNull Bundle in,
+ @Nullable String key, @NonNull Class<? extends Parcelable> clazz) {
// Even though API was introduced in 33, we use 34 as 33 is bugged in some scenarios.
if (Build.VERSION.SDK_INT >= 34) {
return Api33Impl.getParcelableArray(in, key, clazz);
@@ -135,11 +134,10 @@
* unparceling.
* @return an ArrayList<T> value, or {@code null}
*/
- @Nullable
@SuppressWarnings({"deprecation", "unchecked"})
@SuppressLint({"ConcreteCollection", "NullableCollection"})
- public static <T> ArrayList<T> getParcelableArrayList(@NonNull Bundle in, @Nullable String key,
- @NonNull Class<? extends T> clazz) {
+ public static <T> @Nullable ArrayList<T> getParcelableArrayList(@NonNull Bundle in,
+ @Nullable String key, @NonNull Class<? extends T> clazz) {
// Even though API was introduced in 33, we use 34 as 33 is bugged in some scenarios.
if (Build.VERSION.SDK_INT >= 34) {
return Api33Impl.getParcelableArrayList(in, key, clazz);
@@ -169,8 +167,7 @@
* @return a SparseArray of T values, or null
*/
@SuppressWarnings({"deprecation", "unchecked"})
- @Nullable
- public static <T> SparseArray<T> getSparseParcelableArray(@NonNull Bundle in,
+ public static <T> @Nullable SparseArray<T> getSparseParcelableArray(@NonNull Bundle in,
@Nullable String key, @NonNull Class<? extends T> clazz) {
if (Build.VERSION.SDK_INT >= 34) {
return Api33Impl.getSparseParcelableArray(in, key, clazz);
@@ -191,8 +188,7 @@
*/
@androidx.annotation.ReplaceWith(expression = "bundle.getBinder(key)")
@Deprecated
- @Nullable
- public static IBinder getBinder(@NonNull Bundle bundle, @Nullable String key) {
+ public static @Nullable IBinder getBinder(@NonNull Bundle bundle, @Nullable String key) {
return bundle.getBinder(key);
}
@@ -233,8 +229,7 @@
* @return a Serializable value, or {@code null}
*/
@SuppressWarnings({"deprecation", "unchecked"})
- @Nullable
- public static <T extends Serializable> T getSerializable(@NonNull Bundle in,
+ public static <T extends Serializable> @Nullable T getSerializable(@NonNull Bundle in,
@Nullable String key, @NonNull Class<T> clazz) {
if (Build.VERSION.SDK_INT >= 34) {
return Api33Impl.getSerializable(in, key, clazz);
diff --git a/core/core/src/main/java/androidx/core/os/CancellationSignal.java b/core/core/src/main/java/androidx/core/os/CancellationSignal.java
index 85486f8..ac20827 100644
--- a/core/core/src/main/java/androidx/core/os/CancellationSignal.java
+++ b/core/core/src/main/java/androidx/core/os/CancellationSignal.java
@@ -18,7 +18,7 @@
import android.os.Build;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Static library support version of the framework's {@link android.os.CancellationSignal}.
@@ -138,8 +138,7 @@
* @return A framework cancellation signal object, or null on platform versions
* prior to Jellybean.
*/
- @Nullable
- public Object getCancellationSignalObject() {
+ public @Nullable Object getCancellationSignalObject() {
synchronized (this) {
if (mCancellationSignalObj == null) {
mCancellationSignalObj = new android.os.CancellationSignal();
diff --git a/core/core/src/main/java/androidx/core/os/ConfigurationCompat.java b/core/core/src/main/java/androidx/core/os/ConfigurationCompat.java
index a1360cc..082060c 100644
--- a/core/core/src/main/java/androidx/core/os/ConfigurationCompat.java
+++ b/core/core/src/main/java/androidx/core/os/ConfigurationCompat.java
@@ -21,9 +21,10 @@
import android.content.res.Configuration;
import android.os.LocaleList;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
import java.util.Locale;
/**
@@ -40,8 +41,7 @@
* @return The locale list.
*/
@SuppressWarnings("deprecation")
- @NonNull
- public static LocaleListCompat getLocales(@NonNull Configuration configuration) {
+ public static @NonNull LocaleListCompat getLocales(@NonNull Configuration configuration) {
if (SDK_INT >= 24) {
return LocaleListCompat.wrap(Api24Impl.getLocales(configuration));
} else {
diff --git a/core/core/src/main/java/androidx/core/os/EnvironmentCompat.java b/core/core/src/main/java/androidx/core/os/EnvironmentCompat.java
index e49fe27..6258903 100644
--- a/core/core/src/main/java/androidx/core/os/EnvironmentCompat.java
+++ b/core/core/src/main/java/androidx/core/os/EnvironmentCompat.java
@@ -19,9 +19,10 @@
import android.os.Build;
import android.os.Environment;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
import java.io.File;
/**
@@ -55,8 +56,7 @@
* {@link Environment#MEDIA_UNMOUNTABLE}.
*/
@SuppressWarnings("deprecation")
- @NonNull
- public static String getStorageState(@NonNull File path) {
+ public static @NonNull String getStorageState(@NonNull File path) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.getExternalStorageState(path);
} else {
diff --git a/core/core/src/main/java/androidx/core/os/ExecutorCompat.java b/core/core/src/main/java/androidx/core/os/ExecutorCompat.java
index bbe6047..354f922 100644
--- a/core/core/src/main/java/androidx/core/os/ExecutorCompat.java
+++ b/core/core/src/main/java/androidx/core/os/ExecutorCompat.java
@@ -18,9 +18,10 @@
import android.os.Handler;
-import androidx.annotation.NonNull;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
diff --git a/core/core/src/main/java/androidx/core/os/HandlerCompat.java b/core/core/src/main/java/androidx/core/os/HandlerCompat.java
index 1f5917d..74fda00 100644
--- a/core/core/src/main/java/androidx/core/os/HandlerCompat.java
+++ b/core/core/src/main/java/androidx/core/os/HandlerCompat.java
@@ -22,10 +22,11 @@
import android.os.Message;
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -57,8 +58,7 @@
* @see Handler#createAsync(Looper)
*/
@SuppressWarnings("JavaReflectionMemberAccess")
- @NonNull
- public static Handler createAsync(@NonNull Looper looper) {
+ public static @NonNull Handler createAsync(@NonNull Looper looper) {
Exception wrappedException;
if (Build.VERSION.SDK_INT >= 28) {
@@ -116,8 +116,8 @@
* @see Handler#createAsync(Looper, Handler.Callback)
*/
@SuppressWarnings("JavaReflectionMemberAccess")
- @NonNull
- public static Handler createAsync(@NonNull Looper looper, @NonNull Handler.Callback callback) {
+ public static @NonNull Handler createAsync(@NonNull Looper looper,
+ Handler.@NonNull Callback callback) {
Exception wrappedException;
if (Build.VERSION.SDK_INT >= 28) {
diff --git a/core/core/src/main/java/androidx/core/os/LocaleListCompat.java b/core/core/src/main/java/androidx/core/os/LocaleListCompat.java
index b24537f..9e758786 100644
--- a/core/core/src/main/java/androidx/core/os/LocaleListCompat.java
+++ b/core/core/src/main/java/androidx/core/os/LocaleListCompat.java
@@ -20,12 +20,13 @@
import android.os.LocaleList;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.Size;
import androidx.core.text.ICUCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.Locale;
/**
@@ -51,8 +52,7 @@
* Creates a new instance of {@link LocaleListCompat} from the Locale list.
*/
@RequiresApi(24)
- @NonNull
- public static LocaleListCompat wrap(@NonNull LocaleList localeList) {
+ public static @NonNull LocaleListCompat wrap(@NonNull LocaleList localeList) {
return new LocaleListCompat(new LocaleListPlatformWrapper(localeList));
}
@@ -61,16 +61,14 @@
*
* @return an android.os.LocaleList object if API >= 24 , or {@code null} if not.
*/
- @Nullable
- public Object unwrap() {
+ public @Nullable Object unwrap() {
return mImpl.getLocaleList();
}
/**
* Creates a new instance of {@link LocaleListCompat} from the {@link Locale} array.
*/
- @NonNull
- public static LocaleListCompat create(@NonNull Locale... localeList) {
+ public static @NonNull LocaleListCompat create(Locale @NonNull ... localeList) {
if (Build.VERSION.SDK_INT >= 24) {
return wrap(Api24Impl.createLocaleList(localeList));
}
@@ -83,8 +81,7 @@
* @param index The position to retrieve.
* @return The {@link Locale} in the given index
*/
- @Nullable
- public Locale get(int index) {
+ public @Nullable Locale get(int index) {
return mImpl.get(index);
}
@@ -122,8 +119,7 @@
/**
* Retrieves a String representation of the language tags in this list.
*/
- @NonNull
- public String toLanguageTags() {
+ public @NonNull String toLanguageTags() {
return mImpl.toLanguageTags();
}
@@ -134,16 +130,14 @@
* @return The first {@link Locale} from this list that appears in the given array, or
* {@code null} if the {@link LocaleListCompat} is empty.
*/
- @Nullable
- public Locale getFirstMatch(@NonNull String[] supportedLocales) {
+ public @Nullable Locale getFirstMatch(String @NonNull [] supportedLocales) {
return mImpl.getFirstMatch(supportedLocales);
}
/**
* Retrieve an empty instance of {@link LocaleListCompat}.
*/
- @NonNull
- public static LocaleListCompat getEmptyLocaleList() {
+ public static @NonNull LocaleListCompat getEmptyLocaleList() {
return sEmptyLocaleList;
}
@@ -155,8 +149,7 @@
* @param list The language tags to be included as a single {@link String} separated by commas.
* @return A new instance with the {@link Locale} items identified by the given tags.
*/
- @NonNull
- public static LocaleListCompat forLanguageTags(@Nullable String list) {
+ public static @NonNull LocaleListCompat forLanguageTags(@Nullable String list) {
if (list == null || list.isEmpty()) {
return getEmptyLocaleList();
} else {
@@ -202,8 +195,8 @@
* Returns the default locale list, adjusted by moving the default locale to its first
* position.
*/
- @NonNull @Size(min = 1)
- public static LocaleListCompat getAdjustedDefault() {
+ @Size(min = 1)
+ public static @NonNull LocaleListCompat getAdjustedDefault() {
if (Build.VERSION.SDK_INT >= 24) {
return LocaleListCompat.wrap(Api24Impl.getAdjustedDefault());
} else {
@@ -222,8 +215,8 @@
* called. This method takes that into account by always checking the output of
* Locale.getDefault() and recalculating the default LocaleList if needed.</p>
*/
- @NonNull @Size(min = 1)
- public static LocaleListCompat getDefault() {
+ @Size(min = 1)
+ public static @NonNull LocaleListCompat getDefault() {
if (Build.VERSION.SDK_INT >= 24) {
return LocaleListCompat.wrap(Api24Impl.getDefault());
} else {
@@ -319,9 +312,8 @@
return mImpl.hashCode();
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
return mImpl.toString();
}
diff --git a/core/core/src/main/java/androidx/core/os/LocaleListCompatWrapper.java b/core/core/src/main/java/androidx/core/os/LocaleListCompatWrapper.java
index 09a6189..941cda3 100644
--- a/core/core/src/main/java/androidx/core/os/LocaleListCompatWrapper.java
+++ b/core/core/src/main/java/androidx/core/os/LocaleListCompatWrapper.java
@@ -19,11 +19,12 @@
import android.os.Build;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -36,14 +37,12 @@
// This is a comma-separated list of the locales in the LocaleListHelper created at construction
// time, basically the result of running each locale's toLanguageTag() method and concatenating
// them with commas in between.
- @NonNull
- private final String mStringRepresentation;
+ private final @NonNull String mStringRepresentation;
private static final Locale[] sEmptyList = new Locale[0];
- @Nullable
@Override
- public Object getLocaleList() {
+ public @Nullable Object getLocaleList() {
return null;
}
@@ -101,9 +100,8 @@
return result;
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < mList.length; i++) {
@@ -121,7 +119,7 @@
return mStringRepresentation;
}
- LocaleListCompatWrapper(@NonNull Locale... list) {
+ LocaleListCompatWrapper(Locale @NonNull ... list) {
if (list.length == 0) {
mList = sEmptyList;
mStringRepresentation = "";
@@ -265,7 +263,7 @@
}
@Override
- public Locale getFirstMatch(@NonNull String[] supportedLocales) {
+ public Locale getFirstMatch(String @NonNull [] supportedLocales) {
return computeFirstMatch(Arrays.asList(supportedLocales),
false /* assume English is not supported */);
}
diff --git a/core/core/src/main/java/androidx/core/os/LocaleListInterface.java b/core/core/src/main/java/androidx/core/os/LocaleListInterface.java
index 4b9a313..a31535c 100644
--- a/core/core/src/main/java/androidx/core/os/LocaleListInterface.java
+++ b/core/core/src/main/java/androidx/core/os/LocaleListInterface.java
@@ -17,8 +17,9 @@
package androidx.core.os;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import java.util.Locale;
@@ -37,6 +38,5 @@
String toLanguageTags();
- @Nullable
- Locale getFirstMatch(@NonNull String[] supportedLocales);
+ @Nullable Locale getFirstMatch(String @NonNull [] supportedLocales);
}
diff --git a/core/core/src/main/java/androidx/core/os/LocaleListPlatformWrapper.java b/core/core/src/main/java/androidx/core/os/LocaleListPlatformWrapper.java
index 0d96663..6231b04 100644
--- a/core/core/src/main/java/androidx/core/os/LocaleListPlatformWrapper.java
+++ b/core/core/src/main/java/androidx/core/os/LocaleListPlatformWrapper.java
@@ -18,10 +18,11 @@
import android.os.LocaleList;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.Locale;
@RequiresApi(24)
@@ -77,9 +78,8 @@
return mLocaleList.toLanguageTags();
}
- @Nullable
@Override
- public Locale getFirstMatch(@NonNull String[] supportedLocales) {
+ public @Nullable Locale getFirstMatch(String @NonNull [] supportedLocales) {
return mLocaleList.getFirstMatch(supportedLocales);
}
}
diff --git a/core/core/src/main/java/androidx/core/os/MessageCompat.java b/core/core/src/main/java/androidx/core/os/MessageCompat.java
index 7a09d89..00fe687 100644
--- a/core/core/src/main/java/androidx/core/os/MessageCompat.java
+++ b/core/core/src/main/java/androidx/core/os/MessageCompat.java
@@ -22,9 +22,10 @@
import android.os.Message;
import android.view.View;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
/**
* Helper for accessing features in {@link Message}.
*/
diff --git a/core/core/src/main/java/androidx/core/os/OperationCanceledException.java b/core/core/src/main/java/androidx/core/os/OperationCanceledException.java
index b9d9e474..655b49c 100644
--- a/core/core/src/main/java/androidx/core/os/OperationCanceledException.java
+++ b/core/core/src/main/java/androidx/core/os/OperationCanceledException.java
@@ -17,9 +17,10 @@
package androidx.core.os;
-import androidx.annotation.Nullable;
import androidx.core.util.ObjectsCompat;
+import org.jspecify.annotations.Nullable;
+
/**
* An exception type that is thrown when an operation in progress is canceled.
*/
diff --git a/core/core/src/main/java/androidx/core/os/OutcomeReceiverCompat.java b/core/core/src/main/java/androidx/core/os/OutcomeReceiverCompat.java
index 7fb7387..cef9652 100644
--- a/core/core/src/main/java/androidx/core/os/OutcomeReceiverCompat.java
+++ b/core/core/src/main/java/androidx/core/os/OutcomeReceiverCompat.java
@@ -16,7 +16,7 @@
package androidx.core.os;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Callback interface intended for use when an asynchronous operation may result in a failure.
diff --git a/core/core/src/main/java/androidx/core/os/ParcelCompat.java b/core/core/src/main/java/androidx/core/os/ParcelCompat.java
index 425a77ac..1150639 100644
--- a/core/core/src/main/java/androidx/core/os/ParcelCompat.java
+++ b/core/core/src/main/java/androidx/core/os/ParcelCompat.java
@@ -23,10 +23,11 @@
import android.os.Parcelable;
import android.util.SparseArray;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.Serializable;
import java.lang.reflect.Array;
import java.util.ArrayList;
@@ -98,9 +99,8 @@
*/
@SuppressLint({"ConcreteCollection", "NullableCollection"})
@SuppressWarnings({"deprecation", "unchecked"})
- @Nullable
- public static <T> ArrayList<T> readArrayList(@NonNull Parcel in, @Nullable ClassLoader loader,
- @NonNull Class<? extends T> clazz) {
+ public static <T> @Nullable ArrayList<T> readArrayList(@NonNull Parcel in,
+ @Nullable ClassLoader loader, @NonNull Class<? extends T> clazz) {
if (Build.VERSION.SDK_INT >= 34) {
return Api33Impl.readArrayList(in, loader, clazz);
} else {
@@ -124,9 +124,8 @@
*/
@SuppressWarnings({"deprecation", "unchecked"})
@SuppressLint({"ArrayReturn", "NullableCollection"})
- @Nullable
- public static <T> Object[] readArray(@NonNull Parcel in, @Nullable ClassLoader loader,
- @NonNull Class<T> clazz) {
+ public static <T> Object @Nullable [] readArray(@NonNull Parcel in,
+ @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
if (Build.VERSION.SDK_INT >= 34) {
return Api33Impl.readArray(in, loader, clazz);
} else {
@@ -149,8 +148,7 @@
* an error trying to instantiate an element.
*/
@SuppressWarnings("deprecation")
- @Nullable
- public static <T> SparseArray<T> readSparseArray(@NonNull Parcel in,
+ public static <T> @Nullable SparseArray<T> readSparseArray(@NonNull Parcel in,
@Nullable ClassLoader loader, @NonNull Class<? extends T> clazz) {
if (Build.VERSION.SDK_INT >= 34) {
return Api33Impl.readSparseArray(in, loader, clazz);
@@ -198,9 +196,9 @@
*/
@SuppressLint({"ConcreteCollection", "NullableCollection"})
@SuppressWarnings({"deprecation", "unchecked"})
- @Nullable
- public static <K, V> HashMap<K, V> readHashMap(@NonNull Parcel in, @Nullable ClassLoader loader,
- @NonNull Class<? extends K> clazzKey, @NonNull Class<? extends V> clazzValue) {
+ public static <K, V> @Nullable HashMap<K, V> readHashMap(@NonNull Parcel in,
+ @Nullable ClassLoader loader, @NonNull Class<? extends K> clazzKey,
+ @NonNull Class<? extends V> clazzValue) {
if (Build.VERSION.SDK_INT >= 34) {
return Api33Impl.readHashMap(in, loader, clazzKey, clazzValue);
} else {
@@ -223,8 +221,7 @@
* an error trying to instantiate an element.
*/
@SuppressWarnings("deprecation")
- @Nullable
- public static <T extends Parcelable> T readParcelable(@NonNull Parcel in,
+ public static <T extends Parcelable> @Nullable T readParcelable(@NonNull Parcel in,
@Nullable ClassLoader loader, @NonNull Class<T> clazz) {
if (Build.VERSION.SDK_INT >= 34) {
return Api33Impl.readParcelable(in, loader, clazz);
@@ -254,9 +251,8 @@
* there was an error trying to read the {@link Parcelable.Creator}.
*/
@SuppressWarnings({"deprecation", "unchecked"})
- @Nullable
@RequiresApi(30)
- public static <T> Parcelable.Creator<T> readParcelableCreator(@NonNull Parcel in,
+ public static <T> Parcelable.@Nullable Creator<T> readParcelableCreator(@NonNull Parcel in,
@Nullable ClassLoader loader, @NonNull Class<T> clazz) {
if (Build.VERSION.SDK_INT >= 34) {
return Api33Impl.readParcelableCreator(in, loader, clazz);
@@ -284,10 +280,9 @@
*/
@SuppressWarnings({"deprecation", "unchecked"})
@SuppressLint({"ArrayReturn", "NullableCollection"})
- @Nullable
@Deprecated
- public static <T> T[] readParcelableArray(@NonNull Parcel in, @Nullable ClassLoader loader,
- @NonNull Class<T> clazz) {
+ public static <T> T @Nullable [] readParcelableArray(@NonNull Parcel in,
+ @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
if (Build.VERSION.SDK_INT >= 34) {
return Api33Impl.readParcelableArray(in, loader, clazz);
} else {
@@ -328,8 +323,7 @@
*/
@SuppressWarnings({"deprecation"})
@SuppressLint({"ArrayReturn", "NullableCollection"})
- @Nullable
- public static <T> Parcelable[] readParcelableArrayTyped(@NonNull Parcel in,
+ public static <T> Parcelable @Nullable [] readParcelableArrayTyped(@NonNull Parcel in,
@Nullable ClassLoader loader, @NonNull Class<T> clazz) {
if (Build.VERSION.SDK_INT >= 34) {
return (Parcelable[]) Api33Impl.readParcelableArray(in, loader, clazz);
@@ -352,10 +346,9 @@
* deserialized is not an instance of that class or any of its children classes or there was
* an error trying to instantiate an element.
*/
- @NonNull
@SuppressWarnings({"deprecation", "unchecked"})
@RequiresApi(api = Build.VERSION_CODES.Q)
- public static <T> List<T> readParcelableList(@NonNull Parcel in, @NonNull List<T> list,
+ public static <T> @NonNull List<T> readParcelableList(@NonNull Parcel in, @NonNull List<T> list,
@Nullable ClassLoader cl, @NonNull Class<T> clazz) {
if (Build.VERSION.SDK_INT >= 34) {
return Api33Impl.readParcelableList(in, list, cl, clazz);
@@ -380,8 +373,7 @@
* was an error deserializing the object.
*/
@SuppressWarnings({"deprecation", "unchecked"})
- @Nullable
- public static <T extends Serializable> T readSerializable(@NonNull Parcel in,
+ public static <T extends Serializable> @Nullable T readSerializable(@NonNull Parcel in,
@Nullable ClassLoader loader, @NonNull Class<T> clazz) {
if (Build.VERSION.SDK_INT >= 33) {
return Api33Impl.readSerializable(in, loader, clazz);
diff --git a/core/core/src/main/java/androidx/core/os/TraceCompat.java b/core/core/src/main/java/androidx/core/os/TraceCompat.java
index 07074ec..7236a0c 100644
--- a/core/core/src/main/java/androidx/core/os/TraceCompat.java
+++ b/core/core/src/main/java/androidx/core/os/TraceCompat.java
@@ -17,9 +17,10 @@
import android.os.Trace;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
import java.lang.reflect.Field;
import java.lang.reflect.Method;
diff --git a/core/core/src/main/java/androidx/core/os/UserHandleCompat.java b/core/core/src/main/java/androidx/core/os/UserHandleCompat.java
index cfb78e3..5fdafcd 100644
--- a/core/core/src/main/java/androidx/core/os/UserHandleCompat.java
+++ b/core/core/src/main/java/androidx/core/os/UserHandleCompat.java
@@ -19,10 +19,11 @@
import android.os.Build;
import android.os.UserHandle;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -33,10 +34,8 @@
*/
public class UserHandleCompat {
- @Nullable
- private static Method sGetUserIdMethod;
- @Nullable
- private static Constructor<UserHandle> sUserHandleConstructor;
+ private static @Nullable Method sGetUserIdMethod;
+ private static @Nullable Constructor<UserHandle> sUserHandleConstructor;
private UserHandleCompat() {
}
@@ -44,8 +43,7 @@
/**
* Returns the user handle for a given uid.
*/
- @NonNull
- public static UserHandle getUserHandleForUid(int uid) {
+ public static @NonNull UserHandle getUserHandleForUid(int uid) {
if (Build.VERSION.SDK_INT >= 24) {
return Api24Impl.getUserHandleForUid(uid);
} else {
@@ -76,8 +74,7 @@
private Api24Impl() {
}
- @NonNull
- static UserHandle getUserHandleForUid(int uid) {
+ static @NonNull UserHandle getUserHandleForUid(int uid) {
return UserHandle.getUserHandleForUid(uid);
}
}
diff --git a/core/core/src/main/java/androidx/core/os/UserManagerCompat.java b/core/core/src/main/java/androidx/core/os/UserManagerCompat.java
index 596f69a..55a435e 100644
--- a/core/core/src/main/java/androidx/core/os/UserManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/os/UserManagerCompat.java
@@ -20,9 +20,10 @@
import android.os.Build;
import android.os.UserManager;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
/**
* Helper for accessing features in {@link android.os.UserManager} in a backwards compatible
* fashion.
diff --git a/core/core/src/main/java/androidx/core/provider/CallbackWrapper.java b/core/core/src/main/java/androidx/core/provider/CallbackWrapper.java
index 93e2e54..f3daf4b 100644
--- a/core/core/src/main/java/androidx/core/provider/CallbackWrapper.java
+++ b/core/core/src/main/java/androidx/core/provider/CallbackWrapper.java
@@ -20,10 +20,11 @@
import android.graphics.Typeface;
-import androidx.annotation.NonNull;
import androidx.core.provider.FontRequestWorker.TypefaceResult;
import androidx.core.provider.FontsContractCompat.FontRequestCallback;
+import org.jspecify.annotations.NonNull;
+
import java.util.concurrent.Executor;
/**
@@ -33,8 +34,8 @@
* If no Executor is provided, {@link CalleeHandler#create()} is used instead.
*/
class CallbackWrapper {
- @NonNull private final FontRequestCallback mCallback;
- @NonNull private final Executor mExecutor;
+ private final @NonNull FontRequestCallback mCallback;
+ private final @NonNull Executor mExecutor;
/**
* Run callbacks in {@param executor}
@@ -57,7 +58,7 @@
/**
* Mirrors {@link FontRequestCallback#onTypefaceRetrieved(Typeface)}
*/
- private void onTypefaceRetrieved(@NonNull final Typeface typeface) {
+ private void onTypefaceRetrieved(final @NonNull Typeface typeface) {
final FontRequestCallback callback = this.mCallback;
mExecutor.execute(new Runnable() {
@Override
diff --git a/core/core/src/main/java/androidx/core/provider/CalleeHandler.java b/core/core/src/main/java/androidx/core/provider/CalleeHandler.java
index 68fec38..53ff539 100644
--- a/core/core/src/main/java/androidx/core/provider/CalleeHandler.java
+++ b/core/core/src/main/java/androidx/core/provider/CalleeHandler.java
@@ -20,7 +20,7 @@
import android.os.Handler;
import android.os.Looper;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
class CalleeHandler {
private CalleeHandler() { }
@@ -30,8 +30,7 @@
* If the current Thread has a Looper defined uses the current Thread
* looper. Otherwise uses main Looper for the as the Handler.
*/
- @NonNull
- static Handler create() {
+ static @NonNull Handler create() {
final Handler handler;
if (Looper.myLooper() == null) {
handler = new Handler(Looper.getMainLooper());
diff --git a/core/core/src/main/java/androidx/core/provider/DocumentsContractCompat.java b/core/core/src/main/java/androidx/core/provider/DocumentsContractCompat.java
index 11a631f..0d4e9ac 100644
--- a/core/core/src/main/java/androidx/core/provider/DocumentsContractCompat.java
+++ b/core/core/src/main/java/androidx/core/provider/DocumentsContractCompat.java
@@ -24,10 +24,11 @@
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsProvider;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.FileNotFoundException;
import java.util.List;
@@ -90,8 +91,7 @@
*
* @see DocumentsContract#getDocumentId(Uri)
*/
- @Nullable
- public static String getDocumentId(@NonNull Uri documentUri) {
+ public static @Nullable String getDocumentId(@NonNull Uri documentUri) {
return DocumentsContract.getDocumentId(documentUri);
}
@@ -100,8 +100,7 @@
*
* @see DocumentsContract#getTreeDocumentId(Uri)
*/
- @Nullable
- public static String getTreeDocumentId(@NonNull Uri documentUri) {
+ public static @Nullable String getTreeDocumentId(@NonNull Uri documentUri) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return DocumentsContractApi21Impl.getTreeDocumentId(documentUri);
}
@@ -115,8 +114,8 @@
*
* @see DocumentsContract#buildDocumentUri(String, String)
*/
- @Nullable
- public static Uri buildDocumentUri(@NonNull String authority, @NonNull String documentId) {
+ public static @Nullable Uri buildDocumentUri(@NonNull String authority,
+ @NonNull String documentId) {
return DocumentsContract.buildDocumentUri(authority, documentId);
}
@@ -125,8 +124,8 @@
* a document provider. When queried, a provider will return a single row
* with columns defined by {@link Document}.
*/
- @Nullable
- public static Uri buildDocumentUriUsingTree(@NonNull Uri treeUri, @NonNull String documentId) {
+ public static @Nullable Uri buildDocumentUriUsingTree(@NonNull Uri treeUri,
+ @NonNull String documentId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return DocumentsContractApi21Impl.buildDocumentUriUsingTree(treeUri, documentId);
}
@@ -139,8 +138,8 @@
*
* @see DocumentsContract#buildTreeDocumentUri(String, String)
*/
- @Nullable
- public static Uri buildTreeDocumentUri(@NonNull String authority, @NonNull String documentId) {
+ public static @Nullable Uri buildTreeDocumentUri(@NonNull String authority,
+ @NonNull String documentId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return DocumentsContractApi21Impl.buildTreeDocumentUri(authority, documentId);
}
@@ -154,8 +153,7 @@
*
* @see DocumentsContract#buildChildDocumentsUri(String, String)
*/
- @Nullable
- public static Uri buildChildDocumentsUri(@NonNull String authority,
+ public static @Nullable Uri buildChildDocumentsUri(@NonNull String authority,
@Nullable String parentDocumentId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return DocumentsContractApi21Impl.buildChildDocumentsUri(authority, parentDocumentId);
@@ -170,8 +168,7 @@
*
* @see DocumentsContract#buildChildDocumentsUriUsingTree(Uri, String)
*/
- @Nullable
- public static Uri buildChildDocumentsUriUsingTree(@NonNull Uri treeUri,
+ public static @Nullable Uri buildChildDocumentsUriUsingTree(@NonNull Uri treeUri,
@NonNull String parentDocumentId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return DocumentsContractApi21Impl.buildChildDocumentsUriUsingTree(treeUri,
@@ -189,8 +186,7 @@
* @param displayName name of new document
* @return newly created document, or {@code null} if failed
*/
- @Nullable
- public static Uri createDocument(@NonNull ContentResolver content,
+ public static @Nullable Uri createDocument(@NonNull ContentResolver content,
@NonNull Uri parentDocumentUri, @NonNull String mimeType, @NonNull String displayName)
throws FileNotFoundException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@@ -205,8 +201,7 @@
*
* @see DocumentsContract#renameDocument(ContentResolver, Uri, String)
*/
- @Nullable
- public static Uri renameDocument(@NonNull ContentResolver content,
+ public static @Nullable Uri renameDocument(@NonNull ContentResolver content,
@NonNull Uri documentUri, @NonNull String displayName) throws FileNotFoundException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return DocumentsContractApi21Impl.renameDocument(content, documentUri, displayName);
diff --git a/core/core/src/main/java/androidx/core/provider/FontProvider.java b/core/core/src/main/java/androidx/core/provider/FontProvider.java
index 387b0ad..fe2d5c4 100644
--- a/core/core/src/main/java/androidx/core/provider/FontProvider.java
+++ b/core/core/src/main/java/androidx/core/provider/FontProvider.java
@@ -33,8 +33,6 @@
import android.os.RemoteException;
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.collection.LruCache;
@@ -44,6 +42,9 @@
import androidx.core.provider.FontsContractCompat.FontInfo;
import androidx.tracing.Trace;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -54,8 +55,7 @@
class FontProvider {
private FontProvider() {}
- @NonNull
- static FontFamilyResult getFontFamilyResult(@NonNull Context context,
+ static @NonNull FontFamilyResult getFontFamilyResult(@NonNull Context context,
@NonNull List<FontRequest> requests, @Nullable CancellationSignal cancellationSignal)
throws PackageManager.NameNotFoundException {
if (TypefaceCompat.DOWNLOADABLE_FONT_TRACING) {
@@ -132,8 +132,7 @@
* Do not access directly, visible for testing only.
*/
@VisibleForTesting
- @Nullable
- static ProviderInfo getProvider(
+ static @Nullable ProviderInfo getProvider(
@NonNull PackageManager packageManager,
@NonNull FontRequest request,
@Nullable Resources resources
@@ -191,8 +190,7 @@
* Do not access directly, visible for testing only.
*/
@VisibleForTesting
- @NonNull
- static FontInfo[] query(
+ static FontInfo @NonNull [] query(
Context context,
FontRequest request,
String authority,
diff --git a/core/core/src/main/java/androidx/core/provider/FontRequest.java b/core/core/src/main/java/androidx/core/provider/FontRequest.java
index 7088203..2c1af7a 100644
--- a/core/core/src/main/java/androidx/core/provider/FontRequest.java
+++ b/core/core/src/main/java/androidx/core/provider/FontRequest.java
@@ -22,11 +22,12 @@
import android.util.Base64;
import androidx.annotation.ArrayRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.List;
/**
@@ -97,8 +98,7 @@
* Returns the selected font provider's authority. This tells the system what font provider
* it should request the font from.
*/
- @NonNull
- public String getProviderAuthority() {
+ public @NonNull String getProviderAuthority() {
return mProviderAuthority;
}
@@ -106,8 +106,7 @@
* Returns the selected font provider's package. This helps the system verify that the provider
* identified by the given authority is the one requested.
*/
- @NonNull
- public String getProviderPackage() {
+ public @NonNull String getProviderPackage() {
return mProviderPackage;
}
@@ -115,8 +114,7 @@
* Returns the query string. Refer to your font provider's documentation on the format of this
* string.
*/
- @NonNull
- public String getQuery() {
+ public @NonNull String getQuery() {
return mQuery;
}
@@ -127,8 +125,7 @@
*
* @see #getCertificatesArrayResId()
*/
- @Nullable
- public List<List<byte[]>> getCertificates() {
+ public @Nullable List<List<byte[]>> getCertificates() {
return mCertificates;
}
@@ -156,8 +153,7 @@
}
@RestrictTo(LIBRARY)
- @NonNull
- String getId() {
+ @NonNull String getId() {
return mIdentifier;
}
diff --git a/core/core/src/main/java/androidx/core/provider/FontRequestWorker.java b/core/core/src/main/java/androidx/core/provider/FontRequestWorker.java
index 509de8d..3adff39 100644
--- a/core/core/src/main/java/androidx/core/provider/FontRequestWorker.java
+++ b/core/core/src/main/java/androidx/core/provider/FontRequestWorker.java
@@ -32,8 +32,6 @@
import android.os.Process;
import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.collection.LruCache;
import androidx.collection.SimpleArrayMap;
import androidx.core.content.res.FontResourcesParserCompat;
@@ -43,6 +41,9 @@
import androidx.core.util.Consumer;
import androidx.tracing.Trace;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
@@ -99,9 +100,9 @@
* @return
*/
static Typeface requestFontSync(
- @NonNull final Context context,
- @NonNull final FontRequest request,
- @NonNull final CallbackWrapper callback,
+ final @NonNull Context context,
+ final @NonNull FontRequest request,
+ final @NonNull CallbackWrapper callback,
final int style,
int timeoutInMillis
) {
@@ -161,11 +162,11 @@
* @return
*/
static Typeface requestFontAsync(
- @NonNull final Context context,
- @NonNull final List<FontRequest> requests,
+ final @NonNull Context context,
+ final @NonNull List<FontRequest> requests,
final int style,
- @Nullable final Executor executor,
- @NonNull final CallbackWrapper callback
+ final @Nullable Executor executor,
+ final @NonNull CallbackWrapper callback
) {
final String id = createCacheId(requests, style);
Typeface cached = sTypefaceCache.get(id);
@@ -242,11 +243,10 @@
}
/** Package protected to prevent synthetic accessor */
- @NonNull
- static TypefaceResult getFontSync(
- @NonNull final String cacheId,
- @NonNull final Context context,
- @NonNull final List<FontRequest> requests,
+ static @NonNull TypefaceResult getFontSync(
+ final @NonNull String cacheId,
+ final @NonNull Context context,
+ final @NonNull List<FontRequest> requests,
int style
) {
if (TypefaceCompat.DOWNLOADABLE_FONT_TRACING) {
diff --git a/core/core/src/main/java/androidx/core/provider/FontsContractCompat.java b/core/core/src/main/java/androidx/core/provider/FontsContractCompat.java
index b5e4dee..0aa23265 100644
--- a/core/core/src/main/java/androidx/core/provider/FontsContractCompat.java
+++ b/core/core/src/main/java/androidx/core/provider/FontsContractCompat.java
@@ -32,8 +32,6 @@
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.res.ResourcesCompat;
@@ -41,6 +39,9 @@
import androidx.core.graphics.TypefaceCompatUtil;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
@@ -77,11 +78,10 @@
* @param fonts An array of {@link FontInfo} to be used to create a Typeface.
* @return A Typeface object. Returns null if typeface creation fails.
*/
- @Nullable
- public static Typeface buildTypeface(
+ public static @Nullable Typeface buildTypeface(
@NonNull Context context,
@Nullable CancellationSignal cancellationSignal,
- @NonNull FontInfo[] fonts
+ FontInfo @NonNull [] fonts
) {
return TypefaceCompat.createFromFontInfo(context, cancellationSignal, fonts,
Typeface.NORMAL);
@@ -103,8 +103,7 @@
* @throws PackageManager.NameNotFoundException If requested package or authority was not found
* in the system.
*/
- @NonNull
- public static FontFamilyResult fetchFonts(
+ public static @NonNull FontFamilyResult fetchFonts(
@NonNull Context context,
@Nullable CancellationSignal cancellationSignal,
@NonNull FontRequest request
@@ -239,15 +238,14 @@
*
*/
@RestrictTo(LIBRARY)
- @Nullable
- public static Typeface requestFont(
- @NonNull final Context context,
- @NonNull final List<FontRequest> requests,
+ public static @Nullable Typeface requestFont(
+ final @NonNull Context context,
+ final @NonNull List<FontRequest> requests,
@TypefaceStyle final int style,
boolean isBlockingFetch,
@IntRange(from = 0) int timeout,
- @NonNull final Handler handler,
- @NonNull final FontRequestCallback callback
+ final @NonNull Handler handler,
+ final @NonNull FontRequestCallback callback
) {
CallbackWrapper callbackWrapper = new CallbackWrapper(
callback, RequestExecutor.createHandlerExecutor(handler));
@@ -290,15 +288,14 @@
*
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @Nullable
- public static Typeface requestFont(
- @NonNull final Context context,
- @NonNull final FontRequest request,
+ public static @Nullable Typeface requestFont(
+ final @NonNull Context context,
+ final @NonNull FontRequest request,
@TypefaceStyle final int style,
boolean isBlockingFetch,
@IntRange(from = 0) int timeout,
- @NonNull final Handler handler,
- @NonNull final FontRequestCallback callback
+ final @NonNull Handler handler,
+ final @NonNull FontRequestCallback callback
) {
return requestFont(context, List.of(request), style, isBlockingFetch, timeout, handler,
callback);
@@ -516,7 +513,7 @@
// TODO after removing from public API make package private.
@Deprecated
@RestrictTo(LIBRARY_GROUP_PREFIX)
- public FontFamilyResult(@FontResultStatus int statusCode, @Nullable FontInfo[] fonts) {
+ public FontFamilyResult(@FontResultStatus int statusCode, FontInfo @Nullable [] fonts) {
mStatusCode = statusCode;
mFonts = Collections.singletonList(fonts);
}
@@ -547,15 +544,14 @@
/**
* Returns a list of arrays of fonts for each font family requested, in order.
*/
- @NonNull
- public List<FontInfo[]> getFontsWithFallbacks() {
+ public @NonNull List<FontInfo[]> getFontsWithFallbacks() {
return mFonts;
}
@SuppressWarnings("deprecation")
static FontFamilyResult create(
@FontResultStatus int statusCode,
- @Nullable FontInfo[] fonts) {
+ FontInfo @Nullable [] fonts) {
return new FontFamilyResult(statusCode, fonts);
}
@@ -695,7 +691,7 @@
public static Typeface getFontSync(
final Context context,
final FontRequest request,
- final @Nullable ResourcesCompat.FontCallback fontCallback,
+ final ResourcesCompat.@Nullable FontCallback fontCallback,
final @Nullable Handler handler,
boolean isBlockingFetch,
int timeout,
@@ -749,8 +745,7 @@
@Deprecated // unused
@VisibleForTesting
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @Nullable
- public static ProviderInfo getProvider(
+ public static @Nullable ProviderInfo getProvider(
@NonNull PackageManager packageManager,
@NonNull FontRequest request,
@Nullable Resources resources
diff --git a/core/core/src/main/java/androidx/core/provider/RequestExecutor.java b/core/core/src/main/java/androidx/core/provider/RequestExecutor.java
index 3820943..7fdcf33 100644
--- a/core/core/src/main/java/androidx/core/provider/RequestExecutor.java
+++ b/core/core/src/main/java/androidx/core/provider/RequestExecutor.java
@@ -20,10 +20,11 @@
import android.os.Process;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
@@ -59,7 +60,7 @@
static <T> T submit(
@NonNull ExecutorService executor,
- @NonNull final Callable<T> callable,
+ final @NonNull Callable<T> callable,
@IntRange(from = 0) int timeoutMillis
) throws InterruptedException {
Future<T> future = executor.submit(callable);
diff --git a/core/core/src/main/java/androidx/core/service/quicksettings/PendingIntentActivityWrapper.java b/core/core/src/main/java/androidx/core/service/quicksettings/PendingIntentActivityWrapper.java
index d42dd7c..410031c 100644
--- a/core/core/src/main/java/androidx/core/service/quicksettings/PendingIntentActivityWrapper.java
+++ b/core/core/src/main/java/androidx/core/service/quicksettings/PendingIntentActivityWrapper.java
@@ -22,10 +22,11 @@
import android.os.Bundle;
import android.service.quicksettings.TileService;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.app.PendingIntentCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* A wrapper class for developers to use with
* {@link TileServiceCompat#startActivityAndCollapse(TileService, PendingIntentActivityWrapper)}.
@@ -36,17 +37,14 @@
private final int mRequestCode;
- @NonNull
- private final Intent mIntent;
+ private final @NonNull Intent mIntent;
@PendingIntentCompat.Flags
private final int mFlags;
- @Nullable
- private final Bundle mOptions;
+ private final @Nullable Bundle mOptions;
- @Nullable
- private final PendingIntent mPendingIntent;
+ private final @Nullable PendingIntent mPendingIntent;
private final boolean mIsMutable;
diff --git a/core/core/src/main/java/androidx/core/service/quicksettings/TileServiceCompat.java b/core/core/src/main/java/androidx/core/service/quicksettings/TileServiceCompat.java
index 436cd1a..bbc1a6e 100644
--- a/core/core/src/main/java/androidx/core/service/quicksettings/TileServiceCompat.java
+++ b/core/core/src/main/java/androidx/core/service/quicksettings/TileServiceCompat.java
@@ -22,10 +22,11 @@
import android.content.Intent;
import android.service.quicksettings.TileService;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
/**
* A helper for accessing {@link TileService} API methods.
*/
diff --git a/core/core/src/main/java/androidx/core/telephony/TelephonyManagerCompat.java b/core/core/src/main/java/androidx/core/telephony/TelephonyManagerCompat.java
index e29978e..c3f6cf9 100644
--- a/core/core/src/main/java/androidx/core/telephony/TelephonyManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/telephony/TelephonyManagerCompat.java
@@ -23,11 +23,12 @@
import android.os.Build.VERSION;
import android.telephony.TelephonyManager;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -64,8 +65,7 @@
*/
@SuppressLint("MissingPermission")
@RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
- @Nullable
- public static String getImei(@NonNull TelephonyManager telephonyManager) {
+ public static @Nullable String getImei(@NonNull TelephonyManager telephonyManager) {
if (VERSION.SDK_INT >= 26) {
return Api26Impl.getImei(telephonyManager);
} else if (VERSION.SDK_INT >= 22) {
@@ -147,8 +147,7 @@
@SuppressLint("MissingPermission")
@RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
- @Nullable
- static String getImei(TelephonyManager telephonyManager) {
+ static @Nullable String getImei(TelephonyManager telephonyManager) {
return telephonyManager.getImei();
}
}
@@ -159,8 +158,7 @@
@SuppressLint("MissingPermission")
@RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
- @Nullable
- static String getDeviceId(TelephonyManager telephonyManager, int slotIndex) {
+ static @Nullable String getDeviceId(TelephonyManager telephonyManager, int slotIndex) {
return telephonyManager.getDeviceId(slotIndex);
}
}
diff --git a/core/core/src/main/java/androidx/core/telephony/mbms/MbmsHelper.java b/core/core/src/main/java/androidx/core/telephony/mbms/MbmsHelper.java
index 4977aa4..7f534fe 100644
--- a/core/core/src/main/java/androidx/core/telephony/mbms/MbmsHelper.java
+++ b/core/core/src/main/java/androidx/core/telephony/mbms/MbmsHelper.java
@@ -21,10 +21,11 @@
import android.os.LocaleList;
import android.telephony.mbms.ServiceInfo;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.Locale;
import java.util.Set;
@@ -49,8 +50,7 @@
* @return The best name to display to the user for the service, or {@code null} if nothing
* matches.
*/
- @Nullable
- public static CharSequence getBestNameForService(@NonNull Context context,
+ public static @Nullable CharSequence getBestNameForService(@NonNull Context context,
@NonNull ServiceInfo serviceInfo) {
if (Build.VERSION.SDK_INT >= 28) {
return Api28Impl.getBestNameForService(context, serviceInfo);
diff --git a/core/core/src/main/java/androidx/core/text/HtmlCompat.java b/core/core/src/main/java/androidx/core/text/HtmlCompat.java
index 930a601..1e2c667 100644
--- a/core/core/src/main/java/androidx/core/text/HtmlCompat.java
+++ b/core/core/src/main/java/androidx/core/text/HtmlCompat.java
@@ -31,11 +31,12 @@
import android.text.style.ParagraphStyle;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
/**
@@ -141,8 +142,7 @@
* ignored and {@link Html#fromHtml(String)} is used.
*/
@SuppressWarnings("deprecation")
- @NonNull
- public static Spanned fromHtml(@NonNull String source, @FromHtmlFlags int flags) {
+ public static @NonNull Spanned fromHtml(@NonNull String source, @FromHtmlFlags int flags) {
if (Build.VERSION.SDK_INT >= 24) {
return Api24Impl.fromHtml(source, flags);
}
@@ -155,8 +155,7 @@
* {@link Html#fromHtml(String, ImageGetter, TagHandler)} is used.
*/
@SuppressWarnings("deprecation")
- @NonNull
- public static Spanned fromHtml(@NonNull String source, @FromHtmlFlags int flags,
+ public static @NonNull Spanned fromHtml(@NonNull String source, @FromHtmlFlags int flags,
@Nullable ImageGetter imageGetter, @Nullable TagHandler tagHandler) {
if (Build.VERSION.SDK_INT >= 24) {
return Api24Impl.fromHtml(source, flags, imageGetter, tagHandler);
@@ -169,8 +168,7 @@
* ignored and {@link Html#toHtml(Spanned)} is used.
*/
@SuppressWarnings("deprecation")
- @NonNull
- public static String toHtml(@NonNull Spanned text, @ToHtmlOptions int options) {
+ public static @NonNull String toHtml(@NonNull Spanned text, @ToHtmlOptions int options) {
if (Build.VERSION.SDK_INT >= 24) {
return Api24Impl.toHtml(text, options);
}
diff --git a/core/core/src/main/java/androidx/core/text/ICUCompat.java b/core/core/src/main/java/androidx/core/text/ICUCompat.java
index b3b58c9..a12c2ce 100644
--- a/core/core/src/main/java/androidx/core/text/ICUCompat.java
+++ b/core/core/src/main/java/androidx/core/text/ICUCompat.java
@@ -21,10 +21,11 @@
import android.os.Build;
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Locale;
@@ -82,8 +83,7 @@
*
* @return The script for a given Locale if ICU library is available, otherwise null.
*/
- @Nullable
- public static String maximizeAndGetScript(@NonNull Locale locale) {
+ public static @Nullable String maximizeAndGetScript(@NonNull Locale locale) {
if (Build.VERSION.SDK_INT >= 24) {
Object uLocale = Api24Impl.addLikelySubtags(Api24Impl.forLocale(locale));
return Api24Impl.getScript(uLocale);
diff --git a/core/core/src/main/java/androidx/core/text/PrecomputedTextCompat.java b/core/core/src/main/java/androidx/core/text/PrecomputedTextCompat.java
index 34d6050c..9c0f43f 100644
--- a/core/core/src/main/java/androidx/core/text/PrecomputedTextCompat.java
+++ b/core/core/src/main/java/androidx/core/text/PrecomputedTextCompat.java
@@ -34,14 +34,15 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.UiThread;
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
@@ -205,7 +206,7 @@
}
@RequiresApi(28)
- public Params(@NonNull PrecomputedText.Params wrapped) {
+ public Params(PrecomputedText.@NonNull Params wrapped) {
mPaint = wrapped.getTextPaint();
mTextDir = wrapped.getTextDirection();
mBreakStrategy = wrapped.getBreakStrategy();
@@ -386,7 +387,7 @@
private final @NonNull Params mParams;
// The list of measured paragraph info.
- private final @NonNull int[] mParagraphEnds;
+ private final int @NonNull [] mParagraphEnds;
// null on API 27 or before. Non-null on API 29 or later
private final @Nullable PrecomputedText mWrapped;
@@ -466,7 +467,7 @@
// Use PrecomputedText.create instead.
private PrecomputedTextCompat(@NonNull CharSequence text, @NonNull Params params,
- @NonNull int[] paraEnds) {
+ int @NonNull [] paraEnds) {
mText = new SpannableString(text);
mParams = params;
mParagraphEnds = paraEnds;
@@ -544,8 +545,8 @@
private PrecomputedTextCompat.Params mParams;
private CharSequence mText;
- PrecomputedTextCallback(@NonNull final PrecomputedTextCompat.Params params,
- @NonNull final CharSequence cs) {
+ PrecomputedTextCallback(final PrecomputedTextCompat.@NonNull Params params,
+ final @NonNull CharSequence cs) {
mParams = params;
mText = cs;
}
@@ -556,8 +557,8 @@
}
}
- PrecomputedTextFutureTask(@NonNull final PrecomputedTextCompat.Params params,
- @NonNull final CharSequence text) {
+ PrecomputedTextFutureTask(final PrecomputedTextCompat.@NonNull Params params,
+ final @NonNull CharSequence text) {
super(new PrecomputedTextCallback(params, text));
}
}
@@ -623,7 +624,7 @@
*/
@UiThread
public static Future<PrecomputedTextCompat> getTextFuture(
- @NonNull final CharSequence charSequence, @NonNull PrecomputedTextCompat.Params params,
+ final @NonNull CharSequence charSequence, PrecomputedTextCompat.@NonNull Params params,
@Nullable Executor executor) {
PrecomputedTextFutureTask task = new PrecomputedTextFutureTask(params, charSequence);
if (executor == null) {
@@ -731,9 +732,8 @@
return mText.subSequence(start, end);
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
return mText.toString();
}
diff --git a/core/core/src/main/java/androidx/core/text/TextUtilsCompat.java b/core/core/src/main/java/androidx/core/text/TextUtilsCompat.java
index bdcd81ee..c485666 100644
--- a/core/core/src/main/java/androidx/core/text/TextUtilsCompat.java
+++ b/core/core/src/main/java/androidx/core/text/TextUtilsCompat.java
@@ -18,10 +18,11 @@
import android.text.TextUtils;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.Locale;
/**
@@ -35,8 +36,7 @@
* @param s the string to be encoded
* @return the encoded string
*/
- @NonNull
- public static String htmlEncode(@NonNull String s) {
+ public static @NonNull String htmlEncode(@NonNull String s) {
return TextUtils.htmlEncode(s);
}
diff --git a/core/core/src/main/java/androidx/core/text/method/LinkMovementMethodCompat.java b/core/core/src/main/java/androidx/core/text/method/LinkMovementMethodCompat.java
index f0701b0..cd87a66 100644
--- a/core/core/src/main/java/androidx/core/text/method/LinkMovementMethodCompat.java
+++ b/core/core/src/main/java/androidx/core/text/method/LinkMovementMethodCompat.java
@@ -25,8 +25,8 @@
import android.view.MotionEvent;
import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* Backwards compatible version of {@link LinkMovementMethod} which fixes the issue that links can
@@ -81,8 +81,7 @@
*
* @return the singleton instance of {@link LinkMovementMethodCompat}
*/
- @NonNull
- public static LinkMovementMethodCompat getInstance() {
+ public static @NonNull LinkMovementMethodCompat getInstance() {
if (sInstance == null) {
sInstance = new LinkMovementMethodCompat();
}
diff --git a/core/core/src/main/java/androidx/core/text/util/LinkifyCompat.java b/core/core/src/main/java/androidx/core/text/util/LinkifyCompat.java
index d5ddfc9..c509cb7 100644
--- a/core/core/src/main/java/androidx/core/text/util/LinkifyCompat.java
+++ b/core/core/src/main/java/androidx/core/text/util/LinkifyCompat.java
@@ -32,12 +32,13 @@
import android.widget.TextView;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.util.PatternsCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.UnsupportedEncodingException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -242,7 +243,7 @@
* @param transformFilter Filter to allow the client code to update the link found.
*/
public static void addLinks(@NonNull TextView text, @NonNull Pattern pattern,
- @Nullable String defaultScheme, @Nullable String[] schemes,
+ @Nullable String defaultScheme, String @Nullable [] schemes,
@Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter) {
if (shouldAddLinksFallbackToFramework()) {
Api24Impl.addLinks(text, pattern, defaultScheme, schemes, matchFilter, transformFilter);
@@ -317,7 +318,7 @@
* @return True if at least one link is found and applied.
*/
public static boolean addLinks(@NonNull Spannable spannable, @NonNull Pattern pattern,
- @Nullable String defaultScheme, @Nullable String[] schemes,
+ @Nullable String defaultScheme, String @Nullable [] schemes,
@Nullable MatchFilter matchFilter, @Nullable TransformFilter transformFilter) {
if (shouldAddLinksFallbackToFramework()) {
return Api24Impl.addLinks(spannable, pattern, defaultScheme, schemes, matchFilter,
@@ -374,7 +375,7 @@
}
}
- private static String makeUrl(@NonNull String url, @NonNull String[] prefixes,
+ private static String makeUrl(@NonNull String url, String @NonNull [] prefixes,
Matcher matcher, @Nullable TransformFilter filter) {
if (filter != null) {
url = filter.transformUrl(matcher, url);
diff --git a/core/core/src/main/java/androidx/core/text/util/LocalePreferences.java b/core/core/src/main/java/androidx/core/text/util/LocalePreferences.java
index 846f4c2..6b727a8 100644
--- a/core/core/src/main/java/androidx/core/text/util/LocalePreferences.java
+++ b/core/core/src/main/java/androidx/core/text/util/LocalePreferences.java
@@ -24,11 +24,12 @@
import android.os.Build;
import android.os.Build.VERSION_CODES;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.StringDef;
+import org.jspecify.annotations.NonNull;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
@@ -80,9 +81,8 @@
* bases on the {@code Locale#getDefault(Locale.Category)}. It is one of the strings defined in
* {@see HourCycle}, e.g. {@code HourCycle#H11}.
*/
- @NonNull
@HourCycle.HourCycleTypes
- public static String getHourCycle() {
+ public static @NonNull String getHourCycle() {
return getHourCycle(true);
}
@@ -91,9 +91,8 @@
* and based on the input {@code Locale}. It is one of the strings defined in
* {@see HourCycle}, e.g. {@code HourCycle#H11}.
*/
- @NonNull
@HourCycle.HourCycleTypes
- public static String getHourCycle(@NonNull Locale locale) {
+ public static @NonNull String getHourCycle(@NonNull Locale locale) {
return getHourCycle(locale, true);
}
@@ -113,9 +112,8 @@
* in the hour cycle subtag, e.g. en-US-u-hc-h32, this function returns empty string, i.e.
* {@code HourCycle#DEFAULT}.
*/
- @NonNull
@HourCycle.HourCycleTypes
- public static String getHourCycle(
+ public static @NonNull String getHourCycle(
boolean resolved) {
Locale defaultLocale = (Build.VERSION.SDK_INT >= VERSION_CODES.N)
? Api24Impl.getDefaultLocale()
@@ -137,9 +135,8 @@
* in the hour cycle subtag, e.g. en-US-u-hc-h32, this function returns empty string, i.e.
* {@code HourCycle#DEFAULT}.
*/
- @NonNull
@HourCycle.HourCycleTypes
- public static String getHourCycle(@NonNull Locale locale, boolean resolved) {
+ public static @NonNull String getHourCycle(@NonNull Locale locale, boolean resolved) {
String result = getUnicodeLocaleType(HourCycle.U_EXTENSION_TAG,
HourCycle.DEFAULT, locale, resolved);
if (result != null) {
@@ -209,9 +206,8 @@
* the {@code Locale#getDefault(Locale.Category)} settings. It is one of the strings defined in
* {@see CalendarType}, e.g. {@code CalendarType#CHINESE}.
*/
- @NonNull
@CalendarType.CalendarTypes
- public static String getCalendarType() {
+ public static @NonNull String getCalendarType() {
return getCalendarType(true);
}
@@ -220,9 +216,8 @@
* based on the input {@link Locale} settings. It is one of the strings defined in
* {@see CalendarType}, e.g. {@code CalendarType#CHINESE}.
*/
- @NonNull
@CalendarType.CalendarTypes
- public static String getCalendarType(@NonNull Locale locale) {
+ public static @NonNull String getCalendarType(@NonNull Locale locale) {
return getCalendarType(locale, true);
}
@@ -243,9 +238,8 @@
* specified in the calendar type subtag, e.g. en-US-u-ca-calendar, this function returns
* empty string, i.e. {@code CalendarType#DEFAULT}.
*/
- @NonNull
@CalendarType.CalendarTypes
- public static String getCalendarType(boolean resolved) {
+ public static @NonNull String getCalendarType(boolean resolved) {
Locale defaultLocale = (Build.VERSION.SDK_INT >= VERSION_CODES.N)
? Api24Impl.getDefaultLocale()
: getDefaultLocale();
@@ -266,9 +260,8 @@
* specified in the calendar type subtag, e.g. en-US-u-ca-calendar, this function returns
* empty string, i.e. {@code CalendarType#DEFAULT}.
*/
- @NonNull
@CalendarType.CalendarTypes
- public static String getCalendarType(@NonNull Locale locale, boolean resolved) {
+ public static @NonNull String getCalendarType(@NonNull Locale locale, boolean resolved) {
String result = getUnicodeLocaleType(CalendarType.U_EXTENSION_TAG,
CalendarType.DEFAULT, locale, resolved);
if (result != null) {
@@ -314,9 +307,8 @@
* {@code Locale#getDefault(Locale.Category)} settings. It is one of the strings defined in
* {@see TemperatureUnit}, e.g. {@code TemperatureUnit#FAHRENHEIT}.
*/
- @NonNull
@TemperatureUnit.TemperatureUnits
- public static String getTemperatureUnit() {
+ public static @NonNull String getTemperatureUnit() {
return getTemperatureUnit(true);
}
@@ -324,9 +316,8 @@
* Return the temperature unit of the inputted {@link Locale}. It is one of the strings
* defined in {@see TemperatureUnit}, e.g. {@code TemperatureUnit#FAHRENHEIT}.
*/
- @NonNull
@TemperatureUnit.TemperatureUnits
- public static String getTemperatureUnit(
+ public static @NonNull String getTemperatureUnit(
@NonNull Locale locale) {
return getTemperatureUnit(locale, true);
}
@@ -348,9 +339,8 @@
* specified in the temperature unit subtag, e.g. en-US-u-mu-temperature, this function returns
* empty string, i.e. {@code TemperatureUnit#DEFAULT}.
*/
- @NonNull
@TemperatureUnit.TemperatureUnits
- public static String getTemperatureUnit(boolean resolved) {
+ public static @NonNull String getTemperatureUnit(boolean resolved) {
Locale defaultLocale = (Build.VERSION.SDK_INT >= VERSION_CODES.N)
? Api24Impl.getDefaultLocale()
: getDefaultLocale();
@@ -372,9 +362,8 @@
* specified in the temperature unit subtag, e.g. en-US-u-mu-temperature, this function returns
* empty string, i.e. {@code TemperatureUnit#DEFAULT}.
*/
- @NonNull
@TemperatureUnit.TemperatureUnits
- public static String getTemperatureUnit(@NonNull Locale locale, boolean resolved) {
+ public static @NonNull String getTemperatureUnit(@NonNull Locale locale, boolean resolved) {
String result = getUnicodeLocaleType(TemperatureUnit.U_EXTENSION_TAG,
TemperatureUnit.DEFAULT, locale, resolved);
if (result != null) {
@@ -432,9 +421,8 @@
* {@code Locale#getDefault(Locale.Category)} settings. It is one of the strings defined in
* {@see FirstDayOfWeek}, e.g. {@code FirstDayOfWeek#SUNDAY}.
*/
- @NonNull
@FirstDayOfWeek.Days
- public static String getFirstDayOfWeek() {
+ public static @NonNull String getFirstDayOfWeek() {
return getFirstDayOfWeek(true);
}
@@ -443,9 +431,8 @@
* and based on the input {@code Locale} settings. It is one of the strings defined in
* {@see FirstDayOfWeek}, e.g. {@code FirstDayOfWeek#SUNDAY}.
*/
- @NonNull
@FirstDayOfWeek.Days
- public static String getFirstDayOfWeek(@NonNull Locale locale) {
+ public static @NonNull String getFirstDayOfWeek(@NonNull Locale locale) {
return getFirstDayOfWeek(locale, true);
}
@@ -466,9 +453,8 @@
* in the first day of week subtag, e.g. en-US-u-fw-days, this function returns empty string,
* i.e. {@code FirstDayOfWeek#DEFAULT}.
*/
- @NonNull
@FirstDayOfWeek.Days
- public static String getFirstDayOfWeek(boolean resolved) {
+ public static @NonNull String getFirstDayOfWeek(boolean resolved) {
Locale defaultLocale = (Build.VERSION.SDK_INT >= VERSION_CODES.N)
? Api24Impl.getDefaultLocale()
: getDefaultLocale();
@@ -491,9 +477,8 @@
* specified in the first day of week subtag, e.g. en-US-u-fw-days, this function returns
* empty string, i.e. {@code FirstDayOfWeek#DEFAULT}.
*/
- @NonNull
@FirstDayOfWeek.Days
- public static String getFirstDayOfWeek(
+ public static @NonNull String getFirstDayOfWeek(
@NonNull Locale locale, boolean resolved) {
String result = getUnicodeLocaleType(FirstDayOfWeek.U_EXTENSION_TAG,
FirstDayOfWeek.DEFAULT, locale, resolved);
diff --git a/core/core/src/main/java/androidx/core/util/AtomicFile.java b/core/core/src/main/java/androidx/core/util/AtomicFile.java
index 7310972..129115e 100644
--- a/core/core/src/main/java/androidx/core/util/AtomicFile.java
+++ b/core/core/src/main/java/androidx/core/util/AtomicFile.java
@@ -18,8 +18,8 @@
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import java.io.File;
import java.io.FileInputStream;
@@ -63,8 +63,7 @@
* Return the path to the base file. You should not generally use this,
* as the data at that path may not be valid.
*/
- @NonNull
- public File getBaseFile() {
+ public @NonNull File getBaseFile() {
return mBaseName;
}
@@ -91,8 +90,7 @@
* safe (or will be lost). You must do your own threading protection for
* access to AtomicFile.
*/
- @NonNull
- public FileOutputStream startWrite() throws IOException {
+ public @NonNull FileOutputStream startWrite() throws IOException {
if (mLegacyBackupName.exists()) {
rename(mLegacyBackupName, mBaseName);
}
@@ -161,8 +159,7 @@
* <p>
* You must do your own threading protection for access to AtomicFile.
*/
- @NonNull
- public FileInputStream openRead() throws FileNotFoundException {
+ public @NonNull FileInputStream openRead() throws FileNotFoundException {
if (mLegacyBackupName.exists()) {
rename(mLegacyBackupName, mBaseName);
}
@@ -186,8 +183,7 @@
* A convenience for {@link #openRead()} that also reads all of the
* file contents into a byte array which is returned.
*/
- @NonNull
- public byte[] readFully() throws IOException {
+ public byte @NonNull [] readFully() throws IOException {
FileInputStream stream = openRead();
try {
int pos = 0;
diff --git a/core/core/src/main/java/androidx/core/util/ObjectsCompat.java b/core/core/src/main/java/androidx/core/util/ObjectsCompat.java
index b7b9a03..9d65e12 100644
--- a/core/core/src/main/java/androidx/core/util/ObjectsCompat.java
+++ b/core/core/src/main/java/androidx/core/util/ObjectsCompat.java
@@ -15,8 +15,8 @@
*/
package androidx.core.util;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import java.util.Arrays;
import java.util.Objects;
@@ -84,7 +84,7 @@
* @return a hash value of the sequence of input values
* @see Arrays#hashCode(Object[])
*/
- public static int hash(@Nullable Object... values) {
+ public static int hash(Object @Nullable ... values) {
return Objects.hash(values);
}
@@ -97,8 +97,7 @@
* @return the result of calling {@code toString} on the first argument if it is not {@code
* null} and the second argument otherwise.
*/
- @Nullable
- public static String toString(@Nullable Object o, @Nullable String nullDefault) {
+ public static @Nullable String toString(@Nullable Object o, @Nullable String nullDefault) {
return (o != null) ? o.toString() : nullDefault;
}
@@ -117,8 +116,7 @@
* @return {@code obj} if not {@code null}
* @throws NullPointerException if {@code obj} is {@code null}
*/
- @NonNull
- public static <T> T requireNonNull(@Nullable T obj) {
+ public static <T> @NonNull T requireNonNull(@Nullable T obj) {
if (obj == null) throw new NullPointerException();
return obj;
}
@@ -142,8 +140,7 @@
* @return {@code obj} if not {@code null}
* @throws NullPointerException if {@code obj} is {@code null}
*/
- @NonNull
- public static <T> T requireNonNull(@Nullable T obj, @NonNull String message) {
+ public static <T> @NonNull T requireNonNull(@Nullable T obj, @NonNull String message) {
if (obj == null) throw new NullPointerException(message);
return obj;
}
diff --git a/core/core/src/main/java/androidx/core/util/Pair.java b/core/core/src/main/java/androidx/core/util/Pair.java
index 00840d5..75dbc53 100644
--- a/core/core/src/main/java/androidx/core/util/Pair.java
+++ b/core/core/src/main/java/androidx/core/util/Pair.java
@@ -16,7 +16,7 @@
package androidx.core.util;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Container to ease passing around a tuple of two objects. This object provides a sensible
@@ -66,9 +66,8 @@
return (first == null ? 0 : first.hashCode()) ^ (second == null ? 0 : second.hashCode());
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
return "Pair{" + first + " " + second + "}";
}
@@ -78,9 +77,8 @@
* @param b the second object in the pair
* @return a Pair that is templatized with the types of a and b
*/
- @NonNull
@SuppressWarnings("UnknownNullness") // Generic nullness should come from type annotations.
- public static <A, B> Pair<A, B> create(A a, B b) {
+ public static <A, B> @NonNull Pair<A, B> create(A a, B b) {
return new Pair<>(a, b);
}
}
diff --git a/core/core/src/main/java/androidx/core/util/Preconditions.java b/core/core/src/main/java/androidx/core/util/Preconditions.java
index a975b9e7..aeeb020 100644
--- a/core/core/src/main/java/androidx/core/util/Preconditions.java
+++ b/core/core/src/main/java/androidx/core/util/Preconditions.java
@@ -19,10 +19,11 @@
import android.text.TextUtils;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.Locale;
/**
@@ -64,7 +65,7 @@
public static void checkArgument(
final boolean expression,
final @NonNull String messageTemplate,
- final @NonNull Object... messageArgs) {
+ final Object @NonNull ... messageArgs) {
if (!expression) {
throw new IllegalArgumentException(String.format(messageTemplate, messageArgs));
}
@@ -78,8 +79,8 @@
* @return the string reference that was validated
* @throws IllegalArgumentException if {@code string} is empty
*/
- public static @NonNull <T extends CharSequence> T checkStringNotEmpty(
- @Nullable final T string) {
+ public static <T extends CharSequence> @NonNull T checkStringNotEmpty(
+ final @Nullable T string) {
if (TextUtils.isEmpty(string)) {
throw new IllegalArgumentException();
}
@@ -96,8 +97,8 @@
* @return the string reference that was validated
* @throws IllegalArgumentException if {@code string} is empty
*/
- public static @NonNull <T extends CharSequence> T checkStringNotEmpty(
- @Nullable final T string, @NonNull final Object errorMessage) {
+ public static <T extends CharSequence> @NonNull T checkStringNotEmpty(
+ final @Nullable T string, final @NonNull Object errorMessage) {
if (TextUtils.isEmpty(string)) {
throw new IllegalArgumentException(String.valueOf(errorMessage));
}
@@ -114,9 +115,9 @@
* @return the string reference that was validated
* @throws IllegalArgumentException if {@code string} is empty
*/
- public static @NonNull <T extends CharSequence> T checkStringNotEmpty(
- @Nullable final T string, @NonNull final String messageTemplate,
- @NonNull final Object... messageArgs) {
+ public static <T extends CharSequence> @NonNull T checkStringNotEmpty(
+ final @Nullable T string, final @NonNull String messageTemplate,
+ final Object @NonNull ... messageArgs) {
if (TextUtils.isEmpty(string)) {
throw new IllegalArgumentException(String.format(messageTemplate, messageArgs));
}
@@ -131,7 +132,7 @@
* @return the non-null reference that was validated
* @throws NullPointerException if {@code reference} is null
*/
- public static @NonNull <T> T checkNotNull(@Nullable T reference) {
+ public static <T> @NonNull T checkNotNull(@Nullable T reference) {
if (reference == null) {
throw new NullPointerException();
}
@@ -148,7 +149,7 @@
* @return the non-null reference that was validated
* @throws NullPointerException if {@code reference} is null
*/
- public static @NonNull <T> T checkNotNull(@Nullable T reference, @NonNull Object errorMessage) {
+ public static <T> @NonNull T checkNotNull(@Nullable T reference, @NonNull Object errorMessage) {
if (reference == null) {
throw new NullPointerException(String.valueOf(errorMessage));
}
diff --git a/core/core/src/main/java/androidx/core/util/SizeFCompat.java b/core/core/src/main/java/androidx/core/util/SizeFCompat.java
index 4c6e307..e201b9e 100644
--- a/core/core/src/main/java/androidx/core/util/SizeFCompat.java
+++ b/core/core/src/main/java/androidx/core/util/SizeFCompat.java
@@ -18,9 +18,10 @@
import android.util.SizeF;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
/**
* Immutable class for describing width and height dimensions in some arbitrary unit. Width and
* height are finite values stored as a floating point representation.
@@ -66,36 +67,31 @@
return Float.floatToIntBits(mWidth) ^ Float.floatToIntBits(mHeight);
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
return mWidth + "x" + mHeight;
}
/** Converts this {@link SizeFCompat} into a {@link SizeF}. */
@RequiresApi(21)
- @NonNull
- public SizeF toSizeF() {
+ public @NonNull SizeF toSizeF() {
return Api21Impl.toSizeF(this);
}
/** Converts this {@link SizeF} into a {@link SizeFCompat}. */
@RequiresApi(21)
- @NonNull
- public static SizeFCompat toSizeFCompat(@NonNull SizeF size) {
+ public static @NonNull SizeFCompat toSizeFCompat(@NonNull SizeF size) {
return Api21Impl.toSizeFCompat(size);
}
@RequiresApi(21)
private static final class Api21Impl {
- @NonNull
- static SizeFCompat toSizeFCompat(@NonNull SizeF size) {
+ static @NonNull SizeFCompat toSizeFCompat(@NonNull SizeF size) {
Preconditions.checkNotNull(size);
return new SizeFCompat(size.getWidth(), size.getHeight());
}
- @NonNull
- static SizeF toSizeF(@NonNull SizeFCompat size) {
+ static @NonNull SizeF toSizeF(@NonNull SizeFCompat size) {
Preconditions.checkNotNull(size);
return new SizeF(size.getWidth(), size.getHeight());
}
diff --git a/core/core/src/main/java/androidx/core/util/TypedValueCompat.java b/core/core/src/main/java/androidx/core/util/TypedValueCompat.java
index 614f50a..4f8ec11 100644
--- a/core/core/src/main/java/androidx/core/util/TypedValueCompat.java
+++ b/core/core/src/main/java/androidx/core/util/TypedValueCompat.java
@@ -29,10 +29,11 @@
import android.util.TypedValue;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
diff --git a/core/core/src/main/java/androidx/core/view/AccessibilityDelegateCompat.java b/core/core/src/main/java/androidx/core/view/AccessibilityDelegateCompat.java
index da8f831..1794741 100644
--- a/core/core/src/main/java/androidx/core/view/AccessibilityDelegateCompat.java
+++ b/core/core/src/main/java/androidx/core/view/AccessibilityDelegateCompat.java
@@ -30,8 +30,6 @@
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.R;
import androidx.core.view.accessibility.AccessibilityClickableSpanCompat;
@@ -39,6 +37,9 @@
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.List;
@@ -320,8 +321,8 @@
*
* @see AccessibilityNodeProviderCompat
*/
- @Nullable
- public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(@NonNull View host) {
+ public @Nullable AccessibilityNodeProviderCompat getAccessibilityNodeProvider(
+ @NonNull View host) {
Object provider = mOriginalDelegate.getAccessibilityNodeProvider(host);
if (provider != null) {
return new AccessibilityNodeProviderCompat(provider);
diff --git a/core/core/src/main/java/androidx/core/view/ActionProvider.java b/core/core/src/main/java/androidx/core/view/ActionProvider.java
index 341375b..dfa698d 100644
--- a/core/core/src/main/java/androidx/core/view/ActionProvider.java
+++ b/core/core/src/main/java/androidx/core/view/ActionProvider.java
@@ -24,10 +24,11 @@
import android.view.SubMenu;
import android.view.View;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* This class is a mediator for accomplishing a given task, for example sharing a file. It is
* responsible for creating a view that performs an action that accomplishes the task. This class
@@ -139,8 +140,7 @@
/**
* Gets the context associated with this action provider.
*/
- @NonNull
- public Context getContext() {
+ public @NonNull Context getContext() {
return mContext;
}
@@ -149,8 +149,7 @@
*
* @return A new action view.
*/
- @NonNull
- public abstract View onCreateActionView();
+ public abstract @NonNull View onCreateActionView();
/**
* Factory method called by the Android framework to create new action views.
@@ -165,8 +164,7 @@
* @return the new action view
*/
@SuppressWarnings("unused")
- @NonNull
- public View onCreateActionView(@NonNull MenuItem forItem) {
+ public @NonNull View onCreateActionView(@NonNull MenuItem forItem) {
return onCreateActionView();
}
diff --git a/core/core/src/main/java/androidx/core/view/ContentInfoCompat.java b/core/core/src/main/java/androidx/core/view/ContentInfoCompat.java
index f789419..0c0a190 100644
--- a/core/core/src/main/java/androidx/core/view/ContentInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ContentInfoCompat.java
@@ -25,12 +25,13 @@
import android.view.ContentInfo;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
@@ -98,8 +99,7 @@
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
- @NonNull
- static String sourceToString(@Source int source) {
+ static @NonNull String sourceToString(@Source int source) {
switch (source) {
case SOURCE_APP: return "SOURCE_APP";
case SOURCE_CLIPBOARD: return "SOURCE_CLIPBOARD";
@@ -132,16 +132,14 @@
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
- @NonNull
- static String flagsToString(@Flags int flags) {
+ static @NonNull String flagsToString(@Flags int flags) {
if ((flags & FLAG_CONVERT_TO_PLAIN_TEXT) != 0) {
return "FLAG_CONVERT_TO_PLAIN_TEXT";
}
return String.valueOf(flags);
}
- @NonNull
- private final Compat mCompat;
+ private final @NonNull Compat mCompat;
ContentInfoCompat(@NonNull Compat compat) {
mCompat = compat;
@@ -157,8 +155,8 @@
* @return wrapped class
*/
@RequiresApi(31)
- @NonNull
- public static ContentInfoCompat toContentInfoCompat(@NonNull ContentInfo platContentInfo) {
+ public static @NonNull ContentInfoCompat toContentInfoCompat(
+ @NonNull ContentInfo platContentInfo) {
return new ContentInfoCompat(new Compat31Impl(platContentInfo));
}
@@ -172,22 +170,19 @@
* @see ContentInfoCompat#toContentInfoCompat
*/
@RequiresApi(31)
- @NonNull
- public ContentInfo toContentInfo() {
+ public @NonNull ContentInfo toContentInfo() {
return Objects.requireNonNull(mCompat.getWrapped());
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
return mCompat.toString();
}
/**
* The data to be inserted.
*/
- @NonNull
- public ClipData getClip() {
+ public @NonNull ClipData getClip() {
return mCompat.getClip();
}
@@ -214,8 +209,7 @@
* {@link android.view.inputmethod.InputContentInfo#getLinkUri linkUri} was passed by the
* IME.
*/
- @Nullable
- public Uri getLinkUri() {
+ public @Nullable Uri getLinkUri() {
return mCompat.getLinkUri();
}
@@ -224,8 +218,7 @@
* include the {@link android.view.inputmethod.InputConnection#commitContent opts} passed by
* the IME.
*/
- @Nullable
- public Bundle getExtras() {
+ public @Nullable Bundle getExtras() {
return mCompat.getExtras();
}
@@ -245,9 +238,8 @@
* second object will have the content that didn't match the predicate, or null if all of
* the items matched.
*/
- @NonNull
- public Pair<ContentInfoCompat, ContentInfoCompat> partition(
- @NonNull androidx.core.util.Predicate<ClipData.Item> itemPredicate) {
+ public @NonNull Pair<ContentInfoCompat, ContentInfoCompat> partition(
+ androidx.core.util.@NonNull Predicate<ClipData.Item> itemPredicate) {
ClipData clip = mCompat.getClip();
if (clip.getItemCount() == 1) {
boolean matched = itemPredicate.test(clip.getItemAt(0));
@@ -264,9 +256,8 @@
new ContentInfoCompat.Builder(this).setClip(split.second).build());
}
- @NonNull
- static Pair<ClipData, ClipData> partition(@NonNull ClipData clip,
- @NonNull androidx.core.util.Predicate<ClipData.Item> itemPredicate) {
+ static @NonNull Pair<ClipData, ClipData> partition(@NonNull ClipData clip,
+ androidx.core.util.@NonNull Predicate<ClipData.Item> itemPredicate) {
ArrayList<ClipData.Item> acceptedItems = null;
ArrayList<ClipData.Item> remainingItems = null;
for (int i = 0; i < clip.getItemCount(); i++) {
@@ -290,8 +281,7 @@
buildClipData(clip.getDescription(), remainingItems));
}
- @NonNull
- static ClipData buildClipData(@NonNull ClipDescription description,
+ static @NonNull ClipData buildClipData(@NonNull ClipDescription description,
@NonNull List<ClipData.Item> items) {
ClipData clip = new ClipData(new ClipDescription(description), items.get(0));
for (int i = 1; i < items.size(); i++) {
@@ -318,8 +308,7 @@
* the items matched.
*/
@RequiresApi(31)
- @NonNull
- public static Pair<ContentInfo, ContentInfo> partition(@NonNull ContentInfo payload,
+ public static @NonNull Pair<ContentInfo, ContentInfo> partition(@NonNull ContentInfo payload,
@NonNull Predicate<ClipData.Item> itemPredicate) {
return Api31Impl.partition(payload, itemPredicate);
}
@@ -328,9 +317,8 @@
private static final class Api31Impl {
private Api31Impl() {}
- @NonNull
- public static Pair<ContentInfo, ContentInfo> partition(@NonNull ContentInfo payload,
- @NonNull Predicate<ClipData.Item> itemPredicate) {
+ public static @NonNull Pair<ContentInfo, ContentInfo> partition(
+ @NonNull ContentInfo payload, @NonNull Predicate<ClipData.Item> itemPredicate) {
ClipData clip = payload.getClip();
if (clip.getItemCount() == 1) {
boolean matched = itemPredicate.test(clip.getItemAt(0));
@@ -349,31 +337,24 @@
}
private interface Compat {
- @Nullable
- ContentInfo getWrapped();
- @NonNull
- ClipData getClip();
+ @Nullable ContentInfo getWrapped();
+ @NonNull ClipData getClip();
@Source
int getSource();
@Flags
int getFlags();
- @Nullable
- Uri getLinkUri();
- @Nullable
- Bundle getExtras();
+ @Nullable Uri getLinkUri();
+ @Nullable Bundle getExtras();
}
private static final class CompatImpl implements Compat {
- @NonNull
- private final ClipData mClip;
+ private final @NonNull ClipData mClip;
@Source
private final int mSource;
@Flags
private final int mFlags;
- @Nullable
- private final Uri mLinkUri;
- @Nullable
- private final Bundle mExtras;
+ private final @Nullable Uri mLinkUri;
+ private final @Nullable Bundle mExtras;
CompatImpl(BuilderCompatImpl b) {
mClip = Preconditions.checkNotNull(b.mClip);
@@ -384,15 +365,13 @@
mExtras = b.mExtras;
}
- @Nullable
@Override
- public ContentInfo getWrapped() {
+ public @Nullable ContentInfo getWrapped() {
return null;
}
- @NonNull
@Override
- public ClipData getClip() {
+ public @NonNull ClipData getClip() {
return mClip;
}
@@ -408,21 +387,18 @@
return mFlags;
}
- @Nullable
@Override
- public Uri getLinkUri() {
+ public @Nullable Uri getLinkUri() {
return mLinkUri;
}
- @Nullable
@Override
- public Bundle getExtras() {
+ public @Nullable Bundle getExtras() {
return mExtras;
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
return "ContentInfoCompat{"
+ "clip=" + mClip.getDescription()
+ ", source=" + sourceToString(mSource)
@@ -435,22 +411,19 @@
@RequiresApi(31)
private static final class Compat31Impl implements Compat {
- @NonNull
- private final ContentInfo mWrapped;
+ private final @NonNull ContentInfo mWrapped;
Compat31Impl(@NonNull ContentInfo wrapped) {
mWrapped = Preconditions.checkNotNull(wrapped);
}
- @NonNull
@Override
- public ContentInfo getWrapped() {
+ public @NonNull ContentInfo getWrapped() {
return mWrapped;
}
- @NonNull
@Override
- public ClipData getClip() {
+ public @NonNull ClipData getClip() {
return mWrapped.getClip();
}
@@ -466,21 +439,18 @@
return mWrapped.getFlags();
}
- @Nullable
@Override
- public Uri getLinkUri() {
+ public @Nullable Uri getLinkUri() {
return mWrapped.getLinkUri();
}
- @Nullable
@Override
- public Bundle getExtras() {
+ public @Nullable Bundle getExtras() {
return mWrapped.getExtras();
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
return "ContentInfoCompat{" + mWrapped + "}";
}
}
@@ -489,8 +459,7 @@
* Builder for {@link ContentInfoCompat}.
*/
public static final class Builder {
- @NonNull
- private final BuilderCompat mBuilderCompat;
+ private final @NonNull BuilderCompat mBuilderCompat;
/**
* Creates a new builder initialized with the data from the given object (shallow copy).
@@ -523,8 +492,7 @@
* @param clip The data to insert.
* @return this builder
*/
- @NonNull
- public Builder setClip(@NonNull ClipData clip) {
+ public @NonNull Builder setClip(@NonNull ClipData clip) {
mBuilderCompat.setClip(clip);
return this;
}
@@ -535,8 +503,7 @@
* @param source The source of the operation. See {@code SOURCE_} constants.
* @return this builder
*/
- @NonNull
- public Builder setSource(@Source int source) {
+ public @NonNull Builder setSource(@Source int source) {
mBuilderCompat.setSource(source);
return this;
}
@@ -548,8 +515,7 @@
* behavior. See {@code FLAG_} constants.
* @return this builder
*/
- @NonNull
- public Builder setFlags(@Flags int flags) {
+ public @NonNull Builder setFlags(@Flags int flags) {
mBuilderCompat.setFlags(flags);
return this;
}
@@ -561,8 +527,7 @@
* @param linkUri Optional http/https URI for the content.
* @return this builder
*/
- @NonNull
- public Builder setLinkUri(@Nullable Uri linkUri) {
+ public @NonNull Builder setLinkUri(@Nullable Uri linkUri) {
mBuilderCompat.setLinkUri(linkUri);
return this;
}
@@ -573,8 +538,7 @@
* @param extras Optional bundle with additional metadata.
* @return this builder
*/
- @NonNull
- public Builder setExtras(@Nullable Bundle extras) {
+ public @NonNull Builder setExtras(@Nullable Bundle extras) {
mBuilderCompat.setExtras(extras);
return this;
}
@@ -582,8 +546,7 @@
/**
* @return A new {@link ContentInfoCompat} instance with the data from this builder.
*/
- @NonNull
- public ContentInfoCompat build() {
+ public @NonNull ContentInfoCompat build() {
return mBuilderCompat.build();
}
}
@@ -594,21 +557,17 @@
void setFlags(@Flags int flags);
void setLinkUri(@Nullable Uri linkUri);
void setExtras(@Nullable Bundle extras);
- @NonNull
- ContentInfoCompat build();
+ @NonNull ContentInfoCompat build();
}
private static final class BuilderCompatImpl implements BuilderCompat {
- @NonNull
- ClipData mClip;
+ @NonNull ClipData mClip;
@Source
int mSource;
@Flags
int mFlags;
- @Nullable
- Uri mLinkUri;
- @Nullable
- Bundle mExtras;
+ @Nullable Uri mLinkUri;
+ @Nullable Bundle mExtras;
BuilderCompatImpl(@NonNull ClipData clip, int source) {
mClip = clip;
@@ -649,16 +608,14 @@
}
@Override
- @NonNull
- public ContentInfoCompat build() {
+ public @NonNull ContentInfoCompat build() {
return new ContentInfoCompat(new CompatImpl(this));
}
}
@RequiresApi(31)
private static final class BuilderCompat31Impl implements BuilderCompat {
- @NonNull
- private final ContentInfo.Builder mPlatformBuilder;
+ private final ContentInfo.@NonNull Builder mPlatformBuilder;
BuilderCompat31Impl(@NonNull ClipData clip, int source) {
mPlatformBuilder = new ContentInfo.Builder(clip, source);
@@ -693,9 +650,8 @@
mPlatformBuilder.setExtras(extras);
}
- @NonNull
@Override
- public ContentInfoCompat build() {
+ public @NonNull ContentInfoCompat build() {
return new ContentInfoCompat(new Compat31Impl(mPlatformBuilder.build()));
}
}
diff --git a/core/core/src/main/java/androidx/core/view/DifferentialMotionFlingController.java b/core/core/src/main/java/androidx/core/view/DifferentialMotionFlingController.java
index 335d66c..86a7517 100644
--- a/core/core/src/main/java/androidx/core/view/DifferentialMotionFlingController.java
+++ b/core/core/src/main/java/androidx/core/view/DifferentialMotionFlingController.java
@@ -21,10 +21,11 @@
import android.view.VelocityTracker;
import android.view.ViewConfiguration;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Controller differential motion flings.
*
@@ -49,7 +50,7 @@
private final FlingVelocityThresholdCalculator mVelocityThresholdCalculator;
private final DifferentialVelocityProvider mVelocityProvider;
- @Nullable private VelocityTracker mVelocityTracker;
+ private @Nullable VelocityTracker mVelocityTracker;
private float mLastFlingVelocity;
diff --git a/core/core/src/main/java/androidx/core/view/DisplayCompat.java b/core/core/src/main/java/androidx/core/view/DisplayCompat.java
index 6629da5..e6c4ea4 100644
--- a/core/core/src/main/java/androidx/core/view/DisplayCompat.java
+++ b/core/core/src/main/java/androidx/core/view/DisplayCompat.java
@@ -27,11 +27,12 @@
import android.text.TextUtils;
import android.view.Display;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.reflect.Method;
/**
@@ -55,8 +56,7 @@
* Gets the current display mode of the given display, where the size can be relied on to
* determine support for 4k on Android TV devices.
*/
- @NonNull
- public static ModeCompat getMode(@NonNull Context context, @NonNull Display display) {
+ public static @NonNull ModeCompat getMode(@NonNull Context context, @NonNull Display display) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return Api23Impl.getMode(context, display);
}
@@ -65,8 +65,8 @@
return new ModeCompat(getDisplaySize(context, display));
}
- @NonNull
- private static Point getDisplaySize(@NonNull Context context, @NonNull Display display) {
+ private static @NonNull Point getDisplaySize(@NonNull Context context,
+ @NonNull Display display) {
// If a workaround for the display size is present, use it.
Point displaySize = getCurrentDisplaySizeFromWorkarounds(context, display);
if (displaySize != null) {
@@ -82,9 +82,8 @@
* Gets the supported modes of the given display where any mode with the same size as the
* current mode can be relied on to determine support for 4k on Android TV devices.
*/
- @NonNull
@SuppressLint("ArrayReturn")
- public static ModeCompat[] getSupportedModes(
+ public static ModeCompat @NonNull [] getSupportedModes(
@NonNull Context context, @NonNull Display display) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return Api23Impl.getSupportedModes(context, display);
@@ -122,8 +121,7 @@
* @param name the name of the system property
* @return the result string or null if an exception occurred
*/
- @Nullable
- private static String getSystemProperty(String name) {
+ private static @Nullable String getSystemProperty(String name) {
try {
@SuppressLint("PrivateApi")
Class<?> systemProperties = Class.forName("android.os.SystemProperties");
@@ -153,9 +151,8 @@
*
* @return the physical display size, in pixels or null if the information is not available
*/
- @Nullable
- private static Point parsePhysicalDisplaySizeFromSystemProperties(@NonNull String property,
- @NonNull Display display) {
+ private static @Nullable Point parsePhysicalDisplaySizeFromSystemProperties(
+ @NonNull String property, @NonNull Display display) {
// System properties are only relevant for the default display.
if (display.getDisplayId() != Display.DEFAULT_DISPLAY) {
return null;
@@ -228,8 +225,7 @@
static class Api23Impl {
private Api23Impl() {}
- @NonNull
- static ModeCompat getMode(@NonNull Context context, @NonNull Display display) {
+ static @NonNull ModeCompat getMode(@NonNull Context context, @NonNull Display display) {
Display.Mode currentMode = display.getMode();
Point workaroundSize = getCurrentDisplaySizeFromWorkarounds(context, display);
// If the current mode has the wrong physical size, then correct it with the
@@ -239,9 +235,8 @@
: new ModeCompat(currentMode, workaroundSize);
}
- @NonNull
@SuppressLint("ArrayReturn")
- public static ModeCompat[] getSupportedModes(
+ public static ModeCompat @NonNull [] getSupportedModes(
@NonNull Context context, @NonNull Display display) {
Display.Mode[] supportedModes = display.getSupportedModes();
ModeCompat[] supportedModesCompat = new ModeCompat[supportedModes.length];
@@ -327,7 +322,7 @@
* @param mode the wrapped Display.Mode object
*/
@RequiresApi(Build.VERSION_CODES.M)
- ModeCompat(@NonNull Display.Mode mode, boolean isNative) {
+ ModeCompat(Display.@NonNull Mode mode, boolean isNative) {
Preconditions.checkNotNull(mode, "mode == null, can't wrap a null reference");
// This simplifies the getPhysicalWidth() / getPhysicalHeight functions below
mPhysicalSize = new Point(Api23Impl.getPhysicalWidth(mode),
@@ -345,7 +340,7 @@
*
*/
@RequiresApi(Build.VERSION_CODES.M)
- ModeCompat(@NonNull Display.Mode mode, @NonNull Point physicalSize) {
+ ModeCompat(Display.@NonNull Mode mode, @NonNull Point physicalSize) {
Preconditions.checkNotNull(mode, "mode == null, can't wrap a null reference");
Preconditions.checkNotNull(physicalSize, "physicalSize == null");
mPhysicalSize = physicalSize;
@@ -385,8 +380,7 @@
* Returns the wrapped object Display.Mode, which may be null if no mode is available.
*/
@RequiresApi(Build.VERSION_CODES.M)
- @Nullable
- public Display.Mode toMode() {
+ public Display.@Nullable Mode toMode() {
return mMode;
}
diff --git a/core/core/src/main/java/androidx/core/view/DisplayCutoutCompat.java b/core/core/src/main/java/androidx/core/view/DisplayCutoutCompat.java
index 0c4bd43..474fcff 100644
--- a/core/core/src/main/java/androidx/core/view/DisplayCutoutCompat.java
+++ b/core/core/src/main/java/androidx/core/view/DisplayCutoutCompat.java
@@ -23,17 +23,17 @@
import android.os.Build;
import android.view.DisplayCutout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.graphics.Insets;
import androidx.core.util.ObjectsCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-
/**
* Represents the area of the display that is not functional for displaying content.
*
@@ -187,8 +187,7 @@
*
* @return a list of bounding {@code Rect}s, one for each display cutout area.
*/
- @NonNull
- public List<Rect> getBoundingRects() {
+ public @NonNull List<Rect> getBoundingRects() {
if (SDK_INT >= 28) {
return Api28Impl.getBoundingRects(mDisplayCutout);
} else {
@@ -206,8 +205,7 @@
* @return the insets for the curved areas of a waterfall display in pixels or {@code
* Insets.NONE} if there are no curved areas or they don't overlap with the window.
*/
- @NonNull
- public Insets getWaterfallInsets() {
+ public @NonNull Insets getWaterfallInsets() {
if (SDK_INT >= 30) {
return Insets.toCompatInsets(Api30Impl.getWaterfallInsets(mDisplayCutout));
} else {
@@ -223,8 +221,7 @@
*
* @return the path corresponding to the cutout, or null if there is no cutout on the display.
*/
- @Nullable
- public Path getCutoutPath() {
+ public @Nullable Path getCutoutPath() {
if (SDK_INT >= 31) {
return Api31Impl.getCutoutPath(mDisplayCutout);
} else {
@@ -249,9 +246,8 @@
return mDisplayCutout == null ? 0 : mDisplayCutout.hashCode();
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
return "DisplayCutoutCompat{" + mDisplayCutout + "}";
}
@@ -331,8 +327,7 @@
// This class is not instantiable.
}
- @Nullable
- static Path getCutoutPath(DisplayCutout displayCutout) {
+ static @Nullable Path getCutoutPath(DisplayCutout displayCutout) {
return displayCutout.getCutoutPath();
}
}
diff --git a/core/core/src/main/java/androidx/core/view/DragAndDropPermissionsCompat.java b/core/core/src/main/java/androidx/core/view/DragAndDropPermissionsCompat.java
index 316da25..7e65fbe 100644
--- a/core/core/src/main/java/androidx/core/view/DragAndDropPermissionsCompat.java
+++ b/core/core/src/main/java/androidx/core/view/DragAndDropPermissionsCompat.java
@@ -23,11 +23,12 @@
import android.view.DragAndDropPermissions;
import android.view.DragEvent;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Helper for accessing features in {@link DragAndDropPermissions} a backwards
* compatible fashion.
@@ -46,8 +47,7 @@
}
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @Nullable
- public static DragAndDropPermissionsCompat request(@NonNull Activity activity,
+ public static @Nullable DragAndDropPermissionsCompat request(@NonNull Activity activity,
@NonNull DragEvent dragEvent) {
if (Build.VERSION.SDK_INT >= 24) {
DragAndDropPermissions dragAndDropPermissions =
diff --git a/core/core/src/main/java/androidx/core/view/DragStartHelper.java b/core/core/src/main/java/androidx/core/view/DragStartHelper.java
index 6d86d81..37f1be2 100644
--- a/core/core/src/main/java/androidx/core/view/DragStartHelper.java
+++ b/core/core/src/main/java/androidx/core/view/DragStartHelper.java
@@ -21,7 +21,7 @@
import android.view.MotionEvent;
import android.view.View;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* DragStartHelper is a utility class for implementing drag and drop support.
diff --git a/core/core/src/main/java/androidx/core/view/GestureDetectorCompat.java b/core/core/src/main/java/androidx/core/view/GestureDetectorCompat.java
index 1d68424..56843f8 100644
--- a/core/core/src/main/java/androidx/core/view/GestureDetectorCompat.java
+++ b/core/core/src/main/java/androidx/core/view/GestureDetectorCompat.java
@@ -25,8 +25,8 @@
import android.view.MotionEvent;
import android.view.View;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* Detects various gestures and events using the supplied {@link MotionEvent}s.
diff --git a/core/core/src/main/java/androidx/core/view/GravityCompat.java b/core/core/src/main/java/androidx/core/view/GravityCompat.java
index b7e1215..af4fada 100644
--- a/core/core/src/main/java/androidx/core/view/GravityCompat.java
+++ b/core/core/src/main/java/androidx/core/view/GravityCompat.java
@@ -20,7 +20,7 @@
import android.graphics.Rect;
import android.view.Gravity;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Compatibility shim for accessing newer functionality from {@link Gravity}.
diff --git a/core/core/src/main/java/androidx/core/view/KeyEventDispatcher.java b/core/core/src/main/java/androidx/core/view/KeyEventDispatcher.java
index 5605d46..511b1ad 100644
--- a/core/core/src/main/java/androidx/core/view/KeyEventDispatcher.java
+++ b/core/core/src/main/java/androidx/core/view/KeyEventDispatcher.java
@@ -26,10 +26,11 @@
import android.view.View;
import android.view.Window;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -77,7 +78,7 @@
*/
@SuppressLint("LambdaLast")
public static boolean dispatchKeyEvent(@NonNull Component component,
- @Nullable View root, @Nullable Window.Callback callback, @NonNull KeyEvent event) {
+ @Nullable View root, Window.@Nullable Callback callback, @NonNull KeyEvent event) {
if (component == null) {
return false;
}
diff --git a/core/core/src/main/java/androidx/core/view/LayoutInflaterCompat.java b/core/core/src/main/java/androidx/core/view/LayoutInflaterCompat.java
index 667cadd..26358e21 100644
--- a/core/core/src/main/java/androidx/core/view/LayoutInflaterCompat.java
+++ b/core/core/src/main/java/androidx/core/view/LayoutInflaterCompat.java
@@ -23,7 +23,7 @@
import android.view.LayoutInflater;
import android.view.View;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
import java.lang.reflect.Field;
@@ -56,9 +56,8 @@
return mDelegateFactory.onCreateView(parent, name, context, attributeSet);
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
return getClass().getName() + "{" + mDelegateFactory + "}";
}
}
@@ -137,7 +136,7 @@
* @see LayoutInflater#setFactory2(android.view.LayoutInflater.Factory2)
*/
public static void setFactory2(
- @NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) {
+ @NonNull LayoutInflater inflater, LayoutInflater.@NonNull Factory2 factory) {
inflater.setFactory2(factory);
if (Build.VERSION.SDK_INT < 21) {
diff --git a/core/core/src/main/java/androidx/core/view/MarginLayoutParamsCompat.java b/core/core/src/main/java/androidx/core/view/MarginLayoutParamsCompat.java
index 876ca0b..b651f3f 100644
--- a/core/core/src/main/java/androidx/core/view/MarginLayoutParamsCompat.java
+++ b/core/core/src/main/java/androidx/core/view/MarginLayoutParamsCompat.java
@@ -20,7 +20,7 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Helper for accessing API features in
@@ -45,7 +45,7 @@
*/
@androidx.annotation.ReplaceWith(expression = "lp.getMarginStart()")
@Deprecated
- public static int getMarginStart(@NonNull ViewGroup.MarginLayoutParams lp) {
+ public static int getMarginStart(ViewGroup.@NonNull MarginLayoutParams lp) {
return lp.getMarginStart();
}
@@ -63,7 +63,7 @@
*/
@androidx.annotation.ReplaceWith(expression = "lp.getMarginEnd()")
@Deprecated
- public static int getMarginEnd(@NonNull ViewGroup.MarginLayoutParams lp) {
+ public static int getMarginEnd(ViewGroup.@NonNull MarginLayoutParams lp) {
return lp.getMarginEnd();
}
@@ -81,7 +81,7 @@
*/
@androidx.annotation.ReplaceWith(expression = "lp.setMarginStart(marginStart)")
@Deprecated
- public static void setMarginStart(@NonNull ViewGroup.MarginLayoutParams lp, int marginStart) {
+ public static void setMarginStart(ViewGroup.@NonNull MarginLayoutParams lp, int marginStart) {
lp.setMarginStart(marginStart);
}
@@ -99,7 +99,7 @@
*/
@androidx.annotation.ReplaceWith(expression = "lp.setMarginEnd(marginEnd)")
@Deprecated
- public static void setMarginEnd(@NonNull ViewGroup.MarginLayoutParams lp, int marginEnd) {
+ public static void setMarginEnd(ViewGroup.@NonNull MarginLayoutParams lp, int marginEnd) {
lp.setMarginEnd(marginEnd);
}
@@ -111,7 +111,7 @@
*/
@androidx.annotation.ReplaceWith(expression = "lp.isMarginRelative()")
@Deprecated
- public static boolean isMarginRelative(@NonNull ViewGroup.MarginLayoutParams lp) {
+ public static boolean isMarginRelative(ViewGroup.@NonNull MarginLayoutParams lp) {
return lp.isMarginRelative();
}
@@ -123,7 +123,7 @@
* @deprecated Use {@link ViewGroup.MarginLayoutParams#getLayoutDirection} directly.
*/
@Deprecated
- public static int getLayoutDirection(@NonNull ViewGroup.MarginLayoutParams lp) {
+ public static int getLayoutDirection(ViewGroup.@NonNull MarginLayoutParams lp) {
int result;
result = lp.getLayoutDirection();
@@ -147,7 +147,7 @@
*/
@androidx.annotation.ReplaceWith(expression = "lp.setLayoutDirection(layoutDirection)")
@Deprecated
- public static void setLayoutDirection(@NonNull ViewGroup.MarginLayoutParams lp,
+ public static void setLayoutDirection(ViewGroup.@NonNull MarginLayoutParams lp,
int layoutDirection) {
lp.setLayoutDirection(layoutDirection);
}
@@ -160,7 +160,7 @@
*/
@androidx.annotation.ReplaceWith(expression = "lp.resolveLayoutDirection(layoutDirection)")
@Deprecated
- public static void resolveLayoutDirection(@NonNull ViewGroup.MarginLayoutParams lp,
+ public static void resolveLayoutDirection(ViewGroup.@NonNull MarginLayoutParams lp,
int layoutDirection) {
lp.resolveLayoutDirection(layoutDirection);
}
diff --git a/core/core/src/main/java/androidx/core/view/MenuCompat.java b/core/core/src/main/java/androidx/core/view/MenuCompat.java
index e8b7630..7f5caf9 100644
--- a/core/core/src/main/java/androidx/core/view/MenuCompat.java
+++ b/core/core/src/main/java/androidx/core/view/MenuCompat.java
@@ -20,10 +20,11 @@
import android.view.Menu;
import android.view.MenuItem;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.internal.view.SupportMenu;
+import org.jspecify.annotations.NonNull;
+
/**
* Helper for accessing features in {@link Menu}.
*/
diff --git a/core/core/src/main/java/androidx/core/view/MenuHost.java b/core/core/src/main/java/androidx/core/view/MenuHost.java
index fd4b1c3..be9839e 100644
--- a/core/core/src/main/java/androidx/core/view/MenuHost.java
+++ b/core/core/src/main/java/androidx/core/view/MenuHost.java
@@ -17,10 +17,11 @@
import android.annotation.SuppressLint;
-import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
+import org.jspecify.annotations.NonNull;
+
/**
* A class that allows you to host and
* keep track of {@link MenuProvider}s that will supply
@@ -64,7 +65,7 @@
*/
@SuppressLint("LambdaLast")
void addMenuProvider(@NonNull MenuProvider provider, @NonNull LifecycleOwner owner,
- @NonNull Lifecycle.State state);
+ Lifecycle.@NonNull State state);
/**
* Removes the given {@link MenuProvider} from this {@link MenuHost}.
diff --git a/core/core/src/main/java/androidx/core/view/MenuHostHelper.java b/core/core/src/main/java/androidx/core/view/MenuHostHelper.java
index e37bb83..702ba6c 100644
--- a/core/core/src/main/java/androidx/core/view/MenuHostHelper.java
+++ b/core/core/src/main/java/androidx/core/view/MenuHostHelper.java
@@ -21,11 +21,12 @@
import android.view.MenuInflater;
import android.view.MenuItem;
-import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
+import org.jspecify.annotations.NonNull;
+
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -171,7 +172,7 @@
*/
@SuppressLint("LambdaLast")
public void addMenuProvider(@NonNull MenuProvider provider, @NonNull LifecycleOwner owner,
- @NonNull Lifecycle.State state) {
+ Lifecycle.@NonNull State state) {
Lifecycle lifecycle = owner.getLifecycle();
LifecycleContainer lifecycleContainer = mProviderToLifecycleContainers.remove(provider);
if (lifecycleContainer != null) {
diff --git a/core/core/src/main/java/androidx/core/view/MenuItemCompat.java b/core/core/src/main/java/androidx/core/view/MenuItemCompat.java
index 5dbad58f..be6fc792 100644
--- a/core/core/src/main/java/androidx/core/view/MenuItemCompat.java
+++ b/core/core/src/main/java/androidx/core/view/MenuItemCompat.java
@@ -26,11 +26,12 @@
import android.view.MenuItem;
import android.view.View;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.internal.view.SupportMenuItem;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Helper for accessing features in {@link MenuItem}.
* <p class="note"><strong>Note:</strong> You cannot get an instance of this class. Instead,
@@ -211,8 +212,7 @@
*
* @see ActionProvider
*/
- @Nullable
- public static MenuItem setActionProvider(@NonNull MenuItem item,
+ public static @Nullable MenuItem setActionProvider(@NonNull MenuItem item,
@Nullable ActionProvider provider) {
if (item instanceof SupportMenuItem) {
return ((SupportMenuItem) item).setSupportActionProvider(provider);
@@ -230,8 +230,7 @@
* @see ActionProvider
* @see #setActionProvider(MenuItem, ActionProvider)
*/
- @Nullable
- public static ActionProvider getActionProvider(@NonNull MenuItem item) {
+ public static @Nullable ActionProvider getActionProvider(@NonNull MenuItem item) {
if (item instanceof SupportMenuItem) {
return ((SupportMenuItem) item).getSupportActionProvider();
}
@@ -347,9 +346,8 @@
*
* @return The content description.
*/
- @Nullable
@SuppressWarnings("RedundantCast")
- public static CharSequence getContentDescription(@NonNull MenuItem item) {
+ public static @Nullable CharSequence getContentDescription(@NonNull MenuItem item) {
if (item instanceof SupportMenuItem) {
// Cast required to target SupportMenuItem method declaration.
return ((SupportMenuItem) item).getContentDescription();
@@ -379,9 +377,8 @@
*
* @return The tooltip text.
*/
- @Nullable
@SuppressWarnings("RedundantCast")
- public static CharSequence getTooltipText(@NonNull MenuItem item) {
+ public static @Nullable CharSequence getTooltipText(@NonNull MenuItem item) {
if (item instanceof SupportMenuItem) {
// Cast required to target SupportMenuItem method declaration.
return ((SupportMenuItem) item).getTooltipText();
@@ -551,9 +548,8 @@
* @return the tint applied to the item's icon
* @see #setIconTintList(MenuItem, ColorStateList)
*/
- @Nullable
@SuppressWarnings("RedundantCast")
- public static ColorStateList getIconTintList(@NonNull MenuItem item) {
+ public static @Nullable ColorStateList getIconTintList(@NonNull MenuItem item) {
if (item instanceof SupportMenuItem) {
// Cast required to target SupportMenuItem method declaration.
return ((SupportMenuItem) item).getIconTintList();
@@ -575,7 +571,7 @@
* @see #setIconTintList(MenuItem, ColorStateList)
*/
@SuppressWarnings("RedundantCast")
- public static void setIconTintMode(@NonNull MenuItem item, @Nullable PorterDuff.Mode tintMode) {
+ public static void setIconTintMode(@NonNull MenuItem item, PorterDuff.@Nullable Mode tintMode) {
if (item instanceof SupportMenuItem) {
// Cast required to target SupportMenuItem method declaration.
((SupportMenuItem) item).setIconTintMode(tintMode);
@@ -590,9 +586,8 @@
* @return the blending mode used to apply the tint to the item's icon
* @see #setIconTintMode(MenuItem, PorterDuff.Mode)
*/
- @Nullable
@SuppressWarnings("RedundantCast")
- public static PorterDuff.Mode getIconTintMode(@NonNull MenuItem item) {
+ public static PorterDuff.@Nullable Mode getIconTintMode(@NonNull MenuItem item) {
if (item instanceof SupportMenuItem) {
// Cast required to target SupportMenuItem method declaration.
return ((SupportMenuItem) item).getIconTintMode();
diff --git a/core/core/src/main/java/androidx/core/view/MenuProvider.java b/core/core/src/main/java/androidx/core/view/MenuProvider.java
index 26e5cf6..f5d13f1 100644
--- a/core/core/src/main/java/androidx/core/view/MenuProvider.java
+++ b/core/core/src/main/java/androidx/core/view/MenuProvider.java
@@ -20,7 +20,7 @@
import android.view.MenuInflater;
import android.view.MenuItem;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Interface for indicating that a component will be supplying
diff --git a/core/core/src/main/java/androidx/core/view/MotionEventCompat.java b/core/core/src/main/java/androidx/core/view/MotionEventCompat.java
index bad4584..7e6250a6 100644
--- a/core/core/src/main/java/androidx/core/view/MotionEventCompat.java
+++ b/core/core/src/main/java/androidx/core/view/MotionEventCompat.java
@@ -18,7 +18,7 @@
import android.view.MotionEvent;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Helper for accessing features in {@link MotionEvent}.
diff --git a/core/core/src/main/java/androidx/core/view/NestedScrollingChild.java b/core/core/src/main/java/androidx/core/view/NestedScrollingChild.java
index ba616fe..8bab39b 100644
--- a/core/core/src/main/java/androidx/core/view/NestedScrollingChild.java
+++ b/core/core/src/main/java/androidx/core/view/NestedScrollingChild.java
@@ -23,9 +23,10 @@
import android.view.ViewConfiguration;
import android.view.ViewParent;
-import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat.ScrollAxis;
+import org.jspecify.annotations.Nullable;
+
/**
* This interface should be implemented by {@link android.view.View View} subclasses that wish
* to support dispatching nested scrolling operations to a cooperating parent
@@ -153,7 +154,7 @@
* @see #dispatchNestedPreScroll(int, int, int[], int[])
*/
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);
+ int dxUnconsumed, int dyUnconsumed, int @Nullable [] offsetInWindow);
/**
* Dispatch one step of a nested scroll in progress before this view consumes any portion of it.
@@ -174,8 +175,8 @@
* @return true if the parent consumed some or all of the scroll delta
* @see #dispatchNestedScroll(int, int, int, int, int[])
*/
- boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
- @Nullable int[] offsetInWindow);
+ boolean dispatchNestedPreScroll(int dx, int dy, int @Nullable [] consumed,
+ int @Nullable [] offsetInWindow);
/**
* Dispatch a fling to a nested scrolling parent.
diff --git a/core/core/src/main/java/androidx/core/view/NestedScrollingChild2.java b/core/core/src/main/java/androidx/core/view/NestedScrollingChild2.java
index 2da12a4..b6e2b54 100644
--- a/core/core/src/main/java/androidx/core/view/NestedScrollingChild2.java
+++ b/core/core/src/main/java/androidx/core/view/NestedScrollingChild2.java
@@ -21,10 +21,11 @@
import android.view.View;
import android.view.ViewParent;
-import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat.NestedScrollType;
import androidx.core.view.ViewCompat.ScrollAxis;
+import org.jspecify.annotations.Nullable;
+
/**
* This interface should be implemented by {@link View View} subclasses that wish
* to support dispatching nested scrolling operations to a cooperating parent
@@ -129,7 +130,7 @@
* @see #dispatchNestedPreScroll(int, int, int[], int[], int)
*/
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
+ int dxUnconsumed, int dyUnconsumed, int @Nullable [] offsetInWindow,
@NestedScrollType int type);
/**
@@ -152,6 +153,6 @@
* @return true if the parent consumed some or all of the scroll delta
* @see #dispatchNestedScroll(int, int, int, int, int[], int)
*/
- boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
- @Nullable int[] offsetInWindow, @NestedScrollType int type);
+ boolean dispatchNestedPreScroll(int dx, int dy, int @Nullable [] consumed,
+ int @Nullable [] offsetInWindow, @NestedScrollType int type);
}
diff --git a/core/core/src/main/java/androidx/core/view/NestedScrollingChild3.java b/core/core/src/main/java/androidx/core/view/NestedScrollingChild3.java
index dfce4ee..6a972c1 100644
--- a/core/core/src/main/java/androidx/core/view/NestedScrollingChild3.java
+++ b/core/core/src/main/java/androidx/core/view/NestedScrollingChild3.java
@@ -19,8 +19,8 @@
import android.view.View;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* This interface should be implemented by {@link View View} subclasses that wish
@@ -70,6 +70,6 @@
* @see NestedScrollingParent3#onNestedScroll(View, int, int, int, int, int, int[])
*/
void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
- @Nullable int[] offsetInWindow, @ViewCompat.NestedScrollType int type,
- @NonNull int[] consumed);
+ int @Nullable [] offsetInWindow, @ViewCompat.NestedScrollType int type,
+ int @NonNull [] consumed);
}
diff --git a/core/core/src/main/java/androidx/core/view/NestedScrollingChildHelper.java b/core/core/src/main/java/androidx/core/view/NestedScrollingChildHelper.java
index 652c503..9936b77 100644
--- a/core/core/src/main/java/androidx/core/view/NestedScrollingChildHelper.java
+++ b/core/core/src/main/java/androidx/core/view/NestedScrollingChildHelper.java
@@ -23,11 +23,12 @@
import android.view.View;
import android.view.ViewParent;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat.NestedScrollType;
import androidx.core.view.ViewCompat.ScrollAxis;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Helper class for implementing nested scrolling child views compatible with Android platform
* versions earlier than Android 5.0 Lollipop (API 21).
@@ -201,7 +202,7 @@
* @return <code>true</code> if the parent consumed any of the nested scroll distance
*/
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
+ int dxUnconsumed, int dyUnconsumed, int @Nullable [] offsetInWindow) {
return dispatchNestedScrollInternal(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow, TYPE_TOUCH, null);
}
@@ -215,7 +216,7 @@
* @return <code>true</code> if the parent consumed any of the nested scroll distance
*/
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
- int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type) {
+ int dyUnconsumed, int @Nullable [] offsetInWindow, @NestedScrollType int type) {
return dispatchNestedScrollInternal(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow, type, null);
}
@@ -227,15 +228,15 @@
* method with the same signature to implement the standard policy.
*/
public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
- int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type,
- @Nullable int[] consumed) {
+ int dyUnconsumed, int @Nullable [] offsetInWindow, @NestedScrollType int type,
+ int @Nullable [] consumed) {
dispatchNestedScrollInternal(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow, type, consumed);
}
private boolean dispatchNestedScrollInternal(int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
- @NestedScrollType int type, @Nullable int[] consumed) {
+ int dxUnconsumed, int dyUnconsumed, int @Nullable [] offsetInWindow,
+ @NestedScrollType int type, int @Nullable [] consumed) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
@@ -284,8 +285,8 @@
*
* @return true if the parent consumed any of the nested scroll
*/
- public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
- @Nullable int[] offsetInWindow) {
+ public boolean dispatchNestedPreScroll(int dx, int dy, int @Nullable [] consumed,
+ int @Nullable [] offsetInWindow) {
return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, TYPE_TOUCH);
}
@@ -298,8 +299,8 @@
*
* @return true if the parent consumed any of the nested scroll
*/
- public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
- @Nullable int[] offsetInWindow, @NestedScrollType int type) {
+ public boolean dispatchNestedPreScroll(int dx, int dy, int @Nullable [] consumed,
+ int @Nullable [] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
diff --git a/core/core/src/main/java/androidx/core/view/NestedScrollingParent.java b/core/core/src/main/java/androidx/core/view/NestedScrollingParent.java
index b6bd3ca..c2ebf9b 100644
--- a/core/core/src/main/java/androidx/core/view/NestedScrollingParent.java
+++ b/core/core/src/main/java/androidx/core/view/NestedScrollingParent.java
@@ -22,9 +22,10 @@
import android.view.View;
import android.view.ViewConfiguration;
-import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat.ScrollAxis;
+import org.jspecify.annotations.NonNull;
+
/**
* This interface should be implemented by {@link android.view.ViewGroup ViewGroup} subclasses
* that wish to support scrolling operations delegated by a nested child view.
@@ -138,7 +139,7 @@
* @param dy Vertical scroll distance in pixels
* @param consumed Output. The horizontal and vertical scroll distance consumed by this parent
*/
- void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);
+ void onNestedPreScroll(@NonNull View target, int dx, int dy, int @NonNull [] consumed);
/**
* Request a fling from a nested scroll.
diff --git a/core/core/src/main/java/androidx/core/view/NestedScrollingParent2.java b/core/core/src/main/java/androidx/core/view/NestedScrollingParent2.java
index d888700..f29e98f 100644
--- a/core/core/src/main/java/androidx/core/view/NestedScrollingParent2.java
+++ b/core/core/src/main/java/androidx/core/view/NestedScrollingParent2.java
@@ -20,10 +20,11 @@
import android.view.MotionEvent;
import android.view.View;
-import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat.NestedScrollType;
import androidx.core.view.ViewCompat.ScrollAxis;
+import org.jspecify.annotations.NonNull;
+
/**
* This interface should be implemented by {@link android.view.ViewGroup ViewGroup} subclasses
* that wish to support scrolling operations delegated by a nested child view.
@@ -144,7 +145,7 @@
* @param consumed Output. The horizontal and vertical scroll distance consumed by this parent
* @param type the type of input which cause this scroll event
*/
- void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
+ void onNestedPreScroll(@NonNull View target, int dx, int dy, int @NonNull [] consumed,
@NestedScrollType int type);
}
diff --git a/core/core/src/main/java/androidx/core/view/NestedScrollingParent3.java b/core/core/src/main/java/androidx/core/view/NestedScrollingParent3.java
index dca8537..726ac19 100644
--- a/core/core/src/main/java/androidx/core/view/NestedScrollingParent3.java
+++ b/core/core/src/main/java/androidx/core/view/NestedScrollingParent3.java
@@ -19,7 +19,7 @@
import android.view.View;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* This interface should be implemented by {@link android.view.ViewGroup ViewGroup} subclasses
@@ -75,6 +75,6 @@
* @see NestedScrollingChild3#dispatchNestedScroll(int, int, int, int, int[], int, int[])
*/
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
- int dyUnconsumed, @ViewCompat.NestedScrollType int type, @NonNull int[] consumed);
+ int dyUnconsumed, @ViewCompat.NestedScrollType int type, int @NonNull [] consumed);
}
diff --git a/core/core/src/main/java/androidx/core/view/NestedScrollingParentHelper.java b/core/core/src/main/java/androidx/core/view/NestedScrollingParentHelper.java
index 766fe8c..e803e44 100644
--- a/core/core/src/main/java/androidx/core/view/NestedScrollingParentHelper.java
+++ b/core/core/src/main/java/androidx/core/view/NestedScrollingParentHelper.java
@@ -20,10 +20,11 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat.NestedScrollType;
import androidx.core.view.ViewCompat.ScrollAxis;
+import org.jspecify.annotations.NonNull;
+
/**
* Helper class for implementing nested scrolling parent views compatible with Android platform
* versions earlier than Android 5.0 Lollipop (API 21).
diff --git a/core/core/src/main/java/androidx/core/view/OnApplyWindowInsetsListener.java b/core/core/src/main/java/androidx/core/view/OnApplyWindowInsetsListener.java
index 157dad3..dcc569b3 100644
--- a/core/core/src/main/java/androidx/core/view/OnApplyWindowInsetsListener.java
+++ b/core/core/src/main/java/androidx/core/view/OnApplyWindowInsetsListener.java
@@ -18,7 +18,7 @@
import android.view.View;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Listener for applying window insets on a view in a custom way.
@@ -41,6 +41,6 @@
* @param insets The insets to apply
* @return The insets supplied, minus any insets that were consumed
*/
- @NonNull
- WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets);
+ @NonNull WindowInsetsCompat onApplyWindowInsets(@NonNull View v,
+ @NonNull WindowInsetsCompat insets);
}
\ No newline at end of file
diff --git a/core/core/src/main/java/androidx/core/view/OnReceiveContentListener.java b/core/core/src/main/java/androidx/core/view/OnReceiveContentListener.java
index 4682dce..69e2a4fb 100644
--- a/core/core/src/main/java/androidx/core/view/OnReceiveContentListener.java
+++ b/core/core/src/main/java/androidx/core/view/OnReceiveContentListener.java
@@ -18,8 +18,8 @@
import android.view.View;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* Listener for apps to implement handling for insertion of content. Content may be both text and
@@ -121,6 +121,6 @@
* succeed even if this method returns null. For example, an app may end up not inserting
* an item if it exceeds the app's size limit for that type of content.
*/
- @Nullable
- ContentInfoCompat onReceiveContent(@NonNull View view, @NonNull ContentInfoCompat payload);
+ @Nullable ContentInfoCompat onReceiveContent(@NonNull View view,
+ @NonNull ContentInfoCompat payload);
}
diff --git a/core/core/src/main/java/androidx/core/view/OnReceiveContentViewBehavior.java b/core/core/src/main/java/androidx/core/view/OnReceiveContentViewBehavior.java
index 16156c8..430015c 100644
--- a/core/core/src/main/java/androidx/core/view/OnReceiveContentViewBehavior.java
+++ b/core/core/src/main/java/androidx/core/view/OnReceiveContentViewBehavior.java
@@ -16,8 +16,8 @@
package androidx.core.view;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* Interface for widgets to implement default behavior for receiving content. Content may be both
@@ -40,6 +40,5 @@
* @return The portion of the passed-in content that was not handled (may be all, some, or none
* of the passed-in content).
*/
- @Nullable
- ContentInfoCompat onReceiveContent(@NonNull ContentInfoCompat payload);
+ @Nullable ContentInfoCompat onReceiveContent(@NonNull ContentInfoCompat payload);
}
diff --git a/core/core/src/main/java/androidx/core/view/OneShotPreDrawListener.java b/core/core/src/main/java/androidx/core/view/OneShotPreDrawListener.java
index af1ce27..2973e6b 100644
--- a/core/core/src/main/java/androidx/core/view/OneShotPreDrawListener.java
+++ b/core/core/src/main/java/androidx/core/view/OneShotPreDrawListener.java
@@ -19,7 +19,7 @@
import android.view.View;
import android.view.ViewTreeObserver;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* An OnPreDrawListener that will remove itself after one OnPreDraw call. Typical
@@ -53,9 +53,9 @@
* @return The added OneShotPreDrawListener. It can be removed prior to
* the onPreDraw by calling {@link #removeListener()}.
*/
- @NonNull
@SuppressWarnings("ConstantConditions") // Validating nullability contracts.
- public static OneShotPreDrawListener add(@NonNull View view, @NonNull Runnable runnable) {
+ public static @NonNull OneShotPreDrawListener add(@NonNull View view,
+ @NonNull Runnable runnable) {
if (view == null) throw new NullPointerException("view == null");
if (runnable == null) throw new NullPointerException("runnable == null");
diff --git a/core/core/src/main/java/androidx/core/view/PointerIconCompat.java b/core/core/src/main/java/androidx/core/view/PointerIconCompat.java
index b07a397..db4cc26 100644
--- a/core/core/src/main/java/androidx/core/view/PointerIconCompat.java
+++ b/core/core/src/main/java/androidx/core/view/PointerIconCompat.java
@@ -25,11 +25,12 @@
import android.graphics.Bitmap;
import android.view.PointerIcon;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Helper for accessing features in {@link PointerIcon} in a backwards compatible
* fashion.
@@ -112,9 +113,8 @@
/**
*/
- @Nullable
@RestrictTo(LIBRARY_GROUP_PREFIX)
- public Object getPointerIcon() {
+ public @Nullable Object getPointerIcon() {
return mPointerIcon;
}
@@ -126,8 +126,7 @@
* @param style The pointer icon style.
* @return The pointer icon.
*/
- @NonNull
- public static PointerIconCompat getSystemIcon(@NonNull Context context, int style) {
+ public static @NonNull PointerIconCompat getSystemIcon(@NonNull Context context, int style) {
if (SDK_INT >= 24) {
return new PointerIconCompat(Api24Impl.getSystemIcon(context, style));
} else {
@@ -148,8 +147,8 @@
* @throws IllegalArgumentException if bitmap is null, or if the x/y hotspot
* parameters are invalid.
*/
- @NonNull
- public static PointerIconCompat create(@NonNull Bitmap bitmap, float hotSpotX, float hotSpotY) {
+ public static @NonNull PointerIconCompat create(@NonNull Bitmap bitmap, float hotSpotX,
+ float hotSpotY) {
if (SDK_INT >= 24) {
return new PointerIconCompat(Api24Impl.create(bitmap, hotSpotX, hotSpotY));
} else {
@@ -177,8 +176,7 @@
* @throws Resources.NotFoundException if the resource was not found or the drawable
* linked in the resource was not found.
*/
- @NonNull
- public static PointerIconCompat load(@NonNull Resources resources, int resourceId) {
+ public static @NonNull PointerIconCompat load(@NonNull Resources resources, int resourceId) {
if (SDK_INT >= 24) {
return new PointerIconCompat(Api24Impl.load(resources, resourceId));
} else {
diff --git a/core/core/src/main/java/androidx/core/view/ScaleGestureDetectorCompat.java b/core/core/src/main/java/androidx/core/view/ScaleGestureDetectorCompat.java
index 02cc957..929df36 100644
--- a/core/core/src/main/java/androidx/core/view/ScaleGestureDetectorCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ScaleGestureDetectorCompat.java
@@ -18,7 +18,7 @@
import android.view.ScaleGestureDetector;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Helper for accessing features in {@link ScaleGestureDetector}.
diff --git a/core/core/src/main/java/androidx/core/view/ScrollFeedbackProviderCompat.java b/core/core/src/main/java/androidx/core/view/ScrollFeedbackProviderCompat.java
index 07be72b..bb4912e 100644
--- a/core/core/src/main/java/androidx/core/view/ScrollFeedbackProviderCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ScrollFeedbackProviderCompat.java
@@ -23,9 +23,10 @@
import android.view.View;
import android.view.ViewConfiguration;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
/** Compat to access {@link ScrollFeedbackProvider} across different build versions. */
public class ScrollFeedbackProviderCompat {
@@ -40,8 +41,7 @@
}
/** Creates an instance of {@link ScrollFeedbackProviderCompat}. */
- @NonNull
- public static ScrollFeedbackProviderCompat createProvider(@NonNull View view) {
+ public static @NonNull ScrollFeedbackProviderCompat createProvider(@NonNull View view) {
return new ScrollFeedbackProviderCompat(view);
}
diff --git a/core/core/src/main/java/androidx/core/view/SoftwareKeyboardControllerCompat.java b/core/core/src/main/java/androidx/core/view/SoftwareKeyboardControllerCompat.java
index a490031..b0d9fc4 100644
--- a/core/core/src/main/java/androidx/core/view/SoftwareKeyboardControllerCompat.java
+++ b/core/core/src/main/java/androidx/core/view/SoftwareKeyboardControllerCompat.java
@@ -24,10 +24,11 @@
import android.view.WindowInsetsController;
import android.view.inputmethod.InputMethodManager;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -96,8 +97,7 @@
@RequiresApi(20)
private static class Impl20 extends Impl {
- @Nullable
- private final View mView;
+ private final @Nullable View mView;
Impl20(@Nullable View view) {
mView = view;
@@ -152,11 +152,9 @@
@RequiresApi(30)
private static class Impl30 extends Impl20 {
- @Nullable
- private View mView;
+ private @Nullable View mView;
- @Nullable
- private WindowInsetsController mWindowInsetsController;
+ private @Nullable WindowInsetsController mWindowInsetsController;
Impl30(@NonNull View view) {
super(view);
diff --git a/core/core/src/main/java/androidx/core/view/TintableBackgroundView.java b/core/core/src/main/java/androidx/core/view/TintableBackgroundView.java
index 338f46e..6be159c 100644
--- a/core/core/src/main/java/androidx/core/view/TintableBackgroundView.java
+++ b/core/core/src/main/java/androidx/core/view/TintableBackgroundView.java
@@ -19,7 +19,7 @@
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Interface which allows a {@link android.view.View} to receive background tinting calls from
@@ -50,8 +50,7 @@
*
* @return the tint applied to the background drawable
*/
- @Nullable
- ColorStateList getSupportBackgroundTintList();
+ @Nullable ColorStateList getSupportBackgroundTintList();
/**
* Specifies the blending mode used to apply the tint specified by
@@ -62,7 +61,7 @@
* {@code null} to clear tint
* @see #getSupportBackgroundTintMode()
*/
- void setSupportBackgroundTintMode(@Nullable PorterDuff.Mode tintMode);
+ void setSupportBackgroundTintMode(PorterDuff.@Nullable Mode tintMode);
/**
* Return the blending mode used to apply the tint to the background
@@ -71,6 +70,5 @@
* @return the blending mode used to apply the tint to the background
* drawable
*/
- @Nullable
- PorterDuff.Mode getSupportBackgroundTintMode();
+ PorterDuff.@Nullable Mode getSupportBackgroundTintMode();
}
diff --git a/core/core/src/main/java/androidx/core/view/VelocityTrackerCompat.java b/core/core/src/main/java/androidx/core/view/VelocityTrackerCompat.java
index e5b812f..445be25 100644
--- a/core/core/src/main/java/androidx/core/view/VelocityTrackerCompat.java
+++ b/core/core/src/main/java/androidx/core/view/VelocityTrackerCompat.java
@@ -26,11 +26,12 @@
import android.view.VelocityTracker;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.util.Collections;
import java.util.Map;
@@ -296,8 +297,8 @@
sFallbackTrackers.remove(tracker);
}
- @Nullable
- private static VelocityTrackerFallback getFallbackTrackerOrNull(VelocityTracker tracker) {
+ private static @Nullable VelocityTrackerFallback getFallbackTrackerOrNull(
+ VelocityTracker tracker) {
return sFallbackTrackers.get(tracker);
}
diff --git a/core/core/src/main/java/androidx/core/view/VelocityTrackerFallback.java b/core/core/src/main/java/androidx/core/view/VelocityTrackerFallback.java
index 3743472..669ed97 100644
--- a/core/core/src/main/java/androidx/core/view/VelocityTrackerFallback.java
+++ b/core/core/src/main/java/androidx/core/view/VelocityTrackerFallback.java
@@ -20,7 +20,7 @@
import android.view.MotionEvent;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* A fallback implementation of {@link android.view.VelocityTracker}. The methods its provide
diff --git a/core/core/src/main/java/androidx/core/view/ViewCompat.java b/core/core/src/main/java/androidx/core/view/ViewCompat.java
index d0f4a1e..80d0cd2 100644
--- a/core/core/src/main/java/androidx/core/view/ViewCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ViewCompat.java
@@ -65,8 +65,6 @@
import androidx.annotation.FloatRange;
import androidx.annotation.IdRes;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
@@ -85,6 +83,9 @@
import androidx.core.view.contentcapture.ContentCaptureSessionCompat;
import androidx.core.viewtree.ViewTree;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
@@ -577,7 +578,7 @@
* @param defStyleRes Default style resource passed into the view constructor.
*/
public static void saveAttributeDataForStyleable(@NonNull View view,
- @SuppressLint("ContextFirst") @NonNull Context context, @NonNull int[] styleable,
+ @SuppressLint("ContextFirst") @NonNull Context context, int @NonNull [] styleable,
@Nullable AttributeSet attrs, @NonNull TypedArray t, int defStyleAttr,
int defStyleRes) {
if (Build.VERSION.SDK_INT >= 29) {
@@ -833,7 +834,7 @@
* @param autofillHints The autofill hints to set. If the array is emtpy, {@code null} is set.
* {@link android.R.attr#autofillHints}
*/
- public static void setAutofillHints(@NonNull View view, @Nullable String... autofillHints) {
+ public static void setAutofillHints(@NonNull View view, String @Nullable ... autofillHints) {
if (Build.VERSION.SDK_INT >= 26) {
Api26Impl.setAutofillHints(view, autofillHints);
}
@@ -993,8 +994,7 @@
* @param v The View against which to invoke the method.
* @return The View's autofill id.
*/
- @Nullable
- public static AutofillIdCompat getAutofillId(@NonNull View v) {
+ public static @Nullable AutofillIdCompat getAutofillId(@NonNull View v) {
if (Build.VERSION.SDK_INT >= 26) {
return AutofillIdCompat.toAutofillIdCompat(Api26Impl.getAutofillId(v));
}
@@ -1158,8 +1158,7 @@
* inherited by ancestors, default session or {@code null} if content capture is disabled for
* this view.
*/
- @Nullable
- public static ContentCaptureSessionCompat getContentCaptureSession(@NonNull View v) {
+ public static @Nullable ContentCaptureSessionCompat getContentCaptureSession(@NonNull View v) {
if (Build.VERSION.SDK_INT >= 29) {
ContentCaptureSession session = Api29Impl.getContentCaptureSession(v);
if (session == null) {
@@ -1230,8 +1229,8 @@
* still get an object that is being used to provide backward compatibility. Returns
* {@code null} if there is no delegate attached.
*/
- @Nullable
- public static AccessibilityDelegateCompat getAccessibilityDelegate(@NonNull View view) {
+ public static @Nullable AccessibilityDelegateCompat getAccessibilityDelegate(
+ @NonNull View view) {
final View.AccessibilityDelegate delegate = getAccessibilityDelegateInternal(view);
if (delegate == null) {
return null;
@@ -1250,8 +1249,8 @@
setAccessibilityDelegate(v, delegateCompat);
}
- @Nullable
- private static View.AccessibilityDelegate getAccessibilityDelegateInternal(@NonNull View v) {
+ private static View.@Nullable AccessibilityDelegate getAccessibilityDelegateInternal(
+ @NonNull View v) {
if (Build.VERSION.SDK_INT >= 29) {
return Api29Impl.getAccessibilityDelegate(v);
} else {
@@ -1260,8 +1259,7 @@
}
@SuppressWarnings("JavaReflectionMemberAccess") // Private field
- @Nullable
- private static View.AccessibilityDelegate getAccessibilityDelegateThroughReflection(
+ private static View.@Nullable AccessibilityDelegate getAccessibilityDelegateThroughReflection(
@NonNull View v) {
if (sAccessibilityDelegateCheckFailed) {
return null; // View implementation might have changed.
@@ -1668,8 +1666,8 @@
* <li>API < 21: No-op</li>
* </ul>
*/
- public static void replaceAccessibilityAction(@NonNull View view, @NonNull
- AccessibilityActionCompat replacedAction, @Nullable CharSequence label,
+ public static void replaceAccessibilityAction(@NonNull View view,
+ @NonNull AccessibilityActionCompat replacedAction, @Nullable CharSequence label,
@Nullable AccessibilityViewCommand command) {
if (command == null && label == null) {
ViewCompat.removeAccessibilityAction(view, replacedAction.getId());
@@ -1769,8 +1767,7 @@
* @see #setStateDescription(View, CharSequence)
*/
@UiThread
- @Nullable
- public static CharSequence getStateDescription(@NonNull View view) {
+ public static @Nullable CharSequence getStateDescription(@NonNull View view) {
return stateDescriptionProperty().get(view);
}
@@ -1826,8 +1823,8 @@
*
* @see AccessibilityNodeProviderCompat
*/
- @Nullable
- public static AccessibilityNodeProviderCompat getAccessibilityNodeProvider(@NonNull View view) {
+ public static @Nullable AccessibilityNodeProviderCompat getAccessibilityNodeProvider(
+ @NonNull View view) {
AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider();
if (provider != null) {
return new AccessibilityNodeProviderCompat(provider);
@@ -2036,8 +2033,7 @@
*/
@androidx.annotation.ReplaceWith(expression = "view.getParentForAccessibility()")
@Deprecated
- @Nullable
- public static ViewParent getParentForAccessibility(@NonNull View view) {
+ public static @Nullable ViewParent getParentForAccessibility(@NonNull View view) {
return view.getParentForAccessibility();
}
@@ -2057,8 +2053,7 @@
* @see View#findViewById(int)
*/
@SuppressWarnings("TypeParameterUnusedInFormals")
- @NonNull
- public static <T extends View> T requireViewById(@NonNull View view, @IdRes int id) {
+ public static <T extends View> @NonNull T requireViewById(@NonNull View view, @IdRes int id) {
if (Build.VERSION.SDK_INT >= 28) {
return ViewCompat.Api28Impl.requireViewById(view, id);
}
@@ -2395,8 +2390,7 @@
*/
@androidx.annotation.ReplaceWith(expression = "view.getMatrix()")
@Deprecated
- @Nullable
- public static Matrix getMatrix(View view) {
+ public static @Nullable Matrix getMatrix(View view) {
return view.getMatrix();
}
@@ -2440,8 +2434,7 @@
* @deprecated Call {@link View#animate()} directly.
*/
@Deprecated
- @NonNull
- public static ViewPropertyAnimatorCompat animate(@NonNull View view) {
+ public static @NonNull ViewPropertyAnimatorCompat animate(@NonNull View view) {
if (sViewPropertyAnimatorMap == null) {
sViewPropertyAnimatorMap = new WeakHashMap<>();
}
@@ -2829,8 +2822,7 @@
* @return The name used of the View to be used to identify Views in Transitions or null
* if no name has been given.
*/
- @Nullable
- public static String getTransitionName(@NonNull View view) {
+ public static @Nullable String getTransitionName(@NonNull View view) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.getTransitionName(view);
}
@@ -2972,7 +2964,7 @@
* @param view view on which to the listener.
* @param listener listener for the applied window insets.
*/
- public static void setOnApplyWindowInsetsListener(@NonNull final View view,
+ public static void setOnApplyWindowInsetsListener(final @NonNull View view,
final @Nullable OnApplyWindowInsetsListener listener) {
if (Build.VERSION.SDK_INT >= 21) {
Api21Impl.setOnApplyWindowInsetsListener(view, listener);
@@ -2991,8 +2983,7 @@
* @param insets Insets to apply
* @return The supplied insets with any applied insets consumed
*/
- @NonNull
- public static WindowInsetsCompat onApplyWindowInsets(@NonNull View view,
+ public static @NonNull WindowInsetsCompat onApplyWindowInsets(@NonNull View view,
@NonNull WindowInsetsCompat insets) {
if (Build.VERSION.SDK_INT >= 21) {
final WindowInsets unwrapped = insets.toWindowInsets();
@@ -3020,8 +3011,7 @@
* @param insets Insets to apply
* @return The provided insets minus the insets that were consumed
*/
- @NonNull
- public static WindowInsetsCompat dispatchApplyWindowInsets(@NonNull View view,
+ public static @NonNull WindowInsetsCompat dispatchApplyWindowInsets(@NonNull View view,
@NonNull WindowInsetsCompat insets) {
if (Build.VERSION.SDK_INT >= 21) {
final WindowInsets unwrapped = insets.toWindowInsets();
@@ -3067,8 +3057,7 @@
*
* @see View#getSystemGestureExclusionRects
*/
- @NonNull
- public static List<Rect> getSystemGestureExclusionRects(@NonNull View view) {
+ public static @NonNull List<Rect> getSystemGestureExclusionRects(@NonNull View view) {
if (Build.VERSION.SDK_INT >= 29) {
return Api29Impl.getSystemGestureExclusionRects(view);
}
@@ -3083,8 +3072,7 @@
*
* @return WindowInsetsCompat from the top of the view hierarchy or null if View is detached
*/
- @Nullable
- public static WindowInsetsCompat getRootWindowInsets(@NonNull View view) {
+ public static @Nullable WindowInsetsCompat getRootWindowInsets(@NonNull View view) {
if (Build.VERSION.SDK_INT >= 23) {
return Api23Impl.getRootWindowInsets(view);
} else if (Build.VERSION.SDK_INT >= 21) {
@@ -3105,8 +3093,7 @@
* by this view
* @return Insets that should be passed along to views under this one
*/
- @NonNull
- public static WindowInsetsCompat computeSystemWindowInsets(@NonNull View view,
+ public static @NonNull WindowInsetsCompat computeSystemWindowInsets(@NonNull View view,
@NonNull WindowInsetsCompat insets, @NonNull Rect outLocalInsets) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.computeSystemWindowInsets(view, insets, outLocalInsets);
@@ -3123,9 +3110,9 @@
* @deprecated Prefer {@link WindowCompat#getInsetsController(Window, View)} to explicitly
* specify the window (such as when the view is in a dialog).
*/
- @Nullable
@Deprecated
- public static WindowInsetsControllerCompat getWindowInsetsController(@NonNull View view) {
+ public static @Nullable WindowInsetsControllerCompat getWindowInsetsController(
+ @NonNull View view) {
if (Build.VERSION.SDK_INT >= 30) {
return Api30Impl.getWindowInsetsController(view);
} else {
@@ -3164,7 +3151,7 @@
* callback
*/
public static void setWindowInsetsAnimationCallback(@NonNull View view,
- @Nullable final WindowInsetsAnimationCompat.Callback callback) {
+ final WindowInsetsAnimationCompat.@Nullable Callback callback) {
WindowInsetsAnimationCompat.setCallback(view, callback);
}
@@ -3203,8 +3190,8 @@
* not be null or empty if a non-null listener is passed in.
* @param listener The listener to use. This can be null to reset to the default behavior.
*/
- public static void setOnReceiveContentListener(@NonNull View view, @Nullable String[] mimeTypes,
- @Nullable OnReceiveContentListener listener) {
+ public static void setOnReceiveContentListener(@NonNull View view,
+ String @Nullable [] mimeTypes, @Nullable OnReceiveContentListener listener) {
if (Build.VERSION.SDK_INT >= 31) {
Api31Impl.setOnReceiveContentListener(view, mimeTypes, listener);
return;
@@ -3252,8 +3239,7 @@
* @return The MIME types accepted by the {@link OnReceiveContentListener} for the given view
* (may include patterns such as "image/*").
*/
- @Nullable
- public static String[] getOnReceiveContentMimeTypes(@NonNull View view) {
+ public static String @Nullable [] getOnReceiveContentMimeTypes(@NonNull View view) {
if (Build.VERSION.SDK_INT >= 31) {
return Api31Impl.getReceiveContentMimeTypes(view);
}
@@ -3277,8 +3263,7 @@
* @return The portion of the passed-in content that was not handled (may be all, some, or none
* of the passed-in content).
*/
- @Nullable
- public static ContentInfoCompat performReceiveContent(@NonNull View view,
+ public static @Nullable ContentInfoCompat performReceiveContent(@NonNull View view,
@NonNull ContentInfoCompat payload) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "performReceiveContent: " + payload
@@ -3311,7 +3296,7 @@
private Api31Impl() {}
public static void setOnReceiveContentListener(@NonNull View view,
- @Nullable String[] mimeTypes, @Nullable final OnReceiveContentListener listener) {
+ String @Nullable [] mimeTypes, final @Nullable OnReceiveContentListener listener) {
if (listener == null) {
view.setOnReceiveContentListener(mimeTypes, null);
} else {
@@ -3320,13 +3305,11 @@
}
}
- @Nullable
- public static String[] getReceiveContentMimeTypes(@NonNull View view) {
+ public static String @Nullable [] getReceiveContentMimeTypes(@NonNull View view) {
return view.getReceiveContentMimeTypes();
}
- @Nullable
- public static ContentInfoCompat performReceiveContent(@NonNull View view,
+ public static @Nullable ContentInfoCompat performReceiveContent(@NonNull View view,
@NonNull ContentInfoCompat payload) {
ContentInfo platPayload = payload.toContentInfo();
ContentInfo platResult = view.performReceiveContent(platPayload);
@@ -3345,16 +3328,15 @@
private static final class OnReceiveContentListenerAdapter implements
android.view.OnReceiveContentListener {
- @NonNull
- private final OnReceiveContentListener mJetpackListener;
+ private final @NonNull OnReceiveContentListener mJetpackListener;
OnReceiveContentListenerAdapter(@NonNull OnReceiveContentListener jetpackListener) {
mJetpackListener = jetpackListener;
}
- @Nullable
@Override
- public ContentInfo onReceiveContent(@NonNull View view, @NonNull ContentInfo platPayload) {
+ public @Nullable ContentInfo onReceiveContent(@NonNull View view,
+ @NonNull ContentInfo platPayload) {
ContentInfoCompat payload = ContentInfoCompat.toContentInfoCompat(platPayload);
ContentInfoCompat result = mJetpackListener.onReceiveContent(view, payload);
if (result == null) {
@@ -3459,8 +3441,7 @@
* Only returns meaningful info when running on API v21 or newer, or if {@code view}
* implements the {@code TintableBackgroundView} interface.
*/
- @Nullable
- public static ColorStateList getBackgroundTintList(@NonNull View view) {
+ public static @Nullable ColorStateList getBackgroundTintList(@NonNull View view) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.getBackgroundTintList(view);
}
@@ -3506,8 +3487,7 @@
* Only returns meaningful info when running on API v21 or newer, or if {@code view}
* implements the {@code TintableBackgroundView} interface.
*/
- @Nullable
- public static PorterDuff.Mode getBackgroundTintMode(@NonNull View view) {
+ public static PorterDuff.@Nullable Mode getBackgroundTintMode(@NonNull View view) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.getBackgroundTintMode(view);
}
@@ -3525,7 +3505,7 @@
* previous to API v21, it will only take effect if {@code view} implement the
* {@code TintableBackgroundView} interface.
*/
- public static void setBackgroundTintMode(@NonNull View view, @Nullable PorterDuff.Mode mode) {
+ public static void setBackgroundTintMode(@NonNull View view, PorterDuff.@Nullable Mode mode) {
if (Build.VERSION.SDK_INT >= 21) {
Api21Impl.setBackgroundTintMode(view, mode);
@@ -3679,7 +3659,7 @@
*/
@SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static boolean dispatchNestedScroll(@NonNull View view, int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
+ int dxUnconsumed, int dyUnconsumed, int @Nullable [] offsetInWindow) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.dispatchNestedScroll(view, dxConsumed, dyConsumed, dxUnconsumed,
dyUnconsumed, offsetInWindow);
@@ -3711,7 +3691,7 @@
*/
@SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static boolean dispatchNestedPreScroll(@NonNull View view, int dx, int dy,
- @Nullable int[] consumed, @Nullable int[] offsetInWindow) {
+ int @Nullable [] consumed, int @Nullable [] offsetInWindow) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.dispatchNestedPreScroll(view, dx, dy, consumed, offsetInWindow);
}
@@ -3840,8 +3820,8 @@
* component of dx and <code>consumed[1]</code> the consumed dy.
*/
public static void dispatchNestedScroll(@NonNull View view, int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
- @NestedScrollType int type, @NonNull int[] consumed) {
+ int dxUnconsumed, int dyUnconsumed, int @Nullable [] offsetInWindow,
+ @NestedScrollType int type, int @NonNull [] consumed) {
if (view instanceof NestedScrollingChild3) {
((NestedScrollingChild3) view).dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed);
@@ -3877,7 +3857,7 @@
* @see #dispatchNestedPreScroll(View, int, int, int[], int[])
*/
public static boolean dispatchNestedScroll(@NonNull View view, int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
+ int dxUnconsumed, int dyUnconsumed, int @Nullable [] offsetInWindow,
@NestedScrollType int type) {
if (view instanceof NestedScrollingChild2) {
return ((NestedScrollingChild2) view).dispatchNestedScroll(dxConsumed, dyConsumed,
@@ -3911,7 +3891,8 @@
* @see #dispatchNestedScroll(View, int, int, int, int, int[])
*/
public static boolean dispatchNestedPreScroll(@NonNull View view, int dx, int dy,
- @Nullable int[] consumed, @Nullable int[] offsetInWindow, @NestedScrollType int type) {
+ int @Nullable [] consumed, int @Nullable [] offsetInWindow,
+ @NestedScrollType int type) {
if (view instanceof NestedScrollingChild2) {
return ((NestedScrollingChild2) view).dispatchNestedPreScroll(dx, dy, consumed,
offsetInWindow, type);
@@ -4211,8 +4192,7 @@
*/
@androidx.annotation.ReplaceWith(expression = "view.getClipBounds()")
@Deprecated
- @Nullable
- public static Rect getClipBounds(@NonNull View view) {
+ public static @Nullable Rect getClipBounds(@NonNull View view) {
return view.getClipBounds();
}
@@ -4338,8 +4318,7 @@
*/
@androidx.annotation.ReplaceWith(expression = "view.getDisplay()")
@Deprecated
- @Nullable
- public static Display getDisplay(@NonNull View view) {
+ public static @Nullable Display getDisplay(@NonNull View view) {
return view.getDisplay();
}
@@ -4363,7 +4342,7 @@
*/
@SuppressWarnings("deprecation")
public static boolean startDragAndDrop(@NonNull View v, @Nullable ClipData data,
- @NonNull View.DragShadowBuilder shadowBuilder, @Nullable Object myLocalState,
+ View.@NonNull DragShadowBuilder shadowBuilder, @Nullable Object myLocalState,
int flags) {
if (Build.VERSION.SDK_INT >= 24) {
return Api24Impl.startDragAndDrop(v, data, shadowBuilder, myLocalState, flags);
@@ -4385,7 +4364,7 @@
* Update the drag shadow while drag and drop is in progress.
*/
public static void updateDragShadow(@NonNull View v,
- @NonNull View.DragShadowBuilder shadowBuilder) {
+ View.@NonNull DragShadowBuilder shadowBuilder) {
if (Build.VERSION.SDK_INT >= 24) {
Api24Impl.updateDragShadow(v, shadowBuilder);
}
@@ -4494,8 +4473,7 @@
* @return the nearest keyboard navigation cluster in the specified direction, or {@code null}
* if one can't be found or if API < 26.
*/
- @Nullable
- public static View keyboardNavigationClusterSearch(@NonNull View view,
+ public static @Nullable View keyboardNavigationClusterSearch(@NonNull View view,
@Nullable View currentCluster, @FocusDirection int direction) {
if (Build.VERSION.SDK_INT >= 26) {
return Api26Impl.keyboardNavigationClusterSearch(view, currentCluster, direction);
@@ -4784,9 +4762,8 @@
*
* {@see #setAccessibilityPaneTitle}.
*/
- @Nullable
@UiThread
- public static CharSequence getAccessibilityPaneTitle(@NonNull View view) {
+ public static @Nullable CharSequence getAccessibilityPaneTitle(@NonNull View view) {
return paneTitleProperty().get(view);
}
@@ -5080,8 +5057,7 @@
// This is a cache (per keypress) of all the views which either have listeners or
// contain a view with listeners. This is only accessed on the UI thread.
- @Nullable
- private WeakHashMap<View, Boolean> mViewsContainingListeners = null;
+ private @Nullable WeakHashMap<View, Boolean> mViewsContainingListeners = null;
// Keeps track of which Views have unhandled key focus for which keys. This doesn't
// include modifiers.
@@ -5128,8 +5104,7 @@
return consumer != null;
}
- @Nullable
- private View dispatchInOrder(View view, KeyEvent event) {
+ private @Nullable View dispatchInOrder(View view, KeyEvent event) {
if (mViewsContainingListeners == null || !mViewsContainingListeners.containsKey(view)) {
return null;
}
@@ -5264,8 +5239,7 @@
}
// Only called on SDK 21 and 22
- @Nullable
- public static WindowInsetsCompat getRootWindowInsets(@NonNull View v) {
+ public static @Nullable WindowInsetsCompat getRootWindowInsets(@NonNull View v) {
return WindowInsetsCompat.Api21ReflectionHolder.getRootWindowInsets(v);
}
@@ -5465,8 +5439,7 @@
// This class is not instantiable.
}
- @Nullable
- public static WindowInsetsCompat getRootWindowInsets(@NonNull View v) {
+ public static @Nullable WindowInsetsCompat getRootWindowInsets(@NonNull View v) {
final WindowInsets wi = v.getRootWindowInsets();
if (wi == null) return null;
@@ -5498,7 +5471,7 @@
}
static void saveAttributeDataForStyleable(@NonNull View view,
- @NonNull Context context, @NonNull int[] styleable, @Nullable AttributeSet attrs,
+ @NonNull Context context, int @NonNull [] styleable, @Nullable AttributeSet attrs,
@NonNull TypedArray t, int defStyleAttr, int defStyleRes) {
view.saveAttributeDataForStyleable(
context, styleable, attrs, t, defStyleAttr, defStyleRes);
@@ -5533,8 +5506,8 @@
// This class is not instantiable.
}
- @Nullable
- public static WindowInsetsControllerCompat getWindowInsetsController(@NonNull View view) {
+ public static @Nullable WindowInsetsControllerCompat getWindowInsetsController(
+ @NonNull View view) {
WindowInsetsController windowInsetsController = view.getWindowInsetsController();
return windowInsetsController != null
? WindowInsetsControllerCompat.toWindowInsetsControllerCompat(
@@ -5650,7 +5623,7 @@
}
static boolean startDragAndDrop(@NonNull View view, @Nullable ClipData data,
- @NonNull View.DragShadowBuilder shadowBuilder, @Nullable Object myLocalState,
+ View.@NonNull DragShadowBuilder shadowBuilder, @Nullable Object myLocalState,
int flags) {
return view.startDragAndDrop(data, shadowBuilder, myLocalState, flags);
}
@@ -5660,7 +5633,7 @@
}
static void updateDragShadow(@NonNull View view,
- @NonNull View.DragShadowBuilder shadowBuilder) {
+ View.@NonNull DragShadowBuilder shadowBuilder) {
view.updateDragShadow(shadowBuilder);
}
diff --git a/core/core/src/main/java/androidx/core/view/ViewConfigurationCompat.java b/core/core/src/main/java/androidx/core/view/ViewConfigurationCompat.java
index b4e425d..f65b6e9 100644
--- a/core/core/src/main/java/androidx/core/view/ViewConfigurationCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ViewConfigurationCompat.java
@@ -27,10 +27,11 @@
import android.view.VelocityTracker;
import android.view.ViewConfiguration;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.util.Supplier;
+import org.jspecify.annotations.NonNull;
+
import java.lang.reflect.Method;
/**
diff --git a/core/core/src/main/java/androidx/core/view/ViewGroupCompat.java b/core/core/src/main/java/androidx/core/view/ViewGroupCompat.java
index 285036e..7080da9 100644
--- a/core/core/src/main/java/androidx/core/view/ViewGroupCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ViewGroupCompat.java
@@ -22,11 +22,12 @@
import android.view.WindowInsets;
import android.view.accessibility.AccessibilityEvent;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.R;
import androidx.core.view.ViewCompat.ScrollAxis;
+import org.jspecify.annotations.NonNull;
+
/**
* Helper for accessing features in {@link ViewGroup}.
*/
diff --git a/core/core/src/main/java/androidx/core/view/ViewParentCompat.java b/core/core/src/main/java/androidx/core/view/ViewParentCompat.java
index c10dc12..86ce9ea 100644
--- a/core/core/src/main/java/androidx/core/view/ViewParentCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ViewParentCompat.java
@@ -25,9 +25,10 @@
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
/**
* Helper for accessing features in {@link ViewParent}.
*/
@@ -182,7 +183,7 @@
* @param consumed Output. The horizontal and vertical scroll distance consumed by this parent
*/
public static void onNestedPreScroll(@NonNull ViewParent parent, @NonNull View target, int dx,
- int dy, @NonNull int[] consumed) {
+ int dy, int @NonNull [] consumed) {
onNestedPreScroll(parent, target, dx, dy, consumed, ViewCompat.TYPE_TOUCH);
}
@@ -337,7 +338,7 @@
*/
public static void onNestedScroll(@NonNull ViewParent parent, @NonNull View target,
int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type,
- @NonNull int[] consumed) {
+ int @NonNull [] consumed) {
if (parent instanceof NestedScrollingParent3) {
((NestedScrollingParent3) parent).onNestedScroll(target, dxConsumed, dyConsumed,
@@ -394,7 +395,7 @@
*/
@SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static void onNestedPreScroll(@NonNull ViewParent parent, @NonNull View target, int dx,
- int dy, @NonNull int[] consumed, int type) {
+ int dy, int @NonNull [] consumed, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);
diff --git a/core/core/src/main/java/androidx/core/view/ViewPropertyAnimatorCompat.java b/core/core/src/main/java/androidx/core/view/ViewPropertyAnimatorCompat.java
index 18d981b..0da3b51 100644
--- a/core/core/src/main/java/androidx/core/view/ViewPropertyAnimatorCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ViewPropertyAnimatorCompat.java
@@ -25,10 +25,11 @@
import android.view.ViewPropertyAnimator;
import android.view.animation.Interpolator;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.ref.WeakReference;
public final class ViewPropertyAnimatorCompat {
@@ -47,8 +48,7 @@
* cannot be negative.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat setDuration(long value) {
+ public @NonNull ViewPropertyAnimatorCompat setDuration(long value) {
View view;
if ((view = mView.get()) != null) {
view.animate().setDuration(value);
@@ -63,8 +63,7 @@
* @param value The value to be animated to.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat alpha(float value) {
+ public @NonNull ViewPropertyAnimatorCompat alpha(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().alpha(value);
@@ -79,8 +78,7 @@
* @param value The amount to be animated by, as an offset from the current value.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat alphaBy(float value) {
+ public @NonNull ViewPropertyAnimatorCompat alphaBy(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().alphaBy(value);
@@ -95,8 +93,7 @@
* @param value The value to be animated to.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat translationX(float value) {
+ public @NonNull ViewPropertyAnimatorCompat translationX(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().translationX(value);
@@ -111,8 +108,7 @@
* @param value The value to be animated to.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat translationY(float value) {
+ public @NonNull ViewPropertyAnimatorCompat translationY(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().translationY(value);
@@ -145,8 +141,7 @@
* @param runnable The action to run when the next animation ends.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat withEndAction(@NonNull Runnable runnable) {
+ public @NonNull ViewPropertyAnimatorCompat withEndAction(@NonNull Runnable runnable) {
View view;
if ((view = mView.get()) != null) {
ViewPropertyAnimator animator = view.animate();
@@ -180,8 +175,7 @@
* @param value The TimeInterpolator to be used for ensuing property animations.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat setInterpolator(@Nullable Interpolator value) {
+ public @NonNull ViewPropertyAnimatorCompat setInterpolator(@Nullable Interpolator value) {
View view;
if ((view = mView.get()) != null) {
view.animate().setInterpolator(value);
@@ -194,8 +188,7 @@
*
* @return The timing interpolator for this animation.
*/
- @Nullable
- public Interpolator getInterpolator() {
+ public @Nullable Interpolator getInterpolator() {
View view;
if ((view = mView.get()) != null) {
ViewPropertyAnimator animator = view.animate();
@@ -213,8 +206,7 @@
* cannot be negative.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat setStartDelay(long value) {
+ public @NonNull ViewPropertyAnimatorCompat setStartDelay(long value) {
View view;
if ((view = mView.get()) != null) {
view.animate().setStartDelay(value);
@@ -246,8 +238,7 @@
* @param value The value to be animated to.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat rotation(float value) {
+ public @NonNull ViewPropertyAnimatorCompat rotation(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().rotation(value);
@@ -262,8 +253,7 @@
* @param value The amount to be animated by, as an offset from the current value.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat rotationBy(float value) {
+ public @NonNull ViewPropertyAnimatorCompat rotationBy(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().rotationBy(value);
@@ -278,8 +268,7 @@
* @param value The value to be animated to.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat rotationX(float value) {
+ public @NonNull ViewPropertyAnimatorCompat rotationX(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().rotationX(value);
@@ -294,8 +283,7 @@
* @param value The amount to be animated by, as an offset from the current value.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat rotationXBy(float value) {
+ public @NonNull ViewPropertyAnimatorCompat rotationXBy(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().rotationXBy(value);
@@ -310,8 +298,7 @@
* @param value The value to be animated to.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat rotationY(float value) {
+ public @NonNull ViewPropertyAnimatorCompat rotationY(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().rotationY(value);
@@ -326,8 +313,7 @@
* @param value The amount to be animated by, as an offset from the current value.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat rotationYBy(float value) {
+ public @NonNull ViewPropertyAnimatorCompat rotationYBy(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().rotationYBy(value);
@@ -342,8 +328,7 @@
* @param value The value to be animated to.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat scaleX(float value) {
+ public @NonNull ViewPropertyAnimatorCompat scaleX(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().scaleX(value);
@@ -358,8 +343,7 @@
* @param value The amount to be animated by, as an offset from the current value.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat scaleXBy(float value) {
+ public @NonNull ViewPropertyAnimatorCompat scaleXBy(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().scaleXBy(value);
@@ -374,8 +358,7 @@
* @param value The value to be animated to.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat scaleY(float value) {
+ public @NonNull ViewPropertyAnimatorCompat scaleY(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().scaleY(value);
@@ -390,8 +373,7 @@
* @param value The amount to be animated by, as an offset from the current value.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat scaleYBy(float value) {
+ public @NonNull ViewPropertyAnimatorCompat scaleYBy(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().scaleYBy(value);
@@ -416,8 +398,7 @@
* @param value The value to be animated to.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat x(float value) {
+ public @NonNull ViewPropertyAnimatorCompat x(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().x(value);
@@ -432,8 +413,7 @@
* @param value The amount to be animated by, as an offset from the current value.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat xBy(float value) {
+ public @NonNull ViewPropertyAnimatorCompat xBy(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().xBy(value);
@@ -448,8 +428,7 @@
* @param value The value to be animated to.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat y(float value) {
+ public @NonNull ViewPropertyAnimatorCompat y(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().y(value);
@@ -464,8 +443,7 @@
* @param value The amount to be animated by, as an offset from the current value.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat yBy(float value) {
+ public @NonNull ViewPropertyAnimatorCompat yBy(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().yBy(value);
@@ -480,8 +458,7 @@
* @param value The amount to be animated by, as an offset from the current value.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat translationXBy(float value) {
+ public @NonNull ViewPropertyAnimatorCompat translationXBy(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().translationXBy(value);
@@ -496,8 +473,7 @@
* @param value The amount to be animated by, as an offset from the current value.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat translationYBy(float value) {
+ public @NonNull ViewPropertyAnimatorCompat translationYBy(float value) {
View view;
if ((view = mView.get()) != null) {
view.animate().translationYBy(value);
@@ -514,8 +490,7 @@
* @param value The amount to be animated by, as an offset from the current value.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat translationZBy(float value) {
+ public @NonNull ViewPropertyAnimatorCompat translationZBy(float value) {
View view;
if ((view = mView.get()) != null) {
if (Build.VERSION.SDK_INT >= 21) {
@@ -535,8 +510,7 @@
* @param value The amount to be animated by, as an offset from the current value.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat translationZ(float value) {
+ public @NonNull ViewPropertyAnimatorCompat translationZ(float value) {
View view;
if ((view = mView.get()) != null) {
if (Build.VERSION.SDK_INT >= 21) {
@@ -556,8 +530,7 @@
* @param value The amount to be animated by, as an offset from the current value.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat z(float value) {
+ public @NonNull ViewPropertyAnimatorCompat z(float value) {
View view;
if ((view = mView.get()) != null) {
if (Build.VERSION.SDK_INT >= 21) {
@@ -577,8 +550,7 @@
* @param value The amount to be animated by, as an offset from the current value.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat zBy(float value) {
+ public @NonNull ViewPropertyAnimatorCompat zBy(float value) {
View view;
if ((view = mView.get()) != null) {
if (Build.VERSION.SDK_INT >= 21) {
@@ -625,9 +597,8 @@
* @see View#setLayerType(int, Paint)
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
@SuppressLint("WrongConstant")
- public ViewPropertyAnimatorCompat withLayer() {
+ public @NonNull ViewPropertyAnimatorCompat withLayer() {
View view;
if ((view = mView.get()) != null) {
ViewPropertyAnimator animator = view.animate();
@@ -647,8 +618,7 @@
* @param runnable The action to run when the next animation starts.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat withStartAction(@NonNull Runnable runnable) {
+ public @NonNull ViewPropertyAnimatorCompat withStartAction(@NonNull Runnable runnable) {
View view;
if ((view = mView.get()) != null) {
ViewPropertyAnimator animator = view.animate();
@@ -665,8 +635,7 @@
* <code>null</code> removes any existing listener.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat setListener(
+ public @NonNull ViewPropertyAnimatorCompat setListener(
final @Nullable ViewPropertyAnimatorListener listener) {
final View view;
if ((view = mView.get()) != null) {
@@ -706,8 +675,7 @@
* <code>null</code> removes any existing listener.
* @return This object, allowing calls to methods in this class to be chained.
*/
- @NonNull
- public ViewPropertyAnimatorCompat setUpdateListener(
+ public @NonNull ViewPropertyAnimatorCompat setUpdateListener(
final @Nullable ViewPropertyAnimatorUpdateListener listener) {
final View view;
if ((view = mView.get()) != null) {
diff --git a/core/core/src/main/java/androidx/core/view/ViewPropertyAnimatorListener.java b/core/core/src/main/java/androidx/core/view/ViewPropertyAnimatorListener.java
index be25230..f1b9bc7 100644
--- a/core/core/src/main/java/androidx/core/view/ViewPropertyAnimatorListener.java
+++ b/core/core/src/main/java/androidx/core/view/ViewPropertyAnimatorListener.java
@@ -18,7 +18,7 @@
import android.view.View;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* <p>An animation listener receives notifications from an animation.
diff --git a/core/core/src/main/java/androidx/core/view/ViewPropertyAnimatorListenerAdapter.java b/core/core/src/main/java/androidx/core/view/ViewPropertyAnimatorListenerAdapter.java
index 04806fb..c602883 100644
--- a/core/core/src/main/java/androidx/core/view/ViewPropertyAnimatorListenerAdapter.java
+++ b/core/core/src/main/java/androidx/core/view/ViewPropertyAnimatorListenerAdapter.java
@@ -18,7 +18,7 @@
import android.view.View;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* This adapter class provides empty implementations of the methods from
diff --git a/core/core/src/main/java/androidx/core/view/ViewPropertyAnimatorUpdateListener.java b/core/core/src/main/java/androidx/core/view/ViewPropertyAnimatorUpdateListener.java
index 663c46b..293dfc9 100644
--- a/core/core/src/main/java/androidx/core/view/ViewPropertyAnimatorUpdateListener.java
+++ b/core/core/src/main/java/androidx/core/view/ViewPropertyAnimatorUpdateListener.java
@@ -18,7 +18,7 @@
import android.view.View;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Implementors of this interface can add themselves as update listeners
diff --git a/core/core/src/main/java/androidx/core/view/ViewStructureCompat.java b/core/core/src/main/java/androidx/core/view/ViewStructureCompat.java
index 6e1f28e..def380c 100644
--- a/core/core/src/main/java/androidx/core/view/ViewStructureCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ViewStructureCompat.java
@@ -20,9 +20,10 @@
import android.view.ViewStructure;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
/**
* Helper for accessing features in {@link ViewStructure}.
* <p>
@@ -44,8 +45,7 @@
* @return wrapped class
*/
@RequiresApi(23)
- @NonNull
- public static ViewStructureCompat toViewStructureCompat(
+ public static @NonNull ViewStructureCompat toViewStructureCompat(
@NonNull ViewStructure contentCaptureSession) {
return new ViewStructureCompat(contentCaptureSession);
}
@@ -60,8 +60,7 @@
* @see ViewStructureCompat#toViewStructureCompat(ViewStructure)
*/
@RequiresApi(23)
- @NonNull
- public ViewStructure toViewStructure() {
+ public @NonNull ViewStructure toViewStructure() {
return (ViewStructure) mWrappedObj;
}
diff --git a/core/core/src/main/java/androidx/core/view/WindowCompat.java b/core/core/src/main/java/androidx/core/view/WindowCompat.java
index 3c16649..4084f63 100644
--- a/core/core/src/main/java/androidx/core/view/WindowCompat.java
+++ b/core/core/src/main/java/androidx/core/view/WindowCompat.java
@@ -23,9 +23,10 @@
import android.view.Window;
import androidx.annotation.IdRes;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
/**
* Helper for accessing features in {@link Window}.
*/
@@ -84,8 +85,8 @@
* @see Window#findViewById(int)
*/
@SuppressWarnings("TypeParameterUnusedInFormals")
- @NonNull
- public static <T extends View> T requireViewById(@NonNull Window window, @IdRes int id) {
+ public static <T extends View> @NonNull T requireViewById(@NonNull Window window,
+ @IdRes int id) {
if (Build.VERSION.SDK_INT >= 28) {
return Api28Impl.requireViewById(window, id);
}
@@ -131,8 +132,7 @@
* @return The {@link WindowInsetsControllerCompat} for the window.
* @see Window#getInsetsController()
*/
- @NonNull
- public static WindowInsetsControllerCompat getInsetsController(@NonNull Window window,
+ public static @NonNull WindowInsetsControllerCompat getInsetsController(@NonNull Window window,
@NonNull View view) {
return new WindowInsetsControllerCompat(window, view);
}
diff --git a/core/core/src/main/java/androidx/core/view/WindowInsetsAnimationCompat.java b/core/core/src/main/java/androidx/core/view/WindowInsetsAnimationCompat.java
index cb1d92c..b800b8f 100644
--- a/core/core/src/main/java/androidx/core/view/WindowInsetsAnimationCompat.java
+++ b/core/core/src/main/java/androidx/core/view/WindowInsetsAnimationCompat.java
@@ -34,8 +34,6 @@
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.R;
@@ -44,6 +42,9 @@
import androidx.core.view.WindowInsetsCompat.Type.InsetsType;
import androidx.interpolator.view.animation.FastOutLinearInInterpolator;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
@@ -158,8 +159,7 @@
*
* @return The interpolator used for this animation.
*/
- @Nullable
- public Interpolator getInterpolator() {
+ public @Nullable Interpolator getInterpolator() {
return mImpl.getInterpolator();
}
@@ -226,7 +226,7 @@
}
@RequiresApi(30)
- private BoundsCompat(@NonNull WindowInsetsAnimation.Bounds bounds) {
+ private BoundsCompat(WindowInsetsAnimation.@NonNull Bounds bounds) {
mLowerBound = Impl30.getLowerBounds(bounds);
mUpperBound = Impl30.getHigherBounds(bounds);
}
@@ -250,8 +250,7 @@
* @see #getUpperBound()
* @see WindowInsetsAnimationControllerCompat#getHiddenStateInsets
*/
- @NonNull
- public Insets getLowerBound() {
+ public @NonNull Insets getLowerBound() {
return mLowerBound;
}
@@ -274,8 +273,7 @@
* @see #getLowerBound()
* @see WindowInsetsAnimationControllerCompat#getShownStateInsets
*/
- @NonNull
- public Insets getUpperBound() {
+ public @NonNull Insets getUpperBound() {
return mUpperBound;
}
@@ -290,8 +288,7 @@
* @see WindowInsetsCompat#inset
* @see WindowInsetsAnimationCompat.Callback#onStart
*/
- @NonNull
- public BoundsCompat inset(@NonNull Insets insets) {
+ public @NonNull BoundsCompat inset(@NonNull Insets insets) {
return new BoundsCompat(
// TODO: refactor so that WindowInsets.insetInsets() is in a more appropriate
// place eventually.
@@ -310,8 +307,7 @@
* Creates a new instance of {@link WindowInsetsAnimation.Bounds} from this compat instance.
*/
@RequiresApi(30)
- @NonNull
- public WindowInsetsAnimation.Bounds toBounds() {
+ public WindowInsetsAnimation.@NonNull Bounds toBounds() {
return Impl30.createPlatformBounds(this);
}
@@ -320,8 +316,8 @@
* platform {@link android.view.WindowInsetsAnimation.Bounds}.
*/
@RequiresApi(30)
- @NonNull
- public static BoundsCompat toBoundsCompat(@NonNull WindowInsetsAnimation.Bounds bounds) {
+ public static @NonNull BoundsCompat toBoundsCompat(
+ WindowInsetsAnimation.@NonNull Bounds bounds) {
return new BoundsCompat(bounds);
}
}
@@ -483,8 +479,7 @@
* dispatched to
* the subtree of the hierarchy.
*/
- @NonNull
- public BoundsCompat onStart(
+ public @NonNull BoundsCompat onStart(
@NonNull WindowInsetsAnimationCompat animation,
@NonNull BoundsCompat bounds) {
return bounds;
@@ -508,8 +503,7 @@
* @param runningAnimations The currently running animations.
* @return The insets to dispatch to the subtree of the hierarchy.
*/
- @NonNull
- public abstract WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets,
+ public abstract @NonNull WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets,
@NonNull List<WindowInsetsAnimationCompat> runningAnimations);
/**
@@ -535,8 +529,7 @@
@InsetsType
private final int mTypeMask;
private float mFraction;
- @Nullable
- private final Interpolator mInterpolator;
+ private final @Nullable Interpolator mInterpolator;
private final long mDurationMillis;
private float mAlpha = 1f;
@@ -561,8 +554,7 @@
return mFraction;
}
- @Nullable
- public Interpolator getInterpolator() {
+ public @Nullable Interpolator getInterpolator() {
return mInterpolator;
}
@@ -627,8 +619,8 @@
super(typeMask, interpolator, durationMillis);
}
- static void setCallback(@NonNull final View view,
- @Nullable final Callback callback) {
+ static void setCallback(final @NonNull View view,
+ final @Nullable Callback callback) {
final View.OnApplyWindowInsetsListener proxyListener = callback != null
? createProxyListener(view, callback)
: null;
@@ -645,14 +637,12 @@
}
}
- @NonNull
- private static View.OnApplyWindowInsetsListener createProxyListener(
- @NonNull View view, @NonNull final Callback callback) {
+ private static View.@NonNull OnApplyWindowInsetsListener createProxyListener(
+ @NonNull View view, final @NonNull Callback callback) {
return new Impl21OnApplyWindowInsetsListener(view, callback);
}
- @NonNull
- static BoundsCompat computeAnimationBounds(
+ static @NonNull BoundsCompat computeAnimationBounds(
@NonNull WindowInsetsCompat targetInsets,
@NonNull WindowInsetsCompat startingInsets, int mask) {
Insets targetInsetsInsets = targetInsets.getInsets(mask);
@@ -705,8 +695,7 @@
* This allows for a smoother animation especially in the common case of showing and hiding
* the IME.
*/
- @Nullable
- static Interpolator createInsetInterpolator(int showingTypes, int hidingTypes) {
+ static @Nullable Interpolator createInsetInterpolator(int showingTypes, int hidingTypes) {
if ((showingTypes & WindowInsetsCompat.Type.ime()) != 0) {
return SHOW_IME_INTERPOLATOR;
} else if ((hidingTypes & WindowInsetsCompat.Type.ime()) != 0) {
@@ -771,9 +760,9 @@
: null;
}
- @NonNull
@Override
- public WindowInsets onApplyWindowInsets(final View v, @NonNull WindowInsets insets) {
+ public @NonNull WindowInsets onApplyWindowInsets(final View v,
+ @NonNull WindowInsets insets) {
// We cannot rely on the compat insets value until the view is laid out.
if (!v.isLaidOut()) {
mLastInsets = toWindowInsetsCompat(insets, v);
@@ -889,8 +878,8 @@
* Forward the call to view.onApplyWindowInsets if there is no other listener attached to
* the view.
*/
- @NonNull
- static WindowInsets forwardToViewIfNeeded(@NonNull View v, @NonNull WindowInsets insets) {
+ static @NonNull WindowInsets forwardToViewIfNeeded(@NonNull View v,
+ @NonNull WindowInsets insets) {
// If the app set an on apply window listener, it will be called after this
// and will decide whether to call the view's onApplyWindowInsets.
if (v.getTag(R.id.tag_on_apply_window_listener) != null) {
@@ -978,8 +967,7 @@
}
}
- @Nullable
- static Callback getCallback(View child) {
+ static @Nullable Callback getCallback(View child) {
Object listener = child.getTag(
R.id.tag_window_insets_animation_callback);
Callback callback = null;
@@ -993,8 +981,7 @@
@RequiresApi(30)
private static class Impl30 extends Impl {
- @NonNull
- private final WindowInsetsAnimation mWrapped;
+ private final @NonNull WindowInsetsAnimation mWrapped;
Impl30(@NonNull WindowInsetsAnimation wrapped) {
super(0, null, 0);
@@ -1011,8 +998,7 @@
}
@Override
- @Nullable
- public Interpolator getInterpolator() {
+ public @Nullable Interpolator getInterpolator() {
return mWrapped.getInterpolator();
}
@@ -1051,7 +1037,7 @@
private final Callback mCompat;
- ProxyCallback(@NonNull final WindowInsetsAnimationCompat.Callback compat) {
+ ProxyCallback(final WindowInsetsAnimationCompat.@NonNull Callback compat) {
super(compat.getDispatchMode());
mCompat = compat;
}
@@ -1061,8 +1047,7 @@
private final HashMap<WindowInsetsAnimation, WindowInsetsAnimationCompat>
mAnimations = new HashMap<>();
- @NonNull
- private WindowInsetsAnimationCompat getWindowInsetsAnimationCompat(
+ private @NonNull WindowInsetsAnimationCompat getWindowInsetsAnimationCompat(
@NonNull WindowInsetsAnimation animation) {
WindowInsetsAnimationCompat animationCompat = mAnimations.get(
animation);
@@ -1078,19 +1063,17 @@
mCompat.onPrepare(getWindowInsetsAnimationCompat(animation));
}
- @NonNull
@Override
- public WindowInsetsAnimation.Bounds onStart(
+ public WindowInsetsAnimation.@NonNull Bounds onStart(
@NonNull WindowInsetsAnimation animation,
- @NonNull WindowInsetsAnimation.Bounds bounds) {
+ WindowInsetsAnimation.@NonNull Bounds bounds) {
return mCompat.onStart(
getWindowInsetsAnimationCompat(animation),
BoundsCompat.toBoundsCompat(bounds)).toBounds();
}
- @NonNull
@Override
- public WindowInsets onProgress(@NonNull WindowInsets insets,
+ public @NonNull WindowInsets onProgress(@NonNull WindowInsets insets,
@NonNull List<WindowInsetsAnimation> runningAnimations) {
if (mTmpRunningAnimations == null) {
mTmpRunningAnimations = new ArrayList<>(runningAnimations.size());
@@ -1124,20 +1107,18 @@
view.setWindowInsetsAnimationCallback(platformCallback);
}
- @NonNull
- public static WindowInsetsAnimation.Bounds createPlatformBounds(
+ public static WindowInsetsAnimation.@NonNull Bounds createPlatformBounds(
@NonNull BoundsCompat bounds) {
return new WindowInsetsAnimation.Bounds(bounds.getLowerBound().toPlatformInsets(),
bounds.getUpperBound().toPlatformInsets());
}
- @NonNull
- public static Insets getLowerBounds(@NonNull WindowInsetsAnimation.Bounds bounds) {
+ public static @NonNull Insets getLowerBounds(WindowInsetsAnimation.@NonNull Bounds bounds) {
return Insets.toCompatInsets(bounds.getLowerBound());
}
- @NonNull
- public static Insets getHigherBounds(@NonNull WindowInsetsAnimation.Bounds bounds) {
+ public static @NonNull Insets getHigherBounds(
+ WindowInsetsAnimation.@NonNull Bounds bounds) {
return Insets.toCompatInsets(bounds.getUpperBound());
}
}
diff --git a/core/core/src/main/java/androidx/core/view/WindowInsetsAnimationControlListenerCompat.java b/core/core/src/main/java/androidx/core/view/WindowInsetsAnimationControlListenerCompat.java
index cc27431..528168d 100644
--- a/core/core/src/main/java/androidx/core/view/WindowInsetsAnimationControlListenerCompat.java
+++ b/core/core/src/main/java/androidx/core/view/WindowInsetsAnimationControlListenerCompat.java
@@ -18,11 +18,12 @@
import android.view.inputmethod.EditorInfo;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.view.WindowInsetsCompat.Type;
import androidx.core.view.WindowInsetsCompat.Type.InsetsType;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Listener that encapsulates a request to
* {@link WindowInsetsControllerCompat#controlWindowInsetsAnimation}.
diff --git a/core/core/src/main/java/androidx/core/view/WindowInsetsAnimationControllerCompat.java b/core/core/src/main/java/androidx/core/view/WindowInsetsAnimationControllerCompat.java
index 964a06f..f809808 100644
--- a/core/core/src/main/java/androidx/core/view/WindowInsetsAnimationControllerCompat.java
+++ b/core/core/src/main/java/androidx/core/view/WindowInsetsAnimationControllerCompat.java
@@ -21,13 +21,14 @@
import android.view.WindowInsetsAnimationController;
import androidx.annotation.FloatRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.graphics.Insets;
import androidx.core.view.WindowInsetsCompat.Type;
import androidx.core.view.WindowInsetsCompat.Type.InsetsType;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Controller for app-driven animation of system windows.
* <p>
@@ -61,8 +62,7 @@
* @return Insets when the windows this animation is controlling are fully hidden.
* @see WindowInsetsAnimationCompat.BoundsCompat#getLowerBound()
*/
- @NonNull
- public Insets getHiddenStateInsets() {
+ public @NonNull Insets getHiddenStateInsets() {
return mImpl.getHiddenStateInsets();
}
@@ -80,8 +80,7 @@
* @return Insets when the windows this animation is controlling are fully shown.
* @see WindowInsetsAnimationCompat.BoundsCompat#getUpperBound()
*/
- @NonNull
- public Insets getShownStateInsets() {
+ public @NonNull Insets getShownStateInsets() {
return mImpl.getShownStateInsets();
}
@@ -95,8 +94,7 @@
* @return The current insets on the currently showing frame. These insets will change as the
* animation progresses to reflect the current insets provided by the controlled window.
*/
- @NonNull
- public Insets getCurrentInsets() {
+ public @NonNull Insets getCurrentInsets() {
return mImpl.getCurrentInsets();
}
@@ -230,18 +228,15 @@
//privatex
}
- @NonNull
- public Insets getHiddenStateInsets() {
+ public @NonNull Insets getHiddenStateInsets() {
return Insets.NONE;
}
- @NonNull
- public Insets getShownStateInsets() {
+ public @NonNull Insets getShownStateInsets() {
return Insets.NONE;
}
- @NonNull
- public Insets getCurrentInsets() {
+ public @NonNull Insets getCurrentInsets() {
return Insets.NONE;
}
@@ -285,21 +280,18 @@
mController = controller;
}
- @NonNull
@Override
- public Insets getHiddenStateInsets() {
+ public @NonNull Insets getHiddenStateInsets() {
return Insets.toCompatInsets(mController.getHiddenStateInsets());
}
- @NonNull
@Override
- public Insets getShownStateInsets() {
+ public @NonNull Insets getShownStateInsets() {
return Insets.toCompatInsets(mController.getShownStateInsets());
}
- @NonNull
@Override
- public Insets getCurrentInsets() {
+ public @NonNull Insets getCurrentInsets() {
return Insets.toCompatInsets(mController.getCurrentInsets());
}
diff --git a/core/core/src/main/java/androidx/core/view/WindowInsetsCompat.java b/core/core/src/main/java/androidx/core/view/WindowInsetsCompat.java
index 819e4f2..ab3f0b1 100644
--- a/core/core/src/main/java/androidx/core/view/WindowInsetsCompat.java
+++ b/core/core/src/main/java/androidx/core/view/WindowInsetsCompat.java
@@ -31,8 +31,6 @@
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.graphics.Insets;
@@ -40,6 +38,9 @@
import androidx.core.util.Preconditions;
import androidx.core.view.WindowInsetsCompat.Type.InsetsType;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Constructor;
@@ -71,8 +72,7 @@
*
* @see #isConsumed()
*/
- @NonNull
- public static final WindowInsetsCompat CONSUMED;
+ public static final @NonNull WindowInsetsCompat CONSUMED;
static {
if (SDK_INT >= 34) {
@@ -110,7 +110,7 @@
*
* @param src source from which values are copied
*/
- public WindowInsetsCompat(@Nullable final WindowInsetsCompat src) {
+ public WindowInsetsCompat(final @Nullable WindowInsetsCompat src) {
if (src != null) {
// We'll copy over from the 'src' instance's impl
final Impl srcImpl = src.mImpl;
@@ -148,9 +148,8 @@
* @param insets source insets to wrap
* @return the wrapped instance
*/
- @NonNull
@RequiresApi(20)
- public static WindowInsetsCompat toWindowInsetsCompat(@NonNull WindowInsets insets) {
+ public static @NonNull WindowInsetsCompat toWindowInsetsCompat(@NonNull WindowInsets insets) {
return toWindowInsetsCompat(insets, null);
}
@@ -167,9 +166,8 @@
* view needs be attached to the window, otherwise it will be ignored.
* @return the wrapped instance
*/
- @NonNull
@RequiresApi(20)
- public static WindowInsetsCompat toWindowInsetsCompat(@NonNull WindowInsets insets,
+ public static @NonNull WindowInsetsCompat toWindowInsetsCompat(@NonNull WindowInsets insets,
@Nullable View view) {
WindowInsetsCompat wic = new WindowInsetsCompat(Preconditions.checkNotNull(insets));
if (view != null && view.isAttachedToWindow()) {
@@ -321,8 +319,7 @@
* {@link #CONSUMED} instead to stop dispatching insets.
*/
@Deprecated
- @NonNull
- public WindowInsetsCompat consumeSystemWindowInsets() {
+ public @NonNull WindowInsetsCompat consumeSystemWindowInsets() {
return mImpl.consumeSystemWindowInsets();
}
@@ -342,8 +339,8 @@
*/
@SuppressWarnings("deprecation") // Builder.setSystemWindowInsets
@Deprecated
- @NonNull
- public WindowInsetsCompat replaceSystemWindowInsets(int left, int top, int right, int bottom) {
+ public @NonNull WindowInsetsCompat replaceSystemWindowInsets(int left, int top, int right,
+ int bottom) {
return new Builder(this)
.setSystemWindowInsets(Insets.of(left, top, right, bottom))
.build();
@@ -363,8 +360,7 @@
*/
@SuppressWarnings("deprecation")
@Deprecated
- @NonNull
- public WindowInsetsCompat replaceSystemWindowInsets(@NonNull Rect systemWindowInsets) {
+ public @NonNull WindowInsetsCompat replaceSystemWindowInsets(@NonNull Rect systemWindowInsets) {
return new Builder(this)
.setSystemWindowInsets(Insets.of(systemWindowInsets))
.build();
@@ -480,8 +476,7 @@
* {@link #CONSUMED} instead to stop dispatching insets.
*/
@Deprecated
- @NonNull
- public WindowInsetsCompat consumeStableInsets() {
+ public @NonNull WindowInsetsCompat consumeStableInsets() {
return mImpl.consumeStableInsets();
}
@@ -493,8 +488,7 @@
* @return the display cutout or null if there is none
* @see DisplayCutoutCompat
*/
- @Nullable
- public DisplayCutoutCompat getDisplayCutout() {
+ public @Nullable DisplayCutoutCompat getDisplayCutout() {
return mImpl.getDisplayCutout();
}
@@ -509,8 +503,7 @@
* {@link #CONSUMED} instead to stop dispatching insets.
*/
@Deprecated
- @NonNull
- public WindowInsetsCompat consumeDisplayCutout() {
+ public @NonNull WindowInsetsCompat consumeDisplayCutout() {
return mImpl.consumeDisplayCutout();
}
@@ -529,8 +522,7 @@
* @deprecated Use {@link #getInsets(int)} with {@link Type#systemBars()} instead.
*/
@Deprecated
- @NonNull
- public Insets getSystemWindowInsets() {
+ public @NonNull Insets getSystemWindowInsets() {
return mImpl.getSystemWindowInsets();
}
@@ -552,8 +544,7 @@
* instead.
*/
@Deprecated
- @NonNull
- public Insets getStableInsets() {
+ public @NonNull Insets getStableInsets() {
return mImpl.getStableInsets();
}
@@ -569,8 +560,7 @@
* instead.
*/
@Deprecated
- @NonNull
- public Insets getMandatorySystemGestureInsets() {
+ public @NonNull Insets getMandatorySystemGestureInsets() {
return mImpl.getMandatorySystemGestureInsets();
}
@@ -588,8 +578,7 @@
* instead.
*/
@Deprecated
- @NonNull
- public Insets getTappableElementInsets() {
+ public @NonNull Insets getTappableElementInsets() {
return mImpl.getTappableElementInsets();
}
@@ -609,8 +598,7 @@
* instead.
*/
@Deprecated
- @NonNull
- public Insets getSystemGestureInsets() {
+ public @NonNull Insets getSystemGestureInsets() {
return mImpl.getSystemGestureInsets();
}
@@ -629,8 +617,7 @@
*
* @see #inset(int, int, int, int)
*/
- @NonNull
- public WindowInsetsCompat inset(@NonNull Insets insets) {
+ public @NonNull WindowInsetsCompat inset(@NonNull Insets insets) {
return inset(insets.left, insets.top, insets.right, insets.bottom);
}
@@ -653,9 +640,9 @@
*
* @return the inset insets
*/
- @NonNull
- public WindowInsetsCompat inset(@IntRange(from = 0) int left, @IntRange(from = 0) int top,
- @IntRange(from = 0) int right, @IntRange(from = 0) int bottom) {
+ public @NonNull WindowInsetsCompat inset(@IntRange(from = 0) int left,
+ @IntRange(from = 0) int top, @IntRange(from = 0) int right,
+ @IntRange(from = 0) int bottom) {
return mImpl.inset(left, top, right, bottom);
}
@@ -671,8 +658,7 @@
* @param typeMask Bit mask of {@link Type}s to query the insets for.
* @return The insets.
*/
- @NonNull
- public Insets getInsets(@InsetsType int typeMask) {
+ public @NonNull Insets getInsets(@InsetsType int typeMask) {
return mImpl.getInsets(typeMask);
}
@@ -699,8 +685,7 @@
* IME is dynamic depending on the {@link EditorInfo} of the
* currently focused view, as well as the UI state of the IME.
*/
- @NonNull
- public Insets getInsetsIgnoringVisibility(@InsetsType int typeMask) {
+ public @NonNull Insets getInsetsIgnoringVisibility(@InsetsType int typeMask) {
return mImpl.getInsetsIgnoringVisibility(typeMask);
}
@@ -743,16 +728,14 @@
*
* @return the wrapped WindowInsets instance
*/
- @Nullable
@RequiresApi(20)
- public WindowInsets toWindowInsets() {
+ public @Nullable WindowInsets toWindowInsets() {
return mImpl instanceof Impl20 ? ((Impl20) mImpl).mPlatformInsets : null;
}
private static class Impl {
@SuppressWarnings("deprecation")
- @NonNull
- static final WindowInsetsCompat CONSUMED = new WindowInsetsCompat.Builder()
+ static final @NonNull WindowInsetsCompat CONSUMED = new WindowInsetsCompat.Builder()
.build()
.consumeDisplayCutout()
.consumeStableInsets()
@@ -772,66 +755,54 @@
return false;
}
- @NonNull
- WindowInsetsCompat consumeSystemWindowInsets() {
+ @NonNull WindowInsetsCompat consumeSystemWindowInsets() {
return mHost;
}
- @NonNull
- WindowInsetsCompat consumeStableInsets() {
+ @NonNull WindowInsetsCompat consumeStableInsets() {
return mHost;
}
- @Nullable
- DisplayCutoutCompat getDisplayCutout() {
+ @Nullable DisplayCutoutCompat getDisplayCutout() {
return null;
}
- @NonNull
- WindowInsetsCompat consumeDisplayCutout() {
+ @NonNull WindowInsetsCompat consumeDisplayCutout() {
return mHost;
}
- @NonNull
- Insets getSystemWindowInsets() {
+ @NonNull Insets getSystemWindowInsets() {
return Insets.NONE;
}
- @NonNull
- Insets getStableInsets() {
+ @NonNull Insets getStableInsets() {
return Insets.NONE;
}
- @NonNull
- Insets getSystemGestureInsets() {
+ @NonNull Insets getSystemGestureInsets() {
// Pre-Q return the system window insets
return getSystemWindowInsets();
}
- @NonNull
- Insets getMandatorySystemGestureInsets() {
+ @NonNull Insets getMandatorySystemGestureInsets() {
// Pre-Q return the system window insets
return getSystemWindowInsets();
}
- @NonNull
- Insets getTappableElementInsets() {
+ @NonNull Insets getTappableElementInsets() {
// Pre-Q return the system window insets
return getSystemWindowInsets();
}
- @NonNull
- WindowInsetsCompat inset(int left, int top, int right, int bottom) {
+ @NonNull WindowInsetsCompat inset(int left, int top, int right, int bottom) {
return CONSUMED;
}
- @NonNull
- Insets getInsets(@InsetsType int typeMask) {
+ @NonNull Insets getInsets(@InsetsType int typeMask) {
return Insets.NONE;
}
- @NonNull
- Insets getInsetsIgnoringVisibility(@InsetsType int typeMask) {
+ @NonNull Insets getInsetsIgnoringVisibility(@InsetsType int typeMask) {
if ((typeMask & Type.IME) != 0) {
throw new IllegalArgumentException("Unable to query the maximum insets for IME");
}
@@ -895,8 +866,7 @@
private static Field sVisibleInsetsField;
private static Field sAttachInfoField;
- @NonNull
- final WindowInsets mPlatformInsets;
+ final @NonNull WindowInsets mPlatformInsets;
// TODO(175859616) save all insets in the array
private Insets[] mOverriddenInsets;
@@ -923,15 +893,13 @@
return mPlatformInsets.isRound();
}
- @NonNull
@Override
- public Insets getInsets(int typeMask) {
+ public @NonNull Insets getInsets(int typeMask) {
return getInsets(typeMask, false);
}
- @NonNull
@Override
- public Insets getInsetsIgnoringVisibility(int typeMask) {
+ public @NonNull Insets getInsetsIgnoringVisibility(int typeMask) {
return getInsets(typeMask, true);
}
@@ -950,8 +918,7 @@
}
@SuppressLint("WrongConstant")
- @NonNull
- private Insets getInsets(final int typeMask, final boolean ignoreVisibility) {
+ private @NonNull Insets getInsets(final int typeMask, final boolean ignoreVisibility) {
Insets result = Insets.NONE;
for (int i = Type.FIRST; i <= Type.LAST; i = i << 1) {
if ((typeMask & i) == 0) {
@@ -963,8 +930,7 @@
}
@SuppressWarnings("deprecation")
- @NonNull
- protected Insets getInsetsForType(@InsetsType int type, boolean ignoreVisibility) {
+ protected @NonNull Insets getInsetsForType(@InsetsType int type, boolean ignoreVisibility) {
switch (type) {
case Type.STATUS_BARS: {
if (ignoreVisibility) {
@@ -1078,8 +1044,7 @@
}
@Override
- @NonNull
- final Insets getSystemWindowInsets() {
+ final @NonNull Insets getSystemWindowInsets() {
if (mSystemWindowInsets == null) {
mSystemWindowInsets = Insets.of(
mPlatformInsets.getSystemWindowInsetLeft(),
@@ -1090,10 +1055,9 @@
return mSystemWindowInsets;
}
- @NonNull
@Override
@SuppressWarnings("deprecation")
- WindowInsetsCompat inset(int left, int top, int right, int bottom) {
+ @NonNull WindowInsetsCompat inset(int left, int top, int right, int bottom) {
Builder b = new Builder(toWindowInsetsCompat(mPlatformInsets));
b.setSystemWindowInsets(insetInsets(getSystemWindowInsets(), left, top, right, bottom));
b.setStableInsets(insetInsets(getStableInsets(), left, top, right, bottom));
@@ -1145,8 +1109,7 @@
*
* @return a copy of the provided view's AttachInfo.mVisibleRect or null if anything fails
*/
- @Nullable
- private Insets getVisibleInsets(@NonNull View rootView) {
+ private @Nullable Insets getVisibleInsets(@NonNull View rootView) {
if (SDK_INT >= 30) {
throw new UnsupportedOperationException("getVisibleInsets() should not be called "
+ "on API >= 30. Use WindowInsets.isVisible() instead.");
@@ -1237,21 +1200,18 @@
return mPlatformInsets.isConsumed();
}
- @NonNull
@Override
- WindowInsetsCompat consumeStableInsets() {
+ @NonNull WindowInsetsCompat consumeStableInsets() {
return toWindowInsetsCompat(mPlatformInsets.consumeStableInsets());
}
- @NonNull
@Override
- WindowInsetsCompat consumeSystemWindowInsets() {
+ @NonNull WindowInsetsCompat consumeSystemWindowInsets() {
return toWindowInsetsCompat(mPlatformInsets.consumeSystemWindowInsets());
}
@Override
- @NonNull
- final Insets getStableInsets() {
+ final @NonNull Insets getStableInsets() {
if (mStableInsets == null) {
mStableInsets = Insets.of(
mPlatformInsets.getStableInsetLeft(),
@@ -1279,15 +1239,13 @@
super(host, other);
}
- @Nullable
@Override
- DisplayCutoutCompat getDisplayCutout() {
+ @Nullable DisplayCutoutCompat getDisplayCutout() {
return DisplayCutoutCompat.wrap(mPlatformInsets.getDisplayCutout());
}
- @NonNull
@Override
- WindowInsetsCompat consumeDisplayCutout() {
+ @NonNull WindowInsetsCompat consumeDisplayCutout() {
return toWindowInsetsCompat(mPlatformInsets.consumeDisplayCutout());
}
@@ -1324,18 +1282,16 @@
super(host, other);
}
- @NonNull
@Override
- Insets getSystemGestureInsets() {
+ @NonNull Insets getSystemGestureInsets() {
if (mSystemGestureInsets == null) {
mSystemGestureInsets = toCompatInsets(mPlatformInsets.getSystemGestureInsets());
}
return mSystemGestureInsets;
}
- @NonNull
@Override
- Insets getMandatorySystemGestureInsets() {
+ @NonNull Insets getMandatorySystemGestureInsets() {
if (mMandatorySystemGestureInsets == null) {
mMandatorySystemGestureInsets =
toCompatInsets(mPlatformInsets.getMandatorySystemGestureInsets());
@@ -1343,18 +1299,16 @@
return mMandatorySystemGestureInsets;
}
- @NonNull
@Override
- Insets getTappableElementInsets() {
+ @NonNull Insets getTappableElementInsets() {
if (mTappableElementInsets == null) {
mTappableElementInsets = toCompatInsets(mPlatformInsets.getTappableElementInsets());
}
return mTappableElementInsets;
}
- @NonNull
@Override
- WindowInsetsCompat inset(int left, int top, int right, int bottom) {
+ @NonNull WindowInsetsCompat inset(int left, int top, int right, int bottom) {
return toWindowInsetsCompat(mPlatformInsets.inset(left, top, right, bottom));
}
@@ -1377,8 +1331,8 @@
@RequiresApi(30)
private static class Impl30 extends Impl29 {
- @NonNull
- static final WindowInsetsCompat CONSUMED = toWindowInsetsCompat(WindowInsets.CONSUMED);
+ static final @NonNull WindowInsetsCompat CONSUMED =
+ toWindowInsetsCompat(WindowInsets.CONSUMED);
Impl30(@NonNull WindowInsetsCompat host, @NonNull WindowInsets insets) {
super(host, insets);
@@ -1388,17 +1342,15 @@
super(host, other);
}
- @NonNull
@Override
- public Insets getInsets(int typeMask) {
+ public @NonNull Insets getInsets(int typeMask) {
return toCompatInsets(
mPlatformInsets.getInsets(TypeImpl30.toPlatformType(typeMask))
);
}
- @NonNull
@Override
- public Insets getInsetsIgnoringVisibility(int typeMask) {
+ public @NonNull Insets getInsetsIgnoringVisibility(int typeMask) {
return toCompatInsets(
mPlatformInsets.getInsetsIgnoringVisibility(TypeImpl30.toPlatformType(typeMask))
);
@@ -1420,8 +1372,8 @@
@RequiresApi(34)
private static class Impl34 extends Impl30 {
- @NonNull
- static final WindowInsetsCompat CONSUMED = toWindowInsetsCompat(WindowInsets.CONSUMED);
+ static final @NonNull WindowInsetsCompat CONSUMED =
+ toWindowInsetsCompat(WindowInsets.CONSUMED);
Impl34(@NonNull WindowInsetsCompat host, @NonNull WindowInsets insets) {
super(host, insets);
@@ -1431,17 +1383,15 @@
super(host, other);
}
- @NonNull
@Override
- public Insets getInsets(int typeMask) {
+ public @NonNull Insets getInsets(int typeMask) {
return toCompatInsets(
mPlatformInsets.getInsets(TypeImpl34.toPlatformType(typeMask))
);
}
- @NonNull
@Override
- public Insets getInsetsIgnoringVisibility(int typeMask) {
+ public @NonNull Insets getInsetsIgnoringVisibility(int typeMask) {
return toCompatInsets(
mPlatformInsets.getInsetsIgnoringVisibility(TypeImpl34.toPlatformType(typeMask))
);
@@ -1507,8 +1457,7 @@
* @deprecated Use {@link #setInsets(int, Insets)} with {@link Type#systemBars()}.
*/
@Deprecated
- @NonNull
- public Builder setSystemWindowInsets(@NonNull Insets insets) {
+ public @NonNull Builder setSystemWindowInsets(@NonNull Insets insets) {
mImpl.setSystemWindowInsets(insets);
return this;
}
@@ -1527,8 +1476,7 @@
* @deprecated Use {@link #setInsets(int, Insets)} with {@link Type#systemGestures()}.
*/
@Deprecated
- @NonNull
- public Builder setSystemGestureInsets(@NonNull Insets insets) {
+ public @NonNull Builder setSystemGestureInsets(@NonNull Insets insets) {
mImpl.setSystemGestureInsets(insets);
return this;
}
@@ -1552,8 +1500,7 @@
* {@link Type#mandatorySystemGestures()}.
*/
@Deprecated
- @NonNull
- public Builder setMandatorySystemGestureInsets(@NonNull Insets insets) {
+ public @NonNull Builder setMandatorySystemGestureInsets(@NonNull Insets insets) {
mImpl.setMandatorySystemGestureInsets(insets);
return this;
}
@@ -1571,8 +1518,7 @@
* @deprecated Use {@link #setInsets(int, Insets)} with {@link Type#tappableElement()}.
*/
@Deprecated
- @NonNull
- public Builder setTappableElementInsets(@NonNull Insets insets) {
+ public @NonNull Builder setTappableElementInsets(@NonNull Insets insets) {
mImpl.setTappableElementInsets(insets);
return this;
}
@@ -1589,8 +1535,7 @@
* @return itself
* @see #getInsets(int)
*/
- @NonNull
- public Builder setInsets(@InsetsType int typeMask, @NonNull Insets insets) {
+ public @NonNull Builder setInsets(@InsetsType int typeMask, @NonNull Insets insets) {
mImpl.setInsets(typeMask, insets);
return this;
}
@@ -1614,8 +1559,7 @@
* state of the IME.
* @see #getInsetsIgnoringVisibility(int)
*/
- @NonNull
- public Builder setInsetsIgnoringVisibility(@InsetsType int typeMask,
+ public @NonNull Builder setInsetsIgnoringVisibility(@InsetsType int typeMask,
@NonNull Insets insets) {
mImpl.setInsetsIgnoringVisibility(typeMask, insets);
return this;
@@ -1629,8 +1573,7 @@
* @return itself
* @see #isVisible(int)
*/
- @NonNull
- public Builder setVisible(@InsetsType int typeMask, boolean visible) {
+ public @NonNull Builder setVisible(@InsetsType int typeMask, boolean visible) {
mImpl.setVisible(typeMask, visible);
return this;
}
@@ -1652,8 +1595,7 @@
* {@link Type#systemBars()}.
*/
@Deprecated
- @NonNull
- public Builder setStableInsets(@NonNull Insets insets) {
+ public @NonNull Builder setStableInsets(@NonNull Insets insets) {
mImpl.setStableInsets(insets);
return this;
}
@@ -1667,8 +1609,7 @@
* @return itself
* @see #getDisplayCutout()
*/
- @NonNull
- public Builder setDisplayCutout(@Nullable DisplayCutoutCompat displayCutout) {
+ public @NonNull Builder setDisplayCutout(@Nullable DisplayCutoutCompat displayCutout) {
mImpl.setDisplayCutout(displayCutout);
return this;
}
@@ -1678,8 +1619,7 @@
*
* @return the {@link WindowInsetsCompat} instance.
*/
- @NonNull
- public WindowInsetsCompat build() {
+ public @NonNull WindowInsetsCompat build() {
return mImpl.build();
}
}
@@ -1765,8 +1705,7 @@
}
}
- @NonNull
- WindowInsetsCompat build() {
+ @NonNull WindowInsetsCompat build() {
applyInsetTypes();
return mInsets;
}
@@ -1810,8 +1749,7 @@
}
@Override
- @NonNull
- WindowInsetsCompat build() {
+ @NonNull WindowInsetsCompat build() {
applyInsetTypes();
WindowInsetsCompat windowInsetsCompat = WindowInsetsCompat.toWindowInsetsCompat(
mPlatformInsets);
@@ -1820,9 +1758,8 @@
return windowInsetsCompat;
}
- @Nullable
@SuppressWarnings("JavaReflectionMemberAccess")
- private static WindowInsets createWindowInsetsInstance() {
+ private static @Nullable WindowInsets createWindowInsetsInstance() {
// On API 20-28, there is no public way to create an WindowInsets instance, so we
// need to use reflection.
@@ -1922,8 +1859,7 @@
}
@Override
- @NonNull
- WindowInsetsCompat build() {
+ @NonNull WindowInsetsCompat build() {
applyInsetTypes();
WindowInsetsCompat windowInsetsCompat = WindowInsetsCompat.toWindowInsetsCompat(
mPlatBuilder.build());
@@ -2344,8 +2280,7 @@
// Only called on SDK 21 and 22
@SuppressWarnings("deprecation")
- @Nullable
- public static WindowInsetsCompat getRootWindowInsets(@NonNull View v) {
+ public static @Nullable WindowInsetsCompat getRootWindowInsets(@NonNull View v) {
if (!sReflectionSucceeded || !v.isAttachedToWindow()) {
return null;
}
diff --git a/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java b/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java
index e74ec94..d6d7aa3 100644
--- a/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java
+++ b/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java
@@ -31,14 +31,15 @@
import android.view.animation.Interpolator;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.collection.SimpleArrayMap;
import androidx.core.graphics.Insets;
import androidx.core.view.WindowInsetsCompat.Type.InsetsType;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.TimeUnit;
@@ -151,10 +152,9 @@
* {@link WindowInsetsControllerCompat}
* @deprecated Use {@link WindowCompat#getInsetsController(Window, View)} instead
*/
- @NonNull
@RequiresApi(30)
@Deprecated
- public static WindowInsetsControllerCompat toWindowInsetsControllerCompat(
+ public static @NonNull WindowInsetsControllerCompat toWindowInsetsControllerCompat(
@NonNull WindowInsetsController insetsController) {
return new WindowInsetsControllerCompat(insetsController);
}
@@ -340,7 +340,7 @@
*WindowInsetsControllerCompat.OnControllableInsetsChangedListener)
*/
public void addOnControllableInsetsChangedListener(
- @NonNull WindowInsetsControllerCompat.OnControllableInsetsChangedListener listener) {
+ WindowInsetsControllerCompat.@NonNull OnControllableInsetsChangedListener listener) {
mImpl.addOnControllableInsetsChangedListener(listener);
}
@@ -354,7 +354,7 @@
*WindowInsetsControllerCompat.OnControllableInsetsChangedListener)
*/
public void removeOnControllableInsetsChangedListener(
- @NonNull WindowInsetsControllerCompat.OnControllableInsetsChangedListener
+ WindowInsetsControllerCompat.@NonNull OnControllableInsetsChangedListener
listener) {
mImpl.removeOnControllableInsetsChangedListener(listener);
}
@@ -436,7 +436,7 @@
}
void removeOnControllableInsetsChangedListener(
- @NonNull WindowInsetsControllerCompat.OnControllableInsetsChangedListener
+ WindowInsetsControllerCompat.@NonNull OnControllableInsetsChangedListener
listener) {
}
}
@@ -444,11 +444,9 @@
@RequiresApi(20)
private static class Impl20 extends Impl {
- @NonNull
- protected final Window mWindow;
+ protected final @NonNull Window mWindow;
- @NonNull
- private final SoftwareKeyboardControllerCompat mSoftwareKeyboardControllerCompat;
+ private final @NonNull SoftwareKeyboardControllerCompat mSoftwareKeyboardControllerCompat;
Impl20(@NonNull Window window,
@NonNull SoftwareKeyboardControllerCompat softwareKeyboardControllerCompat) {
@@ -565,7 +563,7 @@
@Override
void removeOnControllableInsetsChangedListener(
- @NonNull WindowInsetsControllerCompat.OnControllableInsetsChangedListener
+ WindowInsetsControllerCompat.@NonNull OnControllableInsetsChangedListener
listener) {
}
}
@@ -738,7 +736,7 @@
void controlWindowInsetsAnimation(@InsetsType int types, long durationMillis,
@Nullable Interpolator interpolator,
@Nullable CancellationSignal cancellationSignal,
- @NonNull final WindowInsetsAnimationControlListenerCompat listener) {
+ final @NonNull WindowInsetsAnimationControlListenerCompat listener) {
WindowInsetsAnimationControlListener fwListener =
new WindowInsetsAnimationControlListener() {
@@ -825,7 +823,7 @@
@Override
void addOnControllableInsetsChangedListener(
- @NonNull final WindowInsetsControllerCompat.OnControllableInsetsChangedListener
+ final WindowInsetsControllerCompat.@NonNull OnControllableInsetsChangedListener
listener) {
if (mListeners.containsKey(listener)) {
@@ -845,7 +843,7 @@
@Override
void removeOnControllableInsetsChangedListener(
- @NonNull WindowInsetsControllerCompat.OnControllableInsetsChangedListener
+ WindowInsetsControllerCompat.@NonNull OnControllableInsetsChangedListener
listener) {
WindowInsetsController.OnControllableInsetsChangedListener
fwListener = mListeners.remove(listener);
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityClickableSpanCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityClickableSpanCompat.java
index 543b36a..d390c49 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityClickableSpanCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityClickableSpanCompat.java
@@ -21,9 +21,10 @@
import android.text.style.ClickableSpan;
import android.view.View;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
/**
* {@link ClickableSpan} cannot be parceled, but accessibility services need to be able to cause
* their callback handlers to be called. This class serves as a placeholder for the
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
index 307aa6b..4fc0997 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
@@ -26,10 +26,11 @@
import android.widget.EditText;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityManagerCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityManagerCompat.java
index 12be024..e80ecf8 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityManagerCompat.java
@@ -20,9 +20,10 @@
import android.os.Build;
import android.view.accessibility.AccessibilityManager;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
import java.util.List;
/**
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
index c452246..e594a8c 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
@@ -43,8 +43,6 @@
import android.view.accessibility.AccessibilityNodeInfo.TouchDelegateInfo;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
@@ -60,6 +58,9 @@
import androidx.core.view.accessibility.AccessibilityViewCommand.SetSelectionArguments;
import androidx.core.view.accessibility.AccessibilityViewCommand.SetTextArguments;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.ref.WeakReference;
import java.time.Duration;
import java.util.ArrayList;
@@ -516,8 +517,7 @@
/**
* Action to move to the page above.
*/
- @NonNull
- public static final AccessibilityActionCompat ACTION_PAGE_UP =
+ public static final @NonNull AccessibilityActionCompat ACTION_PAGE_UP =
new AccessibilityActionCompat(Build.VERSION.SDK_INT >= 29
? AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_UP : null,
android.R.id.accessibilityActionPageUp, null, null, null);
@@ -525,8 +525,7 @@
/**
* Action to move to the page below.
*/
- @NonNull
- public static final AccessibilityActionCompat ACTION_PAGE_DOWN =
+ public static final @NonNull AccessibilityActionCompat ACTION_PAGE_DOWN =
new AccessibilityActionCompat(Build.VERSION.SDK_INT >= 29
? AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_DOWN : null,
android.R.id.accessibilityActionPageDown, null, null, null);
@@ -534,8 +533,7 @@
/**
* Action to move to the page left.
*/
- @NonNull
- public static final AccessibilityActionCompat ACTION_PAGE_LEFT =
+ public static final @NonNull AccessibilityActionCompat ACTION_PAGE_LEFT =
new AccessibilityActionCompat(Build.VERSION.SDK_INT >= 29
? AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT : null,
android.R.id.accessibilityActionPageLeft, null, null, null);
@@ -543,8 +541,7 @@
/**
* Action to move to the page right.
*/
- @NonNull
- public static final AccessibilityActionCompat ACTION_PAGE_RIGHT =
+ public static final @NonNull AccessibilityActionCompat ACTION_PAGE_RIGHT =
new AccessibilityActionCompat(Build.VERSION.SDK_INT >= 29
? AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT : null,
android.R.id.accessibilityActionPageRight, null, null, null);
@@ -640,7 +637,7 @@
* delays will lead to unexpected UI behavior.
* <p>
*/
- @NonNull public static final AccessibilityActionCompat ACTION_PRESS_AND_HOLD =
+ public static final @NonNull AccessibilityActionCompat ACTION_PRESS_AND_HOLD =
new AccessibilityActionCompat(Build.VERSION.SDK_INT >= 30
? AccessibilityNodeInfo.AccessibilityAction.ACTION_PRESS_AND_HOLD : null,
android.R.id.accessibilityActionPressAndHold, null, null, null);
@@ -653,7 +650,7 @@
* actionId has set. A node should expose this action only for views that are currently
* with input focus and editable.
*/
- @NonNull public static final AccessibilityActionCompat ACTION_IME_ENTER =
+ public static final @NonNull AccessibilityActionCompat ACTION_IME_ENTER =
new AccessibilityActionCompat(Build.VERSION.SDK_INT >= 30
? AccessibilityNodeInfo.AccessibilityAction.ACTION_IME_ENTER : null,
android.R.id.accessibilityActionImeEnter, null, null, null);
@@ -669,8 +666,7 @@
*
* @see AccessibilityEventCompat#CONTENT_CHANGE_TYPE_DRAG_STARTED
*/
- @NonNull
- public static final AccessibilityActionCompat ACTION_DRAG_START =
+ public static final @NonNull AccessibilityActionCompat ACTION_DRAG_START =
new AccessibilityActionCompat(Build.VERSION.SDK_INT >= 32
? AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_START : null,
android.R.id.accessibilityActionDragStart, null, null, null);
@@ -686,8 +682,7 @@
*
* @see AccessibilityEventCompat#CONTENT_CHANGE_TYPE_DRAG_DROPPED
*/
- @NonNull
- public static final AccessibilityActionCompat ACTION_DRAG_DROP =
+ public static final @NonNull AccessibilityActionCompat ACTION_DRAG_DROP =
new AccessibilityActionCompat(Build.VERSION.SDK_INT >= 32
? AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_DROP : null,
android.R.id.accessibilityActionDragDrop, null, null, null);
@@ -700,8 +695,7 @@
*
* @see AccessibilityEventCompat#CONTENT_CHANGE_TYPE_DRAG_CANCELLED
*/
- @NonNull
- public static final AccessibilityActionCompat ACTION_DRAG_CANCEL =
+ public static final @NonNull AccessibilityActionCompat ACTION_DRAG_CANCEL =
new AccessibilityActionCompat(Build.VERSION.SDK_INT >= 32
? AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_CANCEL : null,
android.R.id.accessibilityActionDragCancel, null, null, null);
@@ -709,8 +703,7 @@
/**
* Action to show suggestions for editable text.
*/
- @NonNull
- public static final AccessibilityActionCompat ACTION_SHOW_TEXT_SUGGESTIONS =
+ public static final @NonNull AccessibilityActionCompat ACTION_SHOW_TEXT_SUGGESTIONS =
new AccessibilityActionCompat(Build.VERSION.SDK_INT >= 33
? AccessibilityNodeInfo.AccessibilityAction.ACTION_SHOW_TEXT_SUGGESTIONS
: null, android.R.id.accessibilityActionShowTextSuggestions, null,
@@ -739,9 +732,8 @@
* required argument.<br>
* </p>
*/
- @NonNull
@OptIn(markerClass = androidx.core.os.BuildCompat.PrereleaseSdkCheck.class)
- public static final AccessibilityActionCompat ACTION_SCROLL_IN_DIRECTION =
+ public static final @NonNull AccessibilityActionCompat ACTION_SCROLL_IN_DIRECTION =
new AccessibilityActionCompat(
Build.VERSION.SDK_INT >= 34 ? Api34Impl.getActionScrollInDirection() : null,
android.R.id.accessibilityActionScrollInDirection, null, null, null);
@@ -880,9 +872,8 @@
return true;
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
StringBuilder builder = new StringBuilder();
builder.append("AccessibilityActionCompat: ");
// Mirror AccessibilityNodeInfoCompat.toString's action string.
@@ -1101,8 +1092,7 @@
* @param rowCount The number of rows in the collection.
* @return This builder.
*/
- @NonNull
- public CollectionInfoCompat.Builder setRowCount(int rowCount) {
+ public CollectionInfoCompat.@NonNull Builder setRowCount(int rowCount) {
mRowCount = rowCount;
return this;
}
@@ -1112,8 +1102,7 @@
* @param columnCount The number of columns in the collection.
* @return This builder.
*/
- @NonNull
- public CollectionInfoCompat.Builder setColumnCount(int columnCount) {
+ public CollectionInfoCompat.@NonNull Builder setColumnCount(int columnCount) {
mColumnCount = columnCount;
return this;
}
@@ -1122,8 +1111,7 @@
* @param hierarchical Whether the collection is hierarchical.
* @return This builder.
*/
- @NonNull
- public CollectionInfoCompat.Builder setHierarchical(boolean hierarchical) {
+ public CollectionInfoCompat.@NonNull Builder setHierarchical(boolean hierarchical) {
mHierarchical = hierarchical;
return this;
}
@@ -1133,8 +1121,7 @@
* @param selectionMode The selection mode.
* @return This builder.
*/
- @NonNull
- public CollectionInfoCompat.Builder setSelectionMode(int selectionMode) {
+ public CollectionInfoCompat.@NonNull Builder setSelectionMode(int selectionMode) {
mSelectionMode = selectionMode;
return this;
}
@@ -1147,8 +1134,7 @@
* {@code UNDEFINED} if the item count is not known.
* @return This builder.
*/
- @NonNull
- public CollectionInfoCompat.Builder setItemCount(int itemCount) {
+ public CollectionInfoCompat.@NonNull Builder setItemCount(int itemCount) {
mItemCount = itemCount;
return this;
}
@@ -1159,8 +1145,7 @@
* accessibility.
* @return This builder.
*/
- @NonNull
- public CollectionInfoCompat.Builder setImportantForAccessibilityItemCount(
+ public CollectionInfoCompat.@NonNull Builder setImportantForAccessibilityItemCount(
int importantForAccessibilityItemCount) {
mImportantForAccessibilityItemCount = importantForAccessibilityItemCount;
return this;
@@ -1169,8 +1154,7 @@
/**
* Creates a new {@link CollectionInfoCompat} instance.
*/
- @NonNull
- public CollectionInfoCompat build() {
+ public @NonNull CollectionInfoCompat build() {
if (Build.VERSION.SDK_INT >= 35) {
return Api35Impl.buildCollectionInfoCompat(mRowCount, mColumnCount,
mHierarchical, mSelectionMode, mItemCount,
@@ -1301,8 +1285,7 @@
*
* @return The row title.
*/
- @Nullable
- public String getRowTitle() {
+ public @Nullable String getRowTitle() {
if (Build.VERSION.SDK_INT >= 33) {
return Api33Impl.getCollectionItemRowTitle(mInfo);
} else {
@@ -1315,8 +1298,7 @@
*
* @return The column title.
*/
- @Nullable
- public String getColumnTitle() {
+ public @Nullable String getColumnTitle() {
if (Build.VERSION.SDK_INT >= 33) {
return Api33Impl.getCollectionItemColumnTitle(mInfo);
} else {
@@ -1349,8 +1331,7 @@
* @param heading The heading state
* @return This builder
*/
- @NonNull
- public Builder setHeading(boolean heading) {
+ public @NonNull Builder setHeading(boolean heading) {
mHeading = heading;
return this;
}
@@ -1361,8 +1342,7 @@
* @param columnIndex The column index
* @return This builder
*/
- @NonNull
- public Builder setColumnIndex(int columnIndex) {
+ public @NonNull Builder setColumnIndex(int columnIndex) {
mColumnIndex = columnIndex;
return this;
}
@@ -1373,8 +1353,7 @@
* @param rowIndex The row index
* @return This builder
*/
- @NonNull
- public Builder setRowIndex(int rowIndex) {
+ public @NonNull Builder setRowIndex(int rowIndex) {
mRowIndex = rowIndex;
return this;
}
@@ -1385,8 +1364,7 @@
* @param columnSpan The number of columns spans
* @return This builder
*/
- @NonNull
- public Builder setColumnSpan(int columnSpan) {
+ public @NonNull Builder setColumnSpan(int columnSpan) {
mColumnSpan = columnSpan;
return this;
}
@@ -1397,8 +1375,7 @@
* @param rowSpan The number of rows spans
* @return This builder
*/
- @NonNull
- public Builder setRowSpan(int rowSpan) {
+ public @NonNull Builder setRowSpan(int rowSpan) {
mRowSpan = rowSpan;
return this;
}
@@ -1409,8 +1386,7 @@
* @param selected The number of rows spans
* @return This builder
*/
- @NonNull
- public Builder setSelected(boolean selected) {
+ public @NonNull Builder setSelected(boolean selected) {
mSelected = selected;
return this;
}
@@ -1421,8 +1397,7 @@
* @param rowTitle The row title
* @return This builder
*/
- @NonNull
- public Builder setRowTitle(@Nullable String rowTitle) {
+ public @NonNull Builder setRowTitle(@Nullable String rowTitle) {
mRowTitle = rowTitle;
return this;
}
@@ -1433,8 +1408,7 @@
* @param columnTitle The column title
* @return This builder
*/
- @NonNull
- public Builder setColumnTitle(@Nullable String columnTitle) {
+ public @NonNull Builder setColumnTitle(@Nullable String columnTitle) {
mColumnTitle = columnTitle;
return this;
}
@@ -1442,8 +1416,7 @@
/**
* Builds and returns a {@link AccessibilityNodeInfo.CollectionItemInfo}.
*/
- @NonNull
- public CollectionItemInfoCompat build() {
+ public @NonNull CollectionItemInfoCompat build() {
if (Build.VERSION.SDK_INT >= 33) {
return Api33Impl.buildCollectionItemInfoCompat(mHeading, mColumnIndex,
mRowIndex, mColumnSpan, mRowSpan, mSelected, mRowTitle, mColumnTitle);
@@ -1597,8 +1570,7 @@
* @param index The desired index, must be between 0 and {@link #getRegionCount()}-1.
* @return Returns the {@link Region} stored at the given index.
*/
- @Nullable
- public Region getRegionAt(@IntRange(from = 0) int index) {
+ public @Nullable Region getRegionAt(@IntRange(from = 0) int index) {
if (Build.VERSION.SDK_INT >= 29) {
return mInfo.getRegionAt(index);
}
@@ -1620,8 +1592,7 @@
* @param region The region retrieved from {@link #getRegionAt(int)}.
* @return The target node associates with the given region.
*/
- @Nullable
- public AccessibilityNodeInfoCompat getTargetForRegion(@NonNull Region region) {
+ public @Nullable AccessibilityNodeInfoCompat getTargetForRegion(@NonNull Region region) {
if (Build.VERSION.SDK_INT >= 29) {
AccessibilityNodeInfo info = mInfo.getTargetForRegion(region);
if (info != null) {
@@ -2533,8 +2504,7 @@
*
* @see AccessibilityNodeInfoCompat#getParent(int) for a description of prefetching.
*/
- @Nullable
- public AccessibilityNodeInfoCompat getChild(int index, int prefetchingStrategy) {
+ public @Nullable AccessibilityNodeInfoCompat getChild(int index, int prefetchingStrategy) {
if (Build.VERSION.SDK_INT >= 33) {
return Api33Impl.getChild(mInfo, index, prefetchingStrategy);
}
@@ -2793,8 +2763,7 @@
* @see #FLAG_PREFETCH_SIBLINGS
* @see #FLAG_PREFETCH_UNINTERRUPTIBLE
*/
- @Nullable
- public AccessibilityNodeInfoCompat getParent(int prefetchingStrategy) {
+ public @Nullable AccessibilityNodeInfoCompat getParent(int prefetchingStrategy) {
if (Build.VERSION.SDK_INT >= 33) {
return Api33Impl.getParent(mInfo, prefetchingStrategy);
}
@@ -2923,7 +2892,7 @@
* </ul>
* @param outBounds The output node bounds.
*/
- public void getBoundsInWindow(@NonNull Rect outBounds) {
+ public void getBoundsInWindow(@NonNull Rect outBounds) {
if (Build.VERSION.SDK_INT >= 34) {
Api34Impl.getBoundsInWindow(mInfo, outBounds);
} else {
@@ -3766,8 +3735,7 @@
* </ul>
* @see #setContainerTitle for details.
*/
- @Nullable
- public CharSequence getContainerTitle() {
+ public @Nullable CharSequence getContainerTitle() {
if (Build.VERSION.SDK_INT >= 34) {
return Api34Impl.getContainerTitle(mInfo);
} else {
@@ -3967,8 +3935,7 @@
* @return The {@link android.view.accessibility.AccessibilityNodeInfo.ExtraRenderingInfo
* extra rendering info}.
*/
- @Nullable
- public AccessibilityNodeInfo.ExtraRenderingInfo getExtraRenderingInfo() {
+ public AccessibilityNodeInfo.@Nullable ExtraRenderingInfo getExtraRenderingInfo() {
if (Build.VERSION.SDK_INT >= 33) {
return Api33Impl.getExtraRenderingInfo(mInfo);
} else {
@@ -4293,8 +4260,7 @@
* @see #EXTRA_DATA_RENDERING_INFO_KEY
* @see #EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY
*/
- @NonNull
- public List<String> getAvailableExtraData() {
+ public @NonNull List<String> getAvailableExtraData() {
if (Build.VERSION.SDK_INT >= 26) {
return mInfo.getAvailableExtraData();
} else {
@@ -4603,8 +4569,7 @@
*
* @return The tooltip text.
*/
- @Nullable
- public CharSequence getTooltipText() {
+ public @Nullable CharSequence getTooltipText() {
if (Build.VERSION.SDK_INT >= 28) {
return mInfo.getTooltipText();
} else {
@@ -4912,8 +4877,7 @@
* @return {@link TouchDelegateInfoCompat} or {@code null} if there are no touch delegates
* in this node.
*/
- @Nullable
- public TouchDelegateInfoCompat getTouchDelegateInfo() {
+ public @Nullable TouchDelegateInfoCompat getTouchDelegateInfo() {
if (Build.VERSION.SDK_INT >= 29) {
TouchDelegateInfo delegateInfo = mInfo.getTouchDelegateInfo();
if (delegateInfo != null) {
@@ -5033,9 +4997,8 @@
}
@SuppressWarnings("deprecation")
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
StringBuilder builder = new StringBuilder();
builder.append(super.toString());
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeProviderCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeProviderCompat.java
index 6d44092..7d52a97 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeProviderCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeProviderCompat.java
@@ -21,10 +21,11 @@
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.List;
@@ -109,8 +110,7 @@
*/
public static final int HOST_VIEW_ID = -1;
- @Nullable
- private final Object mProvider;
+ private final @Nullable Object mProvider;
/**
* Creates a new instance.
@@ -136,8 +136,7 @@
/**
* @return The wrapped {@link android.view.accessibility.AccessibilityNodeProvider}.
*/
- @Nullable
- public Object getProvider() {
+ public @Nullable Object getProvider() {
return mProvider;
}
@@ -163,8 +162,7 @@
*
* @see AccessibilityNodeInfoCompat
*/
- @Nullable
- public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) {
+ public @Nullable AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) {
return null;
}
@@ -201,9 +199,8 @@
* @see AccessibilityNodeInfoCompat
*/
@SuppressWarnings("unused")
- @Nullable
- public List<AccessibilityNodeInfoCompat> findAccessibilityNodeInfosByText(@NonNull String text,
- int virtualViewId) {
+ public @Nullable List<AccessibilityNodeInfoCompat> findAccessibilityNodeInfosByText(
+ @NonNull String text, int virtualViewId) {
return null;
}
@@ -219,8 +216,7 @@
* @see AccessibilityNodeInfoCompat#FOCUS_ACCESSIBILITY
*/
@SuppressWarnings("unused")
- @Nullable
- public AccessibilityNodeInfoCompat findFocus(int focus) {
+ public @Nullable AccessibilityNodeInfoCompat findFocus(int focus) {
return null;
}
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityRecordCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityRecordCompat.java
index cf9d33f..8c01131 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityRecordCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityRecordCompat.java
@@ -23,8 +23,8 @@
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityRecord;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import java.util.List;
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityViewCommand.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityViewCommand.java
index b020b83..9f24878 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityViewCommand.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityViewCommand.java
@@ -21,10 +21,11 @@
import android.os.Bundle;
import android.view.View;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Functional interface used to create a custom accessibility action.
*/
@@ -38,7 +39,7 @@
* @param arguments Optional action arguments
*/
boolean perform(@NonNull View view,
- @Nullable AccessibilityViewCommand.CommandArguments arguments);
+ AccessibilityViewCommand.@Nullable CommandArguments arguments);
/**
* Object containing arguments passed into an {@link AccessibilityViewCommand}
@@ -99,8 +100,7 @@
/**
* @return HTML element type, for example BUTTON, INPUT, TABLE, etc.
*/
- @Nullable
- public String getHTMLElement() {
+ public @Nullable String getHTMLElement() {
return mBundle.getString(
AccessibilityNodeInfoCompat.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
}
@@ -135,8 +135,7 @@
/**
* @return The text content to set.
*/
- @Nullable
- public CharSequence getText() {
+ public @Nullable CharSequence getText() {
return mBundle.getCharSequence(
AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE);
}
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompat.java
index ecb83c1..8e3103f 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompat.java
@@ -27,11 +27,12 @@
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.os.LocaleListCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Helper for accessing {@link android.view.accessibility.AccessibilityWindowInfo}.
*/
@@ -155,8 +156,7 @@
*
* @return The root node.
*/
- @Nullable
- public AccessibilityNodeInfoCompat getRoot() {
+ public @Nullable AccessibilityNodeInfoCompat getRoot() {
if (SDK_INT >= 21) {
return AccessibilityNodeInfoCompat.wrapNonNullInstance(
Api21Impl.getRoot((AccessibilityWindowInfo) mInfo));
@@ -173,8 +173,7 @@
*
* @see AccessibilityNodeInfoCompat#getParent(int) for a description of prefetching.
*/
- @Nullable
- public AccessibilityNodeInfoCompat getRoot(int prefetchingStrategy) {
+ public @Nullable AccessibilityNodeInfoCompat getRoot(int prefetchingStrategy) {
if (Build.VERSION.SDK_INT >= 33) {
return Api33Impl.getRoot(mInfo, prefetchingStrategy);
}
@@ -203,8 +202,7 @@
*
* @return The parent window.
*/
- @Nullable
- public AccessibilityWindowInfoCompat getParent() {
+ public @Nullable AccessibilityWindowInfoCompat getParent() {
if (SDK_INT >= 21) {
return wrapNonNullInstance(Api21Impl.getParent((AccessibilityWindowInfo) mInfo));
} else {
@@ -322,8 +320,7 @@
* @param index The index.
* @return The child.
*/
- @Nullable
- public AccessibilityWindowInfoCompat getChild(int index) {
+ public @Nullable AccessibilityWindowInfoCompat getChild(int index) {
if (SDK_INT >= 21) {
return wrapNonNullInstance(Api21Impl.getChild((AccessibilityWindowInfo) mInfo, index));
} else {
@@ -390,8 +387,7 @@
* @return The title of the window, or the application label for the window if no title was
* explicitly set, or {@code null} if neither is available.
*/
- @Nullable
- public CharSequence getTitle() {
+ public @Nullable CharSequence getTitle() {
if (SDK_INT >= 24) {
return Api24Impl.getTitle((AccessibilityWindowInfo) mInfo);
} else {
@@ -404,8 +400,7 @@
*
* @return The anchor node, or {@code null} if none exists.
*/
- @Nullable
- public AccessibilityNodeInfoCompat getAnchor() {
+ public @Nullable AccessibilityNodeInfoCompat getAnchor() {
if (SDK_INT >= 24) {
return AccessibilityNodeInfoCompat.wrapNonNullInstance(
Api24Impl.getAnchor((AccessibilityWindowInfo) mInfo));
@@ -420,8 +415,7 @@
*
* @return An instance.
*/
- @Nullable
- public static AccessibilityWindowInfoCompat obtain() {
+ public static @Nullable AccessibilityWindowInfoCompat obtain() {
if (SDK_INT >= 21) {
return wrapNonNullInstance(Api21Impl.obtain());
} else {
@@ -437,8 +431,7 @@
* @param info The other info.
* @return An instance.
*/
- @Nullable
- public static AccessibilityWindowInfoCompat obtain(
+ public static @Nullable AccessibilityWindowInfoCompat obtain(
@Nullable AccessibilityWindowInfoCompat info) {
if (SDK_INT >= 21) {
return info == null
@@ -464,8 +457,7 @@
/**
* @return The unwrapped {@link android.view.accessibility.AccessibilityWindowInfo}.
*/
- @Nullable
- public AccessibilityWindowInfo unwrap() {
+ public @Nullable AccessibilityWindowInfo unwrap() {
if (SDK_INT >= 21) {
return (AccessibilityWindowInfo) mInfo;
} else {
@@ -496,9 +488,8 @@
return mInfo.equals(other.mInfo);
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
StringBuilder builder = new StringBuilder();
Rect bounds = new Rect();
getBoundsInScreen(bounds);
diff --git a/core/core/src/main/java/androidx/core/view/animation/PathInterpolatorCompat.java b/core/core/src/main/java/androidx/core/view/animation/PathInterpolatorCompat.java
index fe65e9f..d426319 100644
--- a/core/core/src/main/java/androidx/core/view/animation/PathInterpolatorCompat.java
+++ b/core/core/src/main/java/androidx/core/view/animation/PathInterpolatorCompat.java
@@ -21,9 +21,10 @@
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
/**
* Helper for creating path-based {@link Interpolator} instances. On API 21 or newer, the
* platform implementation will be used and on older platforms a compatible alternative
@@ -47,8 +48,7 @@
* @param path the {@link Path} to use to make the line representing the {@link Interpolator}
* @return the {@link Interpolator} representing the {@link Path}
*/
- @NonNull
- public static Interpolator create(@NonNull Path path) {
+ public static @NonNull Interpolator create(@NonNull Path path) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.createPathInterpolator(path);
}
@@ -63,8 +63,7 @@
* @param controlY the y coordinate of the quadratic Bezier control point
* @return the {@link Interpolator} representing the quadratic Bezier curve
*/
- @NonNull
- public static Interpolator create(float controlX, float controlY) {
+ public static @NonNull Interpolator create(float controlX, float controlY) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.createPathInterpolator(controlX, controlY);
}
@@ -81,8 +80,7 @@
* @param controlY2 the y coordinate of the second control point of the cubic Bezier
* @return the {@link Interpolator} representing the cubic Bezier curve
*/
- @NonNull
- public static Interpolator create(float controlX1, float controlY1,
+ public static @NonNull Interpolator create(float controlX1, float controlY1,
float controlX2, float controlY2) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.createPathInterpolator(controlX1, controlY1, controlX2, controlY2);
diff --git a/core/core/src/main/java/androidx/core/view/autofill/AutofillIdCompat.java b/core/core/src/main/java/androidx/core/view/autofill/AutofillIdCompat.java
index ccf9824..6ef7072 100644
--- a/core/core/src/main/java/androidx/core/view/autofill/AutofillIdCompat.java
+++ b/core/core/src/main/java/androidx/core/view/autofill/AutofillIdCompat.java
@@ -18,9 +18,10 @@
import android.view.autofill.AutofillId;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
/**
* Helper for accessing features in {@link AutofillId}.
*/
@@ -43,8 +44,7 @@
* @return wrapped class
*/
@RequiresApi(26)
- @NonNull
- public static AutofillIdCompat toAutofillIdCompat(@NonNull AutofillId autofillId) {
+ public static @NonNull AutofillIdCompat toAutofillIdCompat(@NonNull AutofillId autofillId) {
return new AutofillIdCompat(autofillId);
}
@@ -58,8 +58,7 @@
* @see AutofillIdCompat#toAutofillIdCompat(AutofillId)
*/
@RequiresApi(26)
- @NonNull
- public AutofillId toAutofillId() {
+ public @NonNull AutofillId toAutofillId() {
return (AutofillId) mWrappedObj;
}
}
diff --git a/core/core/src/main/java/androidx/core/view/contentcapture/ContentCaptureSessionCompat.java b/core/core/src/main/java/androidx/core/view/contentcapture/ContentCaptureSessionCompat.java
index 0af5435..f900db3 100644
--- a/core/core/src/main/java/androidx/core/view/contentcapture/ContentCaptureSessionCompat.java
+++ b/core/core/src/main/java/androidx/core/view/contentcapture/ContentCaptureSessionCompat.java
@@ -24,12 +24,13 @@
import android.view.autofill.AutofillId;
import android.view.contentcapture.ContentCaptureSession;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.view.ViewCompat;
import androidx.core.view.ViewStructureCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.List;
import java.util.Objects;
@@ -55,8 +56,7 @@
* @return wrapped class
*/
@RequiresApi(29)
- @NonNull
- public static ContentCaptureSessionCompat toContentCaptureSessionCompat(
+ public static @NonNull ContentCaptureSessionCompat toContentCaptureSessionCompat(
@NonNull ContentCaptureSession contentCaptureSession, @NonNull View host) {
return new ContentCaptureSessionCompat(contentCaptureSession, host);
}
@@ -71,8 +71,7 @@
* @see ContentCaptureSessionCompat#toContentCaptureSessionCompat(ContentCaptureSession, View)
*/
@RequiresApi(29)
- @NonNull
- public ContentCaptureSession toContentCaptureSession() {
+ public @NonNull ContentCaptureSession toContentCaptureSession() {
return (ContentCaptureSession) mWrappedObj;
}
@@ -103,8 +102,7 @@
*
* @return {@link AutofillId} for the virtual child
*/
- @Nullable
- public AutofillId newAutofillId(long virtualChildId) {
+ public @Nullable AutofillId newAutofillId(long virtualChildId) {
if (SDK_INT >= 29) {
return Api29Impl.newAutofillId(
(ContentCaptureSession) mWrappedObj,
@@ -130,8 +128,7 @@
*
* @return a new {@link ViewStructure} that can be used for Content Capture purposes.
*/
- @Nullable
- public ViewStructureCompat newVirtualViewStructure(
+ public @Nullable ViewStructureCompat newVirtualViewStructure(
@NonNull AutofillId parentId, long virtualId) {
if (SDK_INT >= 29) {
return ViewStructureCompat.toViewStructureCompat(
@@ -194,7 +191,7 @@
*
* @param virtualIds ids of the virtual children.
*/
- public void notifyViewsDisappeared(@NonNull long[] virtualIds) {
+ public void notifyViewsDisappeared(long @NonNull [] virtualIds) {
if (SDK_INT >= 34) {
Api29Impl.notifyViewsDisappeared(
(ContentCaptureSession) mWrappedObj,
diff --git a/core/core/src/main/java/androidx/core/view/inputmethod/EditorInfoCompat.java b/core/core/src/main/java/androidx/core/view/inputmethod/EditorInfoCompat.java
index 22a15e9..61938b4 100644
--- a/core/core/src/main/java/androidx/core/view/inputmethod/EditorInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/inputmethod/EditorInfoCompat.java
@@ -37,12 +37,13 @@
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
/**
@@ -160,7 +161,7 @@
* is not supported on this Editor
*/
public static void setContentMimeTypes(@NonNull EditorInfo editorInfo,
- @Nullable String[] contentMimeTypes) {
+ String @Nullable [] contentMimeTypes) {
if (Build.VERSION.SDK_INT >= 25) {
editorInfo.contentMimeTypes = contentMimeTypes;
} else {
@@ -182,8 +183,7 @@
* InputConnectionCompat#commitContent(InputConnection, EditorInfo, InputContentInfoCompat,
* int, Bundle)} is not supported on this editor
*/
- @NonNull
- public static String[] getContentMimeTypes(@NonNull EditorInfo editorInfo) {
+ public static String @NonNull [] getContentMimeTypes(@NonNull EditorInfo editorInfo) {
if (Build.VERSION.SDK_INT >= 25) {
final String[] result = editorInfo.contentMimeTypes;
return result != null ? result : EMPTY_STRING_ARRAY;
@@ -395,8 +395,7 @@
* than <var>n</var>. When there is no text before the cursor, an empty string will be returned.
* It could also be {@code null} when the editor or system could not support this protocol.
*/
- @Nullable
- public static CharSequence getInitialTextBeforeCursor(@NonNull EditorInfo editorInfo,
+ public static @Nullable CharSequence getInitialTextBeforeCursor(@NonNull EditorInfo editorInfo,
int length, int flags) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return Api30Impl.getInitialTextBeforeCursor(editorInfo, length, flags);
@@ -432,8 +431,8 @@
* is no text selected. When {@code null} is returned, the selected text might be too long or
* this protocol is not supported.
*/
- @Nullable
- public static CharSequence getInitialSelectedText(@NonNull EditorInfo editorInfo, int flags) {
+ public static @Nullable CharSequence getInitialSelectedText(@NonNull EditorInfo editorInfo,
+ int flags) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return Api30Impl.getInitialSelectedText(editorInfo, flags);
}
@@ -478,9 +477,8 @@
* than <var>n</var>. When there is no text after the cursor, an empty string will be returned.
* It could also be {@code null} when the editor or system could not support this protocol.
*/
- @Nullable
- public static CharSequence getInitialTextAfterCursor(@NonNull EditorInfo editorInfo, int length,
- int flags) {
+ public static @Nullable CharSequence getInitialTextAfterCursor(@NonNull EditorInfo editorInfo,
+ int length, int flags) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return Api30Impl.getInitialTextAfterCursor(editorInfo, length, flags);
}
diff --git a/core/core/src/main/java/androidx/core/view/inputmethod/InputConnectionCompat.java b/core/core/src/main/java/androidx/core/view/inputmethod/InputConnectionCompat.java
index 6bb2927..6023f32 100644
--- a/core/core/src/main/java/androidx/core/view/inputmethod/InputConnectionCompat.java
+++ b/core/core/src/main/java/androidx/core/view/inputmethod/InputConnectionCompat.java
@@ -37,8 +37,6 @@
import android.view.inputmethod.InputConnectionWrapper;
import android.view.inputmethod.InputContentInfo;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Preconditions;
@@ -46,6 +44,9 @@
import androidx.core.view.OnReceiveContentListener;
import androidx.core.view.ViewCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Helper for accessing features in {@link InputConnection} introduced after API level 13 in a
* backwards compatible fashion.
@@ -265,8 +266,7 @@
* {@link ViewCompat#setOnReceiveContentListener} instead.
*/
@Deprecated
- @NonNull
- public static InputConnection createWrapper(@NonNull InputConnection inputConnection,
+ public static @NonNull InputConnection createWrapper(@NonNull InputConnection inputConnection,
@NonNull EditorInfo editorInfo,
@NonNull OnCommitContentListener onCommitContentListener) {
ObjectsCompat.requireNonNull(inputConnection, "inputConnection must be non-null");
@@ -341,8 +341,7 @@
* @return A wrapper {@link InputConnection} object that can be returned to the IME.
*/
@SuppressWarnings("deprecation")
- @NonNull
- public static InputConnection createWrapper(@NonNull View view,
+ public static @NonNull InputConnection createWrapper(@NonNull View view,
@NonNull InputConnection inputConnection, @NonNull EditorInfo editorInfo) {
OnCommitContentListener onCommitContentListener =
createOnCommitContentListenerUsingPerformReceiveContent(view);
@@ -354,9 +353,9 @@
* {@link ViewCompat#performReceiveContent} to insert content. This is useful for widgets
* that support content insertion using an {@link OnReceiveContentListener}.
*/
- @NonNull
- private static OnCommitContentListener createOnCommitContentListenerUsingPerformReceiveContent(
- @NonNull View view) {
+ private static @NonNull OnCommitContentListener
+ createOnCommitContentListenerUsingPerformReceiveContent(
+ @NonNull View view) {
Preconditions.checkNotNull(view);
return (inputContentInfo, flags, opts) -> {
Bundle extras = opts;
diff --git a/core/core/src/main/java/androidx/core/view/inputmethod/InputContentInfoCompat.java b/core/core/src/main/java/androidx/core/view/inputmethod/InputContentInfoCompat.java
index ca56608..bddc38e 100644
--- a/core/core/src/main/java/androidx/core/view/inputmethod/InputContentInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/inputmethod/InputContentInfoCompat.java
@@ -21,10 +21,11 @@
import android.os.Build;
import android.view.inputmethod.InputContentInfo;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Helper for accessing features in InputContentInfo introduced after API level 13 in a backwards
* compatible fashion.
@@ -32,17 +33,13 @@
public final class InputContentInfoCompat {
private interface InputContentInfoCompatImpl {
- @NonNull
- Uri getContentUri();
+ @NonNull Uri getContentUri();
- @NonNull
- ClipDescription getDescription();
+ @NonNull ClipDescription getDescription();
- @Nullable
- Uri getLinkUri();
+ @Nullable Uri getLinkUri();
- @Nullable
- Object getInputContentInfo();
+ @Nullable Object getInputContentInfo();
void requestPermission();
@@ -51,12 +48,9 @@
private static final class InputContentInfoCompatBaseImpl
implements InputContentInfoCompatImpl {
- @NonNull
- private final Uri mContentUri;
- @NonNull
- private final ClipDescription mDescription;
- @Nullable
- private final Uri mLinkUri;
+ private final @NonNull Uri mContentUri;
+ private final @NonNull ClipDescription mDescription;
+ private final @Nullable Uri mLinkUri;
InputContentInfoCompatBaseImpl(@NonNull Uri contentUri,
@NonNull ClipDescription description, @Nullable Uri linkUri) {
@@ -65,27 +59,23 @@
mLinkUri = linkUri;
}
- @NonNull
@Override
- public Uri getContentUri() {
+ public @NonNull Uri getContentUri() {
return mContentUri;
}
- @NonNull
@Override
- public ClipDescription getDescription() {
+ public @NonNull ClipDescription getDescription() {
return mDescription;
}
- @Nullable
@Override
- public Uri getLinkUri() {
+ public @Nullable Uri getLinkUri() {
return mLinkUri;
}
- @Nullable
@Override
- public Object getInputContentInfo() {
+ public @Nullable Object getInputContentInfo() {
return null;
}
@@ -101,8 +91,7 @@
@RequiresApi(25)
private static final class InputContentInfoCompatApi25Impl
implements InputContentInfoCompatImpl {
- @NonNull
- final InputContentInfo mObject;
+ final @NonNull InputContentInfo mObject;
InputContentInfoCompatApi25Impl(@NonNull Object inputContentInfo) {
mObject = (InputContentInfo) inputContentInfo;
@@ -114,26 +103,22 @@
}
@Override
- @NonNull
- public Uri getContentUri() {
+ public @NonNull Uri getContentUri() {
return mObject.getContentUri();
}
@Override
- @NonNull
- public ClipDescription getDescription() {
+ public @NonNull ClipDescription getDescription() {
return mObject.getDescription();
}
@Override
- @Nullable
- public Uri getLinkUri() {
+ public @Nullable Uri getLinkUri() {
return mObject.getLinkUri();
}
@Override
- @NonNull
- public Object getInputContentInfo() {
+ public @NonNull Object getInputContentInfo() {
return mObject;
}
@@ -179,8 +164,7 @@
/**
* @return content URI with which the content can be obtained.
*/
- @NonNull
- public Uri getContentUri() {
+ public @NonNull Uri getContentUri() {
return mImpl.getContentUri();
}
@@ -189,16 +173,14 @@
* such as MIME type(s). {@link ClipDescription#getLabel()} can be used for accessibility
* purpose.
*/
- @NonNull
- public ClipDescription getDescription() {
+ public @NonNull ClipDescription getDescription() {
return mImpl.getDescription();
}
/**
* @return an optional {@code http} or {@code https} URI that is related to this content.
*/
- @Nullable
- public Uri getLinkUri() {
+ public @Nullable Uri getLinkUri() {
return mImpl.getLinkUri();
}
@@ -212,8 +194,7 @@
* @return an equivalent {@link InputContentInfoCompat} object, or {@code null} if not
* supported.
*/
- @Nullable
- public static InputContentInfoCompat wrap(@Nullable Object inputContentInfo) {
+ public static @Nullable InputContentInfoCompat wrap(@Nullable Object inputContentInfo) {
if (inputContentInfo == null) {
return null;
}
@@ -231,8 +212,7 @@
* @return an equivalent android.view.inputmethod.InputContentInfo object, or {@code null} if
* not supported.
*/
- @Nullable
- public Object unwrap() {
+ public @Nullable Object unwrap() {
return mImpl.getInputContentInfo();
}
diff --git a/core/core/src/main/java/androidx/core/view/insetscontrast/ContrastProtection.java b/core/core/src/main/java/androidx/core/view/insetscontrast/ContrastProtection.java
index 2855690..925e81b 100644
--- a/core/core/src/main/java/androidx/core/view/insetscontrast/ContrastProtection.java
+++ b/core/core/src/main/java/androidx/core/view/insetscontrast/ContrastProtection.java
@@ -22,12 +22,13 @@
import android.view.animation.PathInterpolator;
import androidx.annotation.FloatRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.graphics.Insets;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsCompat.Side.InsetsSide;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* An abstract class which describes a layer to be placed on the content of a window and underneath
* the system bar (which has a transparent background) to ensure the readability of the foreground
@@ -109,8 +110,7 @@
*
* @return the attributes this protection is associated with.
*/
- @NonNull
- Attributes getAttributes() {
+ @NonNull Attributes getAttributes() {
return mAttributes;
}
@@ -153,8 +153,7 @@
return updateLayout();
}
- @NonNull
- Insets updateLayout() {
+ @NonNull Insets updateLayout() {
Insets consumed = Insets.NONE;
final int inset;
switch (mSide) {
@@ -416,8 +415,7 @@
*
* @return the margin of the protection in pixels.
*/
- @NonNull
- Insets getMargin() {
+ @NonNull Insets getMargin() {
return mMargin;
}
@@ -435,8 +433,7 @@
*
* @return the {@link Drawable} that fills the protection.
*/
- @Nullable
- Drawable getDrawable() {
+ @Nullable Drawable getDrawable() {
return mDrawable;
}
diff --git a/core/core/src/main/java/androidx/core/view/insetscontrast/ProtectionGroup.java b/core/core/src/main/java/androidx/core/view/insetscontrast/ProtectionGroup.java
index 917bf3a..c494694 100644
--- a/core/core/src/main/java/androidx/core/view/insetscontrast/ProtectionGroup.java
+++ b/core/core/src/main/java/androidx/core/view/insetscontrast/ProtectionGroup.java
@@ -23,9 +23,10 @@
import android.graphics.RectF;
-import androidx.annotation.NonNull;
import androidx.core.graphics.Insets;
+import org.jspecify.annotations.NonNull;
+
import java.util.ArrayList;
import java.util.List;
@@ -170,8 +171,7 @@
* @param index the index of the protection to return.
* @return the protection at the specified position in this group.
*/
- @NonNull
- ContrastProtection getProtection(int index) {
+ @NonNull ContrastProtection getProtection(int index) {
return mProtections.get(index);
}
diff --git a/core/core/src/main/java/androidx/core/view/insetscontrast/ProtectionView.java b/core/core/src/main/java/androidx/core/view/insetscontrast/ProtectionView.java
index 10da02e..b7986b3 100644
--- a/core/core/src/main/java/androidx/core/view/insetscontrast/ProtectionView.java
+++ b/core/core/src/main/java/androidx/core/view/insetscontrast/ProtectionView.java
@@ -25,10 +25,11 @@
import android.view.ViewGroup;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
import androidx.core.graphics.Insets;
import androidx.core.view.WindowInsetsCompat;
+import org.jspecify.annotations.NonNull;
+
import java.util.List;
/**
diff --git a/core/core/src/main/java/androidx/core/view/insetscontrast/SystemBarStateMonitor.java b/core/core/src/main/java/androidx/core/view/insetscontrast/SystemBarStateMonitor.java
index 3bda8d0..81d6036 100644
--- a/core/core/src/main/java/androidx/core/view/insetscontrast/SystemBarStateMonitor.java
+++ b/core/core/src/main/java/androidx/core/view/insetscontrast/SystemBarStateMonitor.java
@@ -34,13 +34,14 @@
import android.view.Window;
import android.view.WindowInsets;
-import androidx.annotation.NonNull;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsAnimationCompat;
import androidx.core.view.WindowInsetsCompat;
+import org.jspecify.annotations.NonNull;
+
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -134,10 +135,9 @@
}
@Override
- @NonNull
- public WindowInsetsAnimationCompat.BoundsCompat onStart(
+ public WindowInsetsAnimationCompat.@NonNull BoundsCompat onStart(
@NonNull WindowInsetsAnimationCompat animation,
- @NonNull WindowInsetsAnimationCompat.BoundsCompat bounds) {
+ WindowInsetsAnimationCompat.@NonNull BoundsCompat bounds) {
if (!animatesSystemBars(animation)) {
return bounds;
}
@@ -161,8 +161,8 @@
}
@Override
- @NonNull
- public WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat windowInsets,
+ public @NonNull WindowInsetsCompat onProgress(
+ @NonNull WindowInsetsCompat windowInsets,
@NonNull List<WindowInsetsAnimationCompat> runningAnimations) {
final RectF alpha = new RectF(1f, 1f, 1f, 1f);
int animatingSides = 0;
diff --git a/core/core/src/main/java/androidx/core/widget/AutoScrollHelper.java b/core/core/src/main/java/androidx/core/widget/AutoScrollHelper.java
index fe2be2a..fee7ffa 100644
--- a/core/core/src/main/java/androidx/core/widget/AutoScrollHelper.java
+++ b/core/core/src/main/java/androidx/core/widget/AutoScrollHelper.java
@@ -26,9 +26,10 @@
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
-import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;
+import org.jspecify.annotations.NonNull;
+
/**
* AutoScrollHelper is a utility class for adding automatic edge-triggered
* scrolling to Views.
@@ -291,8 +292,7 @@
* {@link #NO_MAX} to leave the relative value unconstrained.
* @return The scroll helper, which may used to chain setter calls.
*/
- @NonNull
- public AutoScrollHelper setMaximumVelocity(float horizontalMax, float verticalMax) {
+ public @NonNull AutoScrollHelper setMaximumVelocity(float horizontalMax, float verticalMax) {
mMaximumVelocity[HORIZONTAL] = horizontalMax / 1000f;
mMaximumVelocity[VERTICAL] = verticalMax / 1000f;
return this;
@@ -310,8 +310,7 @@
* {@link #NO_MIN} to leave the relative value unconstrained.
* @return The scroll helper, which may used to chain setter calls.
*/
- @NonNull
- public AutoScrollHelper setMinimumVelocity(float horizontalMin, float verticalMin) {
+ public @NonNull AutoScrollHelper setMinimumVelocity(float horizontalMin, float verticalMin) {
mMinimumVelocity[HORIZONTAL] = horizontalMin / 1000f;
mMinimumVelocity[VERTICAL] = verticalMin / 1000f;
return this;
@@ -332,8 +331,7 @@
* ignore.
* @return The scroll helper, which may used to chain setter calls.
*/
- @NonNull
- public AutoScrollHelper setRelativeVelocity(float horizontal, float vertical) {
+ public @NonNull AutoScrollHelper setRelativeVelocity(float horizontal, float vertical) {
mRelativeVelocity[HORIZONTAL] = horizontal / 1000f;
mRelativeVelocity[VERTICAL] = vertical / 1000f;
return this;
@@ -354,8 +352,7 @@
* @param type The type of edge to use.
* @return The scroll helper, which may used to chain setter calls.
*/
- @NonNull
- public AutoScrollHelper setEdgeType(int type) {
+ public @NonNull AutoScrollHelper setEdgeType(int type) {
mEdgeType = type;
return this;
}
@@ -374,8 +371,7 @@
* maximum value.
* @return The scroll helper, which may used to chain setter calls.
*/
- @NonNull
- public AutoScrollHelper setRelativeEdges(float horizontal, float vertical) {
+ public @NonNull AutoScrollHelper setRelativeEdges(float horizontal, float vertical) {
mRelativeEdges[HORIZONTAL] = horizontal;
mRelativeEdges[VERTICAL] = vertical;
return this;
@@ -397,8 +393,7 @@
* value.
* @return The scroll helper, which may used to chain setter calls.
*/
- @NonNull
- public AutoScrollHelper setMaximumEdges(float horizontalMax, float verticalMax) {
+ public @NonNull AutoScrollHelper setMaximumEdges(float horizontalMax, float verticalMax) {
mMaximumEdges[HORIZONTAL] = horizontalMax;
mMaximumEdges[VERTICAL] = verticalMax;
return this;
@@ -415,8 +410,7 @@
* @param delayMillis The activation delay in milliseconds.
* @return The scroll helper, which may used to chain setter calls.
*/
- @NonNull
- public AutoScrollHelper setActivationDelay(int delayMillis) {
+ public @NonNull AutoScrollHelper setActivationDelay(int delayMillis) {
mActivationDelay = delayMillis;
return this;
}
@@ -431,8 +425,7 @@
* @param durationMillis The ramp-up duration in milliseconds.
* @return The scroll helper, which may used to chain setter calls.
*/
- @NonNull
- public AutoScrollHelper setRampUpDuration(int durationMillis) {
+ public @NonNull AutoScrollHelper setRampUpDuration(int durationMillis) {
mScroller.setRampUpDuration(durationMillis);
return this;
}
@@ -447,8 +440,7 @@
* @param durationMillis The ramp-down duration in milliseconds.
* @return The scroll helper, which may used to chain setter calls.
*/
- @NonNull
- public AutoScrollHelper setRampDownDuration(int durationMillis) {
+ public @NonNull AutoScrollHelper setRampDownDuration(int durationMillis) {
mScroller.setRampDownDuration(durationMillis);
return this;
}
diff --git a/core/core/src/main/java/androidx/core/widget/AutoSizeableTextView.java b/core/core/src/main/java/androidx/core/widget/AutoSizeableTextView.java
index 1af86ae..90d3d94 100644
--- a/core/core/src/main/java/androidx/core/widget/AutoSizeableTextView.java
+++ b/core/core/src/main/java/androidx/core/widget/AutoSizeableTextView.java
@@ -21,9 +21,10 @@
import android.os.Build;
import android.util.TypedValue;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
/**
* Interface which allows a {@link android.widget.TextView} to receive background auto-sizing calls
* from {@link TextViewCompat} when running on API v26 devices or lower.
@@ -100,7 +101,7 @@
* @see #getAutoSizeMaxTextSize()
* @see #getAutoSizeTextAvailableSizes()
*/
- void setAutoSizeTextTypeUniformWithPresetSizes(@NonNull int[] presetSizes, int unit)
+ void setAutoSizeTextTypeUniformWithPresetSizes(int @NonNull [] presetSizes, int unit)
throws IllegalArgumentException;
/**
diff --git a/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java b/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java
index fbdd524..cb8b97e 100644
--- a/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java
@@ -23,11 +23,12 @@
import android.graphics.drawable.Drawable;
import android.widget.CheckedTextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.graphics.drawable.DrawableCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Helper for accessing {@link CheckedTextView}.
*/
@@ -62,8 +63,7 @@
*
* @see #setCheckMarkTintList(CheckedTextView, ColorStateList)
*/
- @Nullable
- public static ColorStateList getCheckMarkTintList(@NonNull CheckedTextView textView) {
+ public static @Nullable ColorStateList getCheckMarkTintList(@NonNull CheckedTextView textView) {
if (SDK_INT >= 21) {
return Api21Impl.getCheckMarkTintList(textView);
}
@@ -85,7 +85,7 @@
* @see DrawableCompat#setTintMode(Drawable, PorterDuff.Mode)
*/
public static void setCheckMarkTintMode(@NonNull CheckedTextView textView,
- @Nullable PorterDuff.Mode tintMode) {
+ PorterDuff.@Nullable Mode tintMode) {
if (SDK_INT >= 21) {
Api21Impl.setCheckMarkTintMode(textView, tintMode);
} else if (textView instanceof TintableCheckedTextView) {
@@ -98,8 +98,8 @@
* @attr name android:checkMarkTintMode
* @see #setCheckMarkTintMode(CheckedTextView, PorterDuff.Mode)
*/
- @Nullable
- public static PorterDuff.Mode getCheckMarkTintMode(@NonNull CheckedTextView textView) {
+ public static PorterDuff.@Nullable Mode getCheckMarkTintMode(
+ @NonNull CheckedTextView textView) {
if (SDK_INT >= 21) {
return Api21Impl.getCheckMarkTintMode(textView);
}
@@ -117,8 +117,7 @@
*/
@Deprecated
@androidx.annotation.ReplaceWith(expression = "textView.getCheckMarkDrawable()")
- @Nullable
- public static Drawable getCheckMarkDrawable(@NonNull CheckedTextView textView) {
+ public static @Nullable Drawable getCheckMarkDrawable(@NonNull CheckedTextView textView) {
return textView.getCheckMarkDrawable();
}
@@ -133,18 +132,16 @@
textView.setCheckMarkTintList(tint);
}
- @Nullable
- static ColorStateList getCheckMarkTintList(@NonNull CheckedTextView textView) {
+ static @Nullable ColorStateList getCheckMarkTintList(@NonNull CheckedTextView textView) {
return textView.getCheckMarkTintList();
}
static void setCheckMarkTintMode(@NonNull CheckedTextView textView,
- @Nullable PorterDuff.Mode tintMode) {
+ PorterDuff.@Nullable Mode tintMode) {
textView.setCheckMarkTintMode(tintMode);
}
- @Nullable
- static PorterDuff.Mode getCheckMarkTintMode(@NonNull CheckedTextView textView) {
+ static PorterDuff.@Nullable Mode getCheckMarkTintMode(@NonNull CheckedTextView textView) {
return textView.getCheckMarkTintMode();
}
}
diff --git a/core/core/src/main/java/androidx/core/widget/CompoundButtonCompat.java b/core/core/src/main/java/androidx/core/widget/CompoundButtonCompat.java
index 03f2904..6ac6123 100644
--- a/core/core/src/main/java/androidx/core/widget/CompoundButtonCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/CompoundButtonCompat.java
@@ -23,11 +23,12 @@
import android.util.Log;
import android.widget.CompoundButton;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.graphics.drawable.DrawableCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.reflect.Field;
/**
@@ -68,8 +69,7 @@
*
* @see #setButtonTintList(CompoundButton, ColorStateList)
*/
- @Nullable
- public static ColorStateList getButtonTintList(@NonNull CompoundButton button) {
+ public static @Nullable ColorStateList getButtonTintList(@NonNull CompoundButton button) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.getButtonTintList(button);
}
@@ -92,7 +92,7 @@
* @see DrawableCompat#setTintMode(Drawable, PorterDuff.Mode)
*/
public static void setButtonTintMode(@NonNull CompoundButton button,
- @Nullable PorterDuff.Mode tintMode) {
+ PorterDuff.@Nullable Mode tintMode) {
if (Build.VERSION.SDK_INT >= 21) {
Api21Impl.setButtonTintMode(button, tintMode);
} else if (button instanceof TintableCompoundButton) {
@@ -105,8 +105,7 @@
* @attr name android:buttonTintMode
* @see #setButtonTintMode(CompoundButton, PorterDuff.Mode)
*/
- @Nullable
- public static PorterDuff.Mode getButtonTintMode(@NonNull CompoundButton button) {
+ public static PorterDuff.@Nullable Mode getButtonTintMode(@NonNull CompoundButton button) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.getButtonTintMode(button);
}
@@ -121,8 +120,7 @@
*
* @see CompoundButton#setButtonDrawable(Drawable)
*/
- @Nullable
- public static Drawable getButtonDrawable(@NonNull CompoundButton button) {
+ public static @Nullable Drawable getButtonDrawable(@NonNull CompoundButton button) {
if (Build.VERSION.SDK_INT >= 23) {
return Api23Impl.getButtonDrawable(button);
}
diff --git a/core/core/src/main/java/androidx/core/widget/ContentLoadingProgressBar.java b/core/core/src/main/java/androidx/core/widget/ContentLoadingProgressBar.java
index 2eb84ef..8cd2b58 100644
--- a/core/core/src/main/java/androidx/core/widget/ContentLoadingProgressBar.java
+++ b/core/core/src/main/java/androidx/core/widget/ContentLoadingProgressBar.java
@@ -21,10 +21,11 @@
import android.view.View;
import android.widget.ProgressBar;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* ContentLoadingProgressBar implements a ProgressBar that waits a minimum time to be
* dismissed before showing. Once visible, the progress bar will be visible for
diff --git a/core/core/src/main/java/androidx/core/widget/EdgeEffectCompat.java b/core/core/src/main/java/androidx/core/widget/EdgeEffectCompat.java
index 9c4fe05..4392c5d 100644
--- a/core/core/src/main/java/androidx/core/widget/EdgeEffectCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/EdgeEffectCompat.java
@@ -25,10 +25,11 @@
import android.widget.OverScroller;
import android.widget.Scroller;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Helper for accessing {@link EdgeEffect}.
*
@@ -63,8 +64,8 @@
* @param context Context to use for theming the effect
* @param attrs The attributes of the XML tag that is inflating the view
*/
- @NonNull
- public static EdgeEffect create(@NonNull Context context, @Nullable AttributeSet attrs) {
+ public static @NonNull EdgeEffect create(@NonNull Context context,
+ @Nullable AttributeSet attrs) {
if (SDK_INT >= 31) {
return Api31Impl.create(context, attrs);
}
diff --git a/core/core/src/main/java/androidx/core/widget/ImageViewCompat.java b/core/core/src/main/java/androidx/core/widget/ImageViewCompat.java
index bf73151..9810047 100644
--- a/core/core/src/main/java/androidx/core/widget/ImageViewCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/ImageViewCompat.java
@@ -22,10 +22,11 @@
import android.os.Build;
import android.widget.ImageView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Helper for accessing features in {@link ImageView}.
*/
@@ -33,8 +34,7 @@
/**
* Return the tint applied to the image drawable, if specified.
*/
- @Nullable
- public static ColorStateList getImageTintList(@NonNull ImageView view) {
+ public static @Nullable ColorStateList getImageTintList(@NonNull ImageView view) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.getImageTintList(view);
}
@@ -70,8 +70,7 @@
/**
* Return the blending mode used to apply the tint to the image drawable, if specified.
*/
- @Nullable
- public static PorterDuff.Mode getImageTintMode(@NonNull ImageView view) {
+ public static PorterDuff.@Nullable Mode getImageTintMode(@NonNull ImageView view) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.getImageTintMode(view);
}
@@ -85,7 +84,7 @@
* {@link #setImageTintList(ImageView, ColorStateList)}
* to the image drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
*/
- public static void setImageTintMode(@NonNull ImageView view, @Nullable PorterDuff.Mode mode) {
+ public static void setImageTintMode(@NonNull ImageView view, PorterDuff.@Nullable Mode mode) {
if (Build.VERSION.SDK_INT >= 21) {
Api21Impl.setImageTintMode(view, mode);
diff --git a/core/core/src/main/java/androidx/core/widget/ListPopupWindowCompat.java b/core/core/src/main/java/androidx/core/widget/ListPopupWindowCompat.java
index 5818729..97835a0 100644
--- a/core/core/src/main/java/androidx/core/widget/ListPopupWindowCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/ListPopupWindowCompat.java
@@ -21,8 +21,8 @@
import android.view.View.OnTouchListener;
import android.widget.ListPopupWindow;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* Helper for accessing features in {@link ListPopupWindow}.
@@ -94,8 +94,7 @@
*/
@Deprecated
@androidx.annotation.ReplaceWith(expression = "listPopupWindow.createDragToOpenListener(src)")
- @Nullable
- public static OnTouchListener createDragToOpenListener(
+ public static @Nullable OnTouchListener createDragToOpenListener(
@NonNull ListPopupWindow listPopupWindow, @NonNull View src) {
return listPopupWindow.createDragToOpenListener(src);
}
diff --git a/core/core/src/main/java/androidx/core/widget/ListViewAutoScrollHelper.java b/core/core/src/main/java/androidx/core/widget/ListViewAutoScrollHelper.java
index c4bc6b6..f068f74 100644
--- a/core/core/src/main/java/androidx/core/widget/ListViewAutoScrollHelper.java
+++ b/core/core/src/main/java/androidx/core/widget/ListViewAutoScrollHelper.java
@@ -19,7 +19,7 @@
import android.view.View;
import android.widget.ListView;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* An implementation of {@link AutoScrollHelper} that knows how to scroll
diff --git a/core/core/src/main/java/androidx/core/widget/ListViewCompat.java b/core/core/src/main/java/androidx/core/widget/ListViewCompat.java
index 328fcb68a..cd5feb2 100644
--- a/core/core/src/main/java/androidx/core/widget/ListViewCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/ListViewCompat.java
@@ -18,7 +18,7 @@
import android.widget.ListView;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Helper for accessing features in {@link ListView}.
diff --git a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
index f7f56af..5e010d2 100644
--- a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
+++ b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
@@ -48,8 +48,6 @@
import android.widget.OverScroller;
import android.widget.ScrollView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
@@ -68,6 +66,9 @@
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityRecordCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.List;
/**
@@ -128,17 +129,14 @@
@RestrictTo(LIBRARY)
@VisibleForTesting
- @NonNull
- public EdgeEffect mEdgeGlowTop;
+ public @NonNull EdgeEffect mEdgeGlowTop;
@RestrictTo(LIBRARY)
@VisibleForTesting
- @NonNull
- public EdgeEffect mEdgeGlowBottom;
+ public @NonNull EdgeEffect mEdgeGlowBottom;
@VisibleForTesting
- @Nullable
- ScrollFeedbackProviderCompat mScrollFeedbackProvider;
+ @Nullable ScrollFeedbackProviderCompat mScrollFeedbackProvider;
/**
* Position of the last motion event; only used with touch related events (usually to assist
@@ -282,7 +280,7 @@
@Override
public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
- int dyUnconsumed, @Nullable int[] offsetInWindow, int type, @NonNull int[] consumed) {
+ int dyUnconsumed, int @Nullable [] offsetInWindow, int type, int @NonNull [] consumed) {
mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow, type, consumed);
}
@@ -306,7 +304,7 @@
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
- int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
+ int dyUnconsumed, int @Nullable [] offsetInWindow, int type) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow, type);
}
@@ -315,8 +313,8 @@
public boolean dispatchNestedPreScroll(
int dx,
int dy,
- @Nullable int[] consumed,
- @Nullable int[] offsetInWindow,
+ int @Nullable [] consumed,
+ int @Nullable [] offsetInWindow,
int type
) {
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
@@ -351,14 +349,14 @@
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
- int dyUnconsumed, @Nullable int[] offsetInWindow) {
+ int dyUnconsumed, int @Nullable [] offsetInWindow) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow);
}
@Override
- public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
- @Nullable int[] offsetInWindow) {
+ public boolean dispatchNestedPreScroll(int dx, int dy, int @Nullable [] consumed,
+ int @Nullable [] offsetInWindow) {
return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH);
}
@@ -376,11 +374,11 @@
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed, int type, @NonNull int[] consumed) {
+ int dxUnconsumed, int dyUnconsumed, int type, int @NonNull [] consumed) {
onNestedScrollInternal(dyUnconsumed, type, consumed);
}
- private void onNestedScrollInternal(int dyUnconsumed, int type, @Nullable int[] consumed) {
+ private void onNestedScrollInternal(int dyUnconsumed, int type, int @Nullable [] consumed) {
final int oldScrollY = getScrollY();
scrollBy(0, dyUnconsumed);
final int myConsumed = getScrollY() - oldScrollY;
@@ -421,7 +419,7 @@
}
@Override
- public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
+ public void onNestedPreScroll(@NonNull View target, int dx, int dy, int @NonNull [] consumed,
int type) {
dispatchNestedPreScroll(dx, dy, consumed, null, type);
}
@@ -452,7 +450,7 @@
}
@Override
- public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
+ public void onNestedPreScroll(@NonNull View target, int dx, int dy, int @NonNull [] consumed) {
onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH);
}
@@ -2502,9 +2500,8 @@
requestLayout();
}
- @NonNull
@Override
- protected Parcelable onSaveInstanceState() {
+ protected @NonNull Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.scrollPosition = getScrollY();
@@ -2529,9 +2526,8 @@
dest.writeInt(scrollPosition);
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
return "HorizontalScrollView.SavedState{"
+ Integer.toHexString(System.identityHashCode(this))
+ " scrollPosition=" + scrollPosition + "}";
diff --git a/core/core/src/main/java/androidx/core/widget/PopupMenuCompat.java b/core/core/src/main/java/androidx/core/widget/PopupMenuCompat.java
index 01a7071..ed6deb9 100644
--- a/core/core/src/main/java/androidx/core/widget/PopupMenuCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/PopupMenuCompat.java
@@ -20,8 +20,8 @@
import android.view.View.OnTouchListener;
import android.widget.PopupMenu;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* Helper for accessing features in {@link PopupMenu}.
@@ -50,8 +50,7 @@
* @return a touch listener that controls drag-to-open behavior, or {@code null} on
* unsupported APIs
*/
- @Nullable
- public static OnTouchListener getDragToOpenListener(@NonNull Object popupMenu) {
+ public static @Nullable OnTouchListener getDragToOpenListener(@NonNull Object popupMenu) {
return ((PopupMenu) popupMenu).getDragToOpenListener();
}
}
diff --git a/core/core/src/main/java/androidx/core/widget/PopupWindowCompat.java b/core/core/src/main/java/androidx/core/widget/PopupWindowCompat.java
index f23c596..580753c 100644
--- a/core/core/src/main/java/androidx/core/widget/PopupWindowCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/PopupWindowCompat.java
@@ -21,9 +21,10 @@
import android.view.View;
import android.widget.PopupWindow;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
import java.lang.reflect.Field;
import java.lang.reflect.Method;
diff --git a/core/core/src/main/java/androidx/core/widget/TextViewCompat.java b/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
index 1bdbdd9..ea26466 100644
--- a/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
@@ -55,8 +55,6 @@
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
@@ -64,6 +62,9 @@
import androidx.core.text.PrecomputedTextCompat;
import androidx.core.util.Preconditions;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.InvocationTargetException;
@@ -72,7 +73,6 @@
import java.util.List;
import java.util.Locale;
-
/**
* Helper for accessing features in {@link TextView}.
*/
@@ -225,8 +225,7 @@
*/
@Deprecated
@androidx.annotation.ReplaceWith(expression = "textView.getCompoundDrawablesRelative()")
- @NonNull
- public static Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) {
+ public static Drawable @NonNull [] getCompoundDrawablesRelative(@NonNull TextView textView) {
return textView.getCompoundDrawablesRelative();
}
@@ -306,7 +305,7 @@
*/
@SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static void setAutoSizeTextTypeUniformWithPresetSizes(@NonNull TextView textView,
- @NonNull int[] presetSizes, int unit) throws IllegalArgumentException {
+ int @NonNull [] presetSizes, int unit) throws IllegalArgumentException {
if (Build.VERSION.SDK_INT >= 27) {
Api26Impl.setAutoSizeTextTypeUniformWithPresetSizes(textView, presetSizes, unit);
} else if (textView instanceof AutoSizeableTextView) {
@@ -390,9 +389,8 @@
*
* @attr name android:autoSizePresetSizes
*/
- @NonNull
@SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
- public static int[] getAutoSizeTextAvailableSizes(@NonNull TextView textView) {
+ public static int @NonNull [] getAutoSizeTextAvailableSizes(@NonNull TextView textView) {
if (Build.VERSION.SDK_INT >= 27) {
return Api26Impl.getAutoSizeTextAvailableSizes(textView);
}
@@ -427,8 +425,8 @@
*/
@Deprecated
@androidx.annotation.ReplaceWith(expression = "textView.setCustomSelectionActionModeCallback(callback)")
- public static void setCustomSelectionActionModeCallback(@NonNull final TextView textView,
- @NonNull final ActionMode.Callback callback) {
+ public static void setCustomSelectionActionModeCallback(final @NonNull TextView textView,
+ final ActionMode.@NonNull Callback callback) {
textView.setCustomSelectionActionModeCallback(
wrapCustomSelectionActionModeCallback(textView, callback));
}
@@ -437,10 +435,9 @@
* @see #setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @Nullable
- public static ActionMode.Callback wrapCustomSelectionActionModeCallback(
- @NonNull final TextView textView,
- @Nullable final ActionMode.Callback callback) {
+ public static ActionMode.@Nullable Callback wrapCustomSelectionActionModeCallback(
+ final @NonNull TextView textView,
+ final ActionMode.@Nullable Callback callback) {
if (Build.VERSION.SDK_INT < 26 || Build.VERSION.SDK_INT > 27
|| callback instanceof OreoCallback || callback == null) {
// If the bug does not affect the current SDK version, or if
@@ -459,9 +456,8 @@
* @see #setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @Nullable
- public static ActionMode.Callback unwrapCustomSelectionActionModeCallback(
- @Nullable ActionMode.Callback callback) {
+ public static ActionMode.@Nullable Callback unwrapCustomSelectionActionModeCallback(
+ ActionMode.@Nullable Callback callback) {
if (callback instanceof OreoCallback && Build.VERSION.SDK_INT >= 26) {
return ((OreoCallback) callback).getWrappedCallback();
}
@@ -512,8 +508,7 @@
mCallback.onDestroyActionMode(mode);
}
- @NonNull
- ActionMode.Callback getWrappedCallback() {
+ ActionMode.@NonNull Callback getWrappedCallback() {
return mCallback;
}
@@ -634,7 +629,7 @@
* @attr name android:firstBaselineToTopHeight
*/
public static void setFirstBaselineToTopHeight(
- @NonNull final TextView textView,
+ final @NonNull TextView textView,
@Px @IntRange(from = 0) final int firstBaselineToTopHeight) {
Preconditions.checkArgumentNonnegative(firstBaselineToTopHeight);
if (Build.VERSION.SDK_INT >= 28) {
@@ -677,7 +672,7 @@
* @attr name android:lastBaselineToBottomHeight
*/
public static void setLastBaselineToBottomHeight(
- @NonNull final TextView textView,
+ final @NonNull TextView textView,
@Px @IntRange(from = 0) int lastBaselineToBottomHeight) {
Preconditions.checkArgumentNonnegative(lastBaselineToBottomHeight);
@@ -705,7 +700,7 @@
* @see #setFirstBaselineToTopHeight(TextView, int)
* @attr name android:firstBaselineToTopHeight
*/
- public static int getFirstBaselineToTopHeight(@NonNull final TextView textView) {
+ public static int getFirstBaselineToTopHeight(final @NonNull TextView textView) {
return textView.getPaddingTop() - textView.getPaint().getFontMetricsInt().top;
}
@@ -715,7 +710,7 @@
* @see #setLastBaselineToBottomHeight(TextView, int)
* @attr name android:lastBaselineToBottomHeight
*/
- public static int getLastBaselineToBottomHeight(@NonNull final TextView textView) {
+ public static int getLastBaselineToBottomHeight(final @NonNull TextView textView) {
return textView.getPaddingBottom() + textView.getPaint().getFontMetricsInt().bottom;
}
@@ -733,7 +728,7 @@
*
* @attr name android:lineHeight
*/
- public static void setLineHeight(@NonNull final TextView textView,
+ public static void setLineHeight(final @NonNull TextView textView,
@Px @IntRange(from = 0) int lineHeight) {
Preconditions.checkArgumentNonnegative(lineHeight);
@@ -784,8 +779,8 @@
* @return a current {@link PrecomputedTextCompat.Params}
* @see PrecomputedTextCompat
*/
- public static @NonNull PrecomputedTextCompat.Params getTextMetricsParams(
- @NonNull final TextView textView) {
+ public static PrecomputedTextCompat.@NonNull Params getTextMetricsParams(
+ final @NonNull TextView textView) {
if (Build.VERSION.SDK_INT >= 28) {
return new PrecomputedTextCompat.Params(Api28Impl.getTextMetricsParams(textView));
} else {
@@ -807,7 +802,7 @@
* @see PrecomputedTextCompat
*/
public static void setTextMetricsParams(@NonNull TextView textView,
- @NonNull PrecomputedTextCompat.Params params) {
+ PrecomputedTextCompat.@NonNull Params params) {
// There is no way of setting text direction heuristics to TextView.
// Convert to the View's text direction int values.
@@ -928,7 +923,7 @@
/**
* Convert TextDirectionHeuristic to TextDirection int values
*/
- private static int getTextDirection(@NonNull TextDirectionHeuristic heuristic) {
+ private static int getTextDirection(@NonNull TextDirectionHeuristic heuristic) {
if (heuristic == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
return TEXT_DIRECTION_FIRST_STRONG;
} else if (heuristic == TextDirectionHeuristics.FIRSTSTRONG_LTR) {
@@ -973,8 +968,7 @@
* Only returns meaningful info when running on API v24 or newer, or if {@code textView}
* implements the {@code TintableCompoundDrawablesView} interface.
*/
- @Nullable
- public static ColorStateList getCompoundDrawableTintList(@NonNull TextView textView) {
+ public static @Nullable ColorStateList getCompoundDrawableTintList(@NonNull TextView textView) {
Preconditions.checkNotNull(textView);
if (Build.VERSION.SDK_INT >= 24) {
return Api23Impl.getCompoundDrawableTintList(textView);
@@ -992,7 +986,7 @@
* {@code TintableCompoundDrawablesView} interface.
*/
public static void setCompoundDrawableTintMode(@NonNull TextView textView,
- @Nullable PorterDuff.Mode tintMode) {
+ PorterDuff.@Nullable Mode tintMode) {
Preconditions.checkNotNull(textView);
if (Build.VERSION.SDK_INT >= 24) {
Api23Impl.setCompoundDrawableTintMode(textView, tintMode);
@@ -1008,8 +1002,8 @@
* Only returns meaningful info when running on API v24 or newer, or if {@code textView}
* implements the {@code TintableCompoundDrawablesView} interface.
*/
- @Nullable
- public static PorterDuff.Mode getCompoundDrawableTintMode(@NonNull TextView textView) {
+ public static PorterDuff.@Nullable Mode getCompoundDrawableTintMode(
+ @NonNull TextView textView) {
Preconditions.checkNotNull(textView);
if (Build.VERSION.SDK_INT >= 24) {
return Api23Impl.getCompoundDrawableTintMode(textView);
diff --git a/core/core/src/main/java/androidx/core/widget/TextViewOnReceiveContentListener.java b/core/core/src/main/java/androidx/core/widget/TextViewOnReceiveContentListener.java
index 988b94e..2d82786 100644
--- a/core/core/src/main/java/androidx/core/widget/TextViewOnReceiveContentListener.java
+++ b/core/core/src/main/java/androidx/core/widget/TextViewOnReceiveContentListener.java
@@ -29,14 +29,15 @@
import android.view.View;
import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.view.ContentInfoCompat;
import androidx.core.view.ContentInfoCompat.Flags;
import androidx.core.view.ContentInfoCompat.Source;
import androidx.core.view.OnReceiveContentListener;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Default implementation inserting content into editable {@link TextView} components. This class
* handles insertion of text (plain text, styled text, HTML, etc) but not images or other content.
@@ -46,9 +47,8 @@
public final class TextViewOnReceiveContentListener implements OnReceiveContentListener {
private static final String LOG_TAG = "ReceiveContent";
- @Nullable
@Override
- public ContentInfoCompat onReceiveContent(@NonNull View view,
+ public @Nullable ContentInfoCompat onReceiveContent(@NonNull View view,
@NonNull ContentInfoCompat payload) {
if (Log.isLoggable(LOG_TAG, Log.DEBUG)) {
Log.d(LOG_TAG, "onReceive: " + payload);
@@ -86,7 +86,7 @@
return null;
}
- private static CharSequence coerceToText(@NonNull Context context, @NonNull ClipData.Item item,
+ private static CharSequence coerceToText(@NonNull Context context, ClipData.@NonNull Item item,
@Flags int flags) {
if ((flags & FLAG_CONVERT_TO_PLAIN_TEXT) != 0) {
CharSequence text = item.coerceToText(context);
diff --git a/core/core/src/main/java/androidx/core/widget/TintableCheckedTextView.java b/core/core/src/main/java/androidx/core/widget/TintableCheckedTextView.java
index 83c0c60..7d320fb 100644
--- a/core/core/src/main/java/androidx/core/widget/TintableCheckedTextView.java
+++ b/core/core/src/main/java/androidx/core/widget/TintableCheckedTextView.java
@@ -22,9 +22,10 @@
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.Nullable;
+
/**
* Interface which allows a {@link android.widget.CheckedTextView} to receive tinting
* calls from {@code CheckedTextViewCompat} when running on API v20 devices or lower.
@@ -56,8 +57,7 @@
*
* @see #setSupportCheckMarkTintList(ColorStateList)
*/
- @Nullable
- ColorStateList getSupportCheckMarkTintList();
+ @Nullable ColorStateList getSupportCheckMarkTintList();
/**
* Specifies the blending mode which should be used to apply the tint specified by
@@ -71,13 +71,12 @@
* @see androidx.core.graphics.drawable.DrawableCompat#setTintMode(Drawable,
* PorterDuff.Mode)
*/
- void setSupportCheckMarkTintMode(@Nullable PorterDuff.Mode tintMode);
+ void setSupportCheckMarkTintMode(PorterDuff.@Nullable Mode tintMode);
/**
* Returns the blending mode used to apply the tint to the check mark drawable
*
* @see #setSupportCheckMarkTintMode(PorterDuff.Mode)
*/
- @Nullable
- PorterDuff.Mode getSupportCheckMarkTintMode();
+ PorterDuff.@Nullable Mode getSupportCheckMarkTintMode();
}
diff --git a/core/core/src/main/java/androidx/core/widget/TintableCompoundButton.java b/core/core/src/main/java/androidx/core/widget/TintableCompoundButton.java
index 2de5353..805a07c 100644
--- a/core/core/src/main/java/androidx/core/widget/TintableCompoundButton.java
+++ b/core/core/src/main/java/androidx/core/widget/TintableCompoundButton.java
@@ -20,7 +20,7 @@
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Interface which allows a {@link android.widget.CompoundButton} to receive tinting
@@ -50,8 +50,7 @@
*
* @see #setSupportButtonTintList(ColorStateList)
*/
- @Nullable
- ColorStateList getSupportButtonTintList();
+ @Nullable ColorStateList getSupportButtonTintList();
/**
* Specifies the blending mode which should be used to apply the tint specified by
@@ -65,13 +64,12 @@
* @see androidx.core.graphics.drawable.DrawableCompat#setTintMode(Drawable,
* PorterDuff.Mode)
*/
- void setSupportButtonTintMode(@Nullable PorterDuff.Mode tintMode);
+ void setSupportButtonTintMode(PorterDuff.@Nullable Mode tintMode);
/**
* Returns the blending mode used to apply the tint to the button drawable
*
* @see #setSupportButtonTintMode(PorterDuff.Mode)
*/
- @Nullable
- PorterDuff.Mode getSupportButtonTintMode();
+ PorterDuff.@Nullable Mode getSupportButtonTintMode();
}
diff --git a/core/core/src/main/java/androidx/core/widget/TintableCompoundDrawablesView.java b/core/core/src/main/java/androidx/core/widget/TintableCompoundDrawablesView.java
index 5afe7a4..f11c4ff 100644
--- a/core/core/src/main/java/androidx/core/widget/TintableCompoundDrawablesView.java
+++ b/core/core/src/main/java/androidx/core/widget/TintableCompoundDrawablesView.java
@@ -19,7 +19,7 @@
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Interface which allows {@link android.widget.TextView} and subclasses to tint compound drawables
@@ -50,8 +50,7 @@
*
* @return the tint applied to the compound drawables
*/
- @Nullable
- ColorStateList getSupportCompoundDrawablesTintList();
+ @Nullable ColorStateList getSupportCompoundDrawablesTintList();
/**
* Specifies the blending mode used to apply the tint specified by
@@ -63,7 +62,7 @@
* @see #getSupportCompoundDrawablesTintMode()
* @see #setSupportCompoundDrawablesTintList(ColorStateList)
*/
- void setSupportCompoundDrawablesTintMode(@Nullable PorterDuff.Mode tintMode);
+ void setSupportCompoundDrawablesTintMode(PorterDuff.@Nullable Mode tintMode);
/**
* Returns the blending mode used to apply the tint to the compound
@@ -73,6 +72,5 @@
* drawables
* @see #setSupportCompoundDrawablesTintMode(PorterDuff.Mode)
*/
- @Nullable
- PorterDuff.Mode getSupportCompoundDrawablesTintMode();
+ PorterDuff.@Nullable Mode getSupportCompoundDrawablesTintMode();
}
diff --git a/core/core/src/main/java/androidx/core/widget/TintableImageSourceView.java b/core/core/src/main/java/androidx/core/widget/TintableImageSourceView.java
index abda77b..5e68657 100644
--- a/core/core/src/main/java/androidx/core/widget/TintableImageSourceView.java
+++ b/core/core/src/main/java/androidx/core/widget/TintableImageSourceView.java
@@ -21,9 +21,10 @@
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.Nullable;
+
/**
* Interface which allows an {@link android.widget.ImageView} to receive image tinting calls
* from {@link ImageViewCompat} when running on API v20 devices or lower.
@@ -55,8 +56,7 @@
*
* @return the tint applied to the image drawable
*/
- @Nullable
- ColorStateList getSupportImageTintList();
+ @Nullable ColorStateList getSupportImageTintList();
/**
* Specifies the blending mode used to apply the tint specified by
@@ -67,7 +67,7 @@
* {@code null} to clear tint
* @see #getSupportImageTintMode()
*/
- void setSupportImageTintMode(@Nullable PorterDuff.Mode tintMode);
+ void setSupportImageTintMode(PorterDuff.@Nullable Mode tintMode);
/**
* Return the blending mode used to apply the tint to the image
@@ -75,6 +75,5 @@
*
* @return the blending mode used to apply the tint to the image drawable
*/
- @Nullable
- PorterDuff.Mode getSupportImageTintMode();
+ PorterDuff.@Nullable Mode getSupportImageTintMode();
}
diff --git a/credentials/credentials-fido/build.gradle b/credentials/credentials-fido/build.gradle
index e8f1534..5a906b0 100644
--- a/credentials/credentials-fido/build.gradle
+++ b/credentials/credentials-fido/build.gradle
@@ -31,7 +31,7 @@
dependencies {
api(libs.kotlinStdlib)
- api project(":credentials:credentials")
+ api(project(":credentials:credentials"))
androidTestImplementation(libs.junit)
androidTestImplementation(libs.testExtJunit)
diff --git a/credentials/credentials-play-services-auth/build.gradle b/credentials/credentials-play-services-auth/build.gradle
index 9fd1bde..ab812ce 100644
--- a/credentials/credentials-play-services-auth/build.gradle
+++ b/credentials/credentials-play-services-auth/build.gradle
@@ -33,7 +33,7 @@
dependencies {
api(libs.kotlinStdlib)
- api project(":credentials:credentials")
+ api(project(":credentials:credentials"))
implementation("com.google.android.libraries.identity.googleid:googleid:1.1.0"){
exclude group: "androidx.credentials"
diff --git a/credentials/credentials-provider/build.gradle b/credentials/credentials-provider/build.gradle
deleted file mode 100644
index 0711936..0000000
--- a/credentials/credentials-provider/build.gradle
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2022 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
- *
- * http://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.
- */
-
-/**
- * This file was created using the `create_project.py` script located in the
- * `<AndroidX root>/development/project-creator` directory.
- *
- * Please use that script when creating a new project, rather than copying an existing project and
- * modifying its settings.
- */
-import androidx.build.Publish
-
-plugins {
- id("AndroidXPlugin")
- id("com.android.library")
- id("org.jetbrains.kotlin.android")
-}
-
-dependencies {
- api(libs.kotlinStdlib)
- // Add dependencies here
-}
-
-android {
- namespace = "androidx.credentials.provider"
-
- defaultConfig {
- }
-}
-
-androidx {
- name = "Credentials Provider"
- publish = Publish.NONE
- inceptionYear = "2022"
- description = "use utility APIs to process requests from, and return responses to the android" +
- "Credential Manager"
-}
diff --git a/credentials/credentials-provider/src/main/androidx/credentials/androidx-credentials-credentials-provider-documentation.md b/credentials/credentials-provider/src/main/androidx/credentials/androidx-credentials-credentials-provider-documentation.md
deleted file mode 100644
index d3a5d02..0000000
--- a/credentials/credentials-provider/src/main/androidx/credentials/androidx-credentials-credentials-provider-documentation.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# Module root
-
-CREDENTIALS CREDENTIALS PROVIDER
-
-# Package androidx.credentials.provider
-
-This package provides utility APIs for providers to use as they respond to credential requests
-from the android Credential Manager.
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerApi34Test.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerApi34Test.kt
index 59acd5d..a6ff959 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerApi34Test.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerApi34Test.kt
@@ -18,11 +18,13 @@
import android.content.Intent
import android.content.pm.SigningInfo
import android.credentials.CredentialOption
+import android.os.Binder
import android.os.Bundle
import android.service.credentials.CallingAppInfo
import android.service.credentials.CreateCredentialRequest
import android.service.credentials.GetCredentialRequest
import androidx.annotation.RequiresApi
+import androidx.credentials.CreateCustomCredentialResponse
import androidx.credentials.CreatePasswordResponse
import androidx.credentials.GetCredentialResponse
import androidx.credentials.PasswordCredential
@@ -30,6 +32,9 @@
import androidx.credentials.equals
import androidx.credentials.exceptions.CreateCredentialInterruptedException
import androidx.credentials.exceptions.GetCredentialInterruptedException
+import androidx.credentials.exceptions.domerrors.ConstraintError
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException
+import androidx.credentials.provider.PendingIntentHandler.Companion.setCreateCredentialResponse
import androidx.credentials.setUpCreatePasswordRequest
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
@@ -280,23 +285,38 @@
}
@Test
- fun test_createCredentialException() {
+ fun createCredentialException_success() {
val intent = Intent()
- val initialException = CreateCredentialInterruptedException("message")
+ val expected = CreateCredentialInterruptedException("message")
- PendingIntentHandler.setCreateCredentialException(intent, initialException)
+ PendingIntentHandler.setCreateCredentialException(intent, expected)
- val finalException = intent.getCreateCredentialException()
- assertThat(finalException).isNotNull()
- assertThat(finalException!!.type).isEqualTo(initialException.type)
- assertThat(finalException.message).isEqualTo(initialException.message)
+ val actual = PendingIntentHandler.retrieveCreateCredentialException(intent)!!
+ assertThat(actual).isInstanceOf(expected::class.java)
+ assertThat(actual.type).isEqualTo(expected.type)
+ assertThat(actual.errorMessage).isEqualTo(expected.errorMessage)
}
@Test
- fun test_createCredentialException_nullExceptionWhenEmptyIntent() {
+ fun createCredentialException_domException_success() {
+ val intent = Intent()
+ val expected = CreatePublicKeyCredentialDomException(ConstraintError(), "Error msg")
+
+ PendingIntentHandler.setCreateCredentialException(intent, expected)
+
+ val actual = PendingIntentHandler.retrieveCreateCredentialException(intent)!!
+ assertThat(actual).isInstanceOf(expected::class.java)
+ assertThat(actual.type).isEqualTo(expected.type)
+ assertThat(actual.errorMessage).isEqualTo(expected.errorMessage)
+ val actualConverted = actual as CreatePublicKeyCredentialDomException
+ assertThat(actualConverted.domError).isInstanceOf((expected.domError)::class.java)
+ }
+
+ @Test
+ fun createCredentialException_emptyIntent_returnsNull() {
val intent = Intent()
- assertThat(intent.getCreateCredentialException()).isNull()
+ assertThat(PendingIntentHandler.retrieveCreateCredentialException(intent)).isNull()
}
@Test
@@ -472,21 +492,34 @@
}
@Test
- fun test_createCredentialCredentialResponse() {
+ fun createCredentialCredentialResponse_passwordResponse_success() {
val intent = Intent()
- val initialResponse = CreatePasswordResponse()
+ val expected = CreatePasswordResponse()
- PendingIntentHandler.setCreateCredentialResponse(intent, initialResponse)
+ PendingIntentHandler.setCreateCredentialResponse(intent, expected)
- val finalResponse = intent.getCreateCredentialCredentialResponse()
- assertThat(finalResponse).isNotNull()
- assertThat(equals(finalResponse!!.data, initialResponse.data))
+ val actual = PendingIntentHandler.retrieveCreateCredentialResponse(expected.type, intent)!!
+ assertEquals(actual, expected)
}
@Test
- fun test_createCredentialCredentialResponse_nullWhenEmptyIntent() {
+ fun setCreateCredentialResponse_customResponse_success() {
val intent = Intent()
+ val customData = Bundle()
+ customData.putString("k1", "text")
+ customData.putBinder("k2", Binder())
+ val expected = CreateCustomCredentialResponse("type", customData)
- assertThat(intent.getCreateCredentialCredentialResponse()).isNull()
+ setCreateCredentialResponse(intent, expected)
+
+ val actual = PendingIntentHandler.retrieveCreateCredentialResponse(expected.type, intent)!!
+ assertEquals(actual, expected)
+ }
+
+ @Test
+ fun retrieveCreateCredentialResponse_emptyResponse_returnsNull() {
+ val actual = PendingIntentHandler.retrieveCreateCredentialResponse("type", Intent())
+
+ assertThat(actual).isNull()
}
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/IntentHandlerConverters.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/IntentHandlerConverters.kt
index 6b3e519..f2f4d5b 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/IntentHandlerConverters.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/IntentHandlerConverters.kt
@@ -15,11 +15,9 @@
*/
@file:JvmName("IntentHandlerConverters")
-@file:SuppressLint("ClassVerificationFailure")
package androidx.credentials.provider
-import android.annotation.SuppressLint
import android.content.Intent
import android.service.credentials.CredentialProviderService
import androidx.annotation.RequiresApi
diff --git a/credentials/registry/registry-digitalcredentials-mdoc/build.gradle b/credentials/registry/registry-digitalcredentials-mdoc/build.gradle
index 9ca8052..45eb760 100644
--- a/credentials/registry/registry-digitalcredentials-mdoc/build.gradle
+++ b/credentials/registry/registry-digitalcredentials-mdoc/build.gradle
@@ -31,7 +31,7 @@
dependencies {
api(libs.kotlinStdlib)
- api project(":credentials:registry:registry-provider")
+ api(project(":credentials:registry:registry-provider"))
androidTestImplementation(libs.junit)
androidTestImplementation(libs.testExtJunit)
diff --git a/credentials/registry/registry-digitalcredentials-preview/build.gradle b/credentials/registry/registry-digitalcredentials-preview/build.gradle
index 4381874..6ff687e 100644
--- a/credentials/registry/registry-digitalcredentials-preview/build.gradle
+++ b/credentials/registry/registry-digitalcredentials-preview/build.gradle
@@ -31,8 +31,8 @@
dependencies {
api(libs.kotlinStdlib)
- api project(":credentials:registry:registry-provider")
- api project(":credentials:registry:registry-digitalcredentials-mdoc")
+ api(project(":credentials:registry:registry-provider"))
+ api(project(":credentials:registry:registry-digitalcredentials-mdoc"))
androidTestImplementation(libs.junit)
androidTestImplementation(libs.testExtJunit)
diff --git a/credentials/registry/registry-provider-play-services/build.gradle b/credentials/registry/registry-provider-play-services/build.gradle
index 4e5a0f4..e284a25 100644
--- a/credentials/registry/registry-provider-play-services/build.gradle
+++ b/credentials/registry/registry-provider-play-services/build.gradle
@@ -33,7 +33,7 @@
dependencies {
api(libs.kotlinStdlib)
- api project(":credentials:registry:registry-provider")
+ api(project(":credentials:registry:registry-provider"))
implementation(libs.playServicesIdentityCredentials){
exclude group: "androidx.loader"
diff --git a/credentials/registry/registry-provider/build.gradle b/credentials/registry/registry-provider/build.gradle
index fcae23a..2cb6006 100644
--- a/credentials/registry/registry-provider/build.gradle
+++ b/credentials/registry/registry-provider/build.gradle
@@ -31,7 +31,7 @@
dependencies {
api(libs.kotlinStdlib)
- api project(":credentials:credentials")
+ api(project(":credentials:credentials"))
implementation(libs.kotlinCoroutinesCore)
androidTestImplementation(libs.junit)
diff --git a/datastore/datastore-preferences-rxjava2/src/main/java/androidx/datastore/preferences/rxjava2/RxPreferenceDataStoreDelegate.kt b/datastore/datastore-preferences-rxjava2/src/main/java/androidx/datastore/preferences/rxjava2/RxPreferenceDataStoreDelegate.kt
index 1863a60..fd4a661 100644
--- a/datastore/datastore-preferences-rxjava2/src/main/java/androidx/datastore/preferences/rxjava2/RxPreferenceDataStoreDelegate.kt
+++ b/datastore/datastore-preferences-rxjava2/src/main/java/androidx/datastore/preferences/rxjava2/RxPreferenceDataStoreDelegate.kt
@@ -95,7 +95,7 @@
INSTANCE =
with(RxPreferenceDataStoreBuilder(applicationContext, fileName)) {
setIoScheduler(scheduler)
- @Suppress("NewApi", "ClassVerificationFailure") // b/187418647
+ @Suppress("NewApi") // b/187418647
produceMigrations(applicationContext).forEach { addDataMigration(it) }
corruptionHandler?.let { setCorruptionHandler(it) }
build()
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index cadecaa..eb367a7 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -356,4 +356,8 @@
# Warning emitted on empty Java files generated from AIDL:
# https://github.com/kythe/kythe/issues/6145
.*com.google\.devtools\.kythe\.extractors\.java\.JavaCompilationUnitExtractor.*
-^WARNING: Empty java source file.*
\ No newline at end of file
+^WARNING: Empty java source file.*
+# b/359397747 Add PrivacySandboxService to mediatee-sdk-provider
+> Transform sdk\-interface\-descriptors\.jar \(project :privacysandbox:ui:integration\-tests:testsdkproviderwrapper\) with ExtractCompileSdkShimTransform
+> Task :privacysandbox:ui:integration\-tests:mediateesdkproviderwrapper:buildModuleForBundle
+> Task :privacysandbox:ui:integration\-tests:mediateesdkproviderwrapper:bundle
diff --git a/development/publishScan.sh b/development/publishScan.sh
index 8845567..778fec2 100755
--- a/development/publishScan.sh
+++ b/development/publishScan.sh
@@ -30,15 +30,11 @@
fi
fi
# find scan dir
-if [ "$OUT_DIR" != "" ]; then
- effectiveGradleUserHome="$OUT_DIR/.gradle"
-else
- if [ "$GRADLE_USER_HOME" != "" ]; then
- effectiveGradleUserHome="$GRADLE_USER_HOME"
- else
- effectiveGradleUserHome="$HOME/.gradle"
- fi
+if [ -z "${OUT_DIR+x}" ] ; then
+ SCRIPT_PATH="$(cd $(dirname $0) && pwd -P)"
+ export OUT_DIR=$SCRIPT_PATH/../../../out
fi
+effectiveGradleUserHome="$OUT_DIR/.gradle"
scanDir="$effectiveGradleUserHome/build-scan-data"
function downloadScan() {
diff --git a/development/studio/studio.vmoptions b/development/studio/studio.vmoptions
index c70257b..166d646 100644
--- a/development/studio/studio.vmoptions
+++ b/development/studio/studio.vmoptions
@@ -5,6 +5,8 @@
-Djdk.attach.allowAttachSelf=true
# b/297379481
-Dprofiler.trace.open.mode.web=true
+# b/343774023
+-Didea.kotlin.plugin.use.k2=true
# https://github.com/google/google-java-format#intellij-jre-config
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
diff --git a/development/validateRefactor.sh b/development/validateRefactor.sh
index 62e907e..f9fe9c6 100755
--- a/development/validateRefactor.sh
+++ b/development/validateRefactor.sh
@@ -119,7 +119,7 @@
}
function doBuild() {
# build androidx
- echoAndDo ./gradlew createArchive zipDocs --no-daemon --rerun-tasks --offline -Pandroidx.highMemory
+ echoAndDo ./gradlew createAllArchives zipDocs --no-daemon --rerun-tasks --offline -Pandroidx.highMemory -Pandroidx.constraints=true
archiveName="top-of-tree-m2repository-all-0.zip"
unzipInPlace "${tempOutPath}/dist/top-of-tree-m2repository-all-0.zip"
unzipInPlace "${tempOutPath}/dist/docs-tip-of-tree-0.zip"
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 45a7584..18bda36 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -452,6 +452,15 @@
docs("androidx.work:work-rxjava2:2.10.0")
docs("androidx.work:work-rxjava3:2.10.0")
docs("androidx.work:work-testing:2.10.0")
+ docs("androidx.xr.arcore:arcore:1.0.0-alpha01")
+ docs("androidx.xr.compose.material3:material3:1.0.0-alpha01")
+ docs("androidx.xr.compose:compose:1.0.0-alpha01")
+ docs("androidx.xr.compose:compose-testing:1.0.0-alpha01")
+ docs("androidx.xr.runtime:runtime:1.0.0-alpha01")
+ docs("androidx.xr.runtime:runtime-openxr:1.0.0-alpha01")
+ docs("androidx.xr.runtime:runtime-testing:1.0.0-alpha01")
+ docs("androidx.xr.scenecore:scenecore:1.0.0-alpha01")
+ docs("androidx.xr.scenecore:scenecore-testing:1.0.0-alpha01")
}
afterEvaluate {
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 20e9239..b28ed69 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -22,8 +22,6 @@
// If there is not at least one samples dependency, DocsImplPlugin breaks. b/332262321
samples("androidx.window:window-samples:1.3.0")
- docsForOptionalProject(":xr:xr")
- docsForOptionalProject(":xr:xr-material3-adaptive")
docs(project(":activity:activity"))
docs(project(":activity:activity-compose"))
docs(project(":activity:activity-ktx"))
@@ -48,6 +46,7 @@
docs(project(":benchmark:benchmark-junit4"))
docs(project(":benchmark:benchmark-macro"))
docs(project(":benchmark:benchmark-macro-junit4"))
+ kmpDocs(project(":benchmark:benchmark-traceprocessor"))
docs(project(":biometric:biometric"))
docs(project(":bluetooth:bluetooth"))
docs(project(":bluetooth:bluetooth-testing"))
@@ -56,7 +55,6 @@
docs(project(":camera:camera-compose"))
docs(project(":camera:camera-core"))
docs(project(":camera:camera-effects"))
- docs(project(":camera:camera-effects-still-portrait"))
docs(project(":camera:camera-extensions"))
stubs(fileTree(dir: "../camera/camera-extensions-stub", include: ["camera-extensions-stub.jar"]))
docs(project(":camera:camera-feature-combination-query"))
@@ -413,6 +411,16 @@
docs(project(":work:work-rxjava2"))
docs(project(":work:work-rxjava3"))
docs(project(":work:work-testing"))
+ docs(project(":xr:arcore:arcore"))
+ docs(project(":xr:compose:compose"))
+ docs(project(":xr:compose:compose-testing"))
+ docs(project(":xr:compose:material3:material3"))
+ docs(project(":xr:runtime:runtime"))
+ docs(project(":xr:runtime:runtime-openxr"))
+ docs(project(":xr:runtime:runtime-testing"))
+ docs(project(":xr:scenecore:scenecore"))
+ docs(project(":xr:scenecore:scenecore-testing"))
+ docs(project(":xr:xr-stubs"))
}
afterEvaluate {
tasks["docs"].doFirst {
diff --git a/docs/api_guidelines/compat.md b/docs/api_guidelines/compat.md
index 78b351c..b258646 100644
--- a/docs/api_guidelines/compat.md
+++ b/docs/api_guidelines/compat.md
@@ -42,140 +42,6 @@
}
```
-##### Preventing invalid casting {#compat-casting}
-
-Even when a call to a new API is moved to a version-specific class, a class
-verification failure is still possible when referencing types introduced in new
-APIs.
-
-When a type does not exist on a device, the verifier treats the type as
-`Object`. This is a problem if the new type is implicitly cast to a different
-type which does exist on the device.
-
-In general, if `A extends B`, using an `A` as a `B` without an explicit cast is
-fine. However, if `A` was introduced at a later API level than `B`, on devices
-below that API level, `A` will be seen as `Object`. An `Object` cannot be used
-as a `B` without an explicit cast. However, adding an explicit cast to `B` won't
-fix this, because the compiler will see the cast as redundant (as it normally
-would be). So, implicit casts between types introduced at different API levels
-should be moved out to version-specific static inner classes.
-
-The `ImplicitCastClassVerificationFailure` lint check detects and provides
-autofixes for instances of invalid implicit casts.
-
-For instance, the following would **not** be valid, because it implicitly casts
-an `AdaptiveIconDrawable` (new in API level 26, `Object` on lower API levels) to
-`Drawable`. Instead, the method inside of `Api26Impl` could return `Drawable`,
-or the cast could be moved into a version-specific static inner class.
-
-```java {.bad}
-private Drawable methodReturnsDrawable() {
- if (Build.VERSION.SDK_INT >= 26) {
- // Implicitly casts the returned AdaptiveIconDrawable to Drawable
- return Api26Impl.createAdaptiveIconDrawable(null, null);
- } else {
- return null;
- }
-}
-
-@RequiresApi(26)
-static class Api26Impl {
- // Returns AdaptiveIconDrawable, introduced in API level 26
- @DoNotInline
- static AdaptiveIconDrawable createAdaptiveIconDrawable(Drawable backgroundDrawable, Drawable foregroundDrawable) {
- return new AdaptiveIconDrawable(backgroundDrawable, foregroundDrawable);
- }
-}
-```
-
-The version-specific static inner class solution would look like this:
-
-```java {.good}
-private Drawable methodReturnsDrawable() {
- if (Build.VERSION.SDK_INT >= 26) {
- return Api26Impl.castToDrawable(Api26Impl.createAdaptiveIconDrawable(null, null));
- } else {
- return null;
- }
-}
-
-@RequiresApi(26)
-static class Api26Impl {
- // Returns AdaptiveIconDrawable, introduced in API level 26
- @DoNotInline
- static AdaptiveIconDrawable createAdaptiveIconDrawable(Drawable backgroundDrawable, Drawable foregroundDrawable) {
- return new AdaptiveIconDrawable(backgroundDrawable, foregroundDrawable);
- }
-
- // Method which performs the implicit cast from AdaptiveIconDrawable to Drawable
- @DoNotInline
- static Drawable castToDrawable(AdaptiveIconDrawable adaptiveIconDrawable) {
- return adaptiveIconDrawable;
- }
-}
-```
-
-The following would also **not** be valid, because it implicitly casts a
-`Notification.MessagingStyle` (new in API level 24, `Object` on lower API
-levels) to `Notification.Style`. Instead, `Api24Impl` could have a `setBuilder`
-method which takes `Notification.MessagingStyle` as a parameter, or the cast
-could be moved into a version-specific static inner class.
-
-```java {.bad}
-public void methodUsesStyle(Notification.MessagingStyle style, Notification.Builder builder) {
- if (Build.VERSION.SDK_INT >= 24) {
- Api16Impl.setBuilder(
- // Implicitly casts the style to Notification.Style (added in API level 16)
- // when it is a Notification.MessagingStyle (added in API level 24)
- style, builder
- );
- }
-}
-
-@RequiresApi(16)
-static class Api16Impl {
- private Api16Impl() { }
-
- @DoNotInline
- static void setBuilder(Notification.Style style, Notification.Builder builder) {
- style.setBuilder(builder);
- }
-}
-```
-
-The version-specific static inner class solution would look like this:
-
-```java {.good}
-public void methodUsesStyle(Notification.MessagingStyle style, Notification.Builder builder) {
- if (Build.VERSION.SDK_INT >= 24) {
- Api16Impl.setBuilder(
- Api24Impl.castToStyle(style), builder
- );
- }
-}
-
-@RequiresApi(16)
-static class Api16Impl {
- private Api16Impl() { }
-
- @DoNotInline
- static void setBuilder(Notification.Style style, Notification.Builder builder) {
- style.setBuilder(builder);
- }
-}
-
-@RequiresApi(24)
-static class Api24Impl {
- private Api24Impl() { }
-
- // Performs the implicit cast from Notification.MessagingStyle to Notification.Style
- @DoNotInline
- static Notification.Style castToStyle(Notification.MessagingStyle messagingStyle) {
- return messagingStyle;
- }
-}
-```
-
#### Validating class verification
To verify that your library does not raise class verification failures, look for
diff --git a/docs/api_guidelines/platform_compat.md b/docs/api_guidelines/platform_compat.md
index 40b4a74..95954ac 100644
--- a/docs/api_guidelines/platform_compat.md
+++ b/docs/api_guidelines/platform_compat.md
@@ -299,52 +299,3 @@
the fact that Room generates code dynamically, means that Room interfaces can be
used in host-side tests (though actual DB code should be tested on device, since
DB impls may be significantly different on host).
-
-### Addressing class verification failures on `super.` invocation {#compat-super}
-
-Invoking a `super` call on a method introduced in an API level higher than a
-class's minimum SDK level will raise a run-time class verification failure, and
-will be detected by the `ClassVerificationFailure` lint check.
-
-```java {.bad}
-public void performAction() {
- if (SDK_INT >= 31) {
- super.performAction(); // This will cause a verification failure.
- }
-}
-```
-
-These failures can be addressed by out-of-lining the `super` call to a
-non-static inner class.
-
-#### Sample {#compat-super-sample}
-
-```java
-class AppCompatTextView : TextView {
-
- @Nullable
- SuperCaller mSuperCaller = null;
-
- @Override
- int getPropertyFromApi99() {
- if (Build.VERSION.SDK_INT > 99) {
- getSuperCaller().getPropertyFromApi99)();
- }
-
- @NonNull
- @RequiresApi(99)
- private SuperCaller getSuperCaller() {
- if (mSuperCaller == null) {
- mSuperCaller = new Api99SuperCaller();
- }
- return mSuperCaller;
- }
-
- @RequiresApi(99)
- private class Api99SuperCaller {
- int getPropertyFromApi99() {
- return AppCompatTextView.super.getPropertyFromApi99();
- }
- }
-}
-```
diff --git a/emoji/emoji/build.gradle b/emoji/emoji/build.gradle
index ef08527..6b429dc 100644
--- a/emoji/emoji/build.gradle
+++ b/emoji/emoji/build.gradle
@@ -44,7 +44,6 @@
android {
sourceSets {
main {
- res.srcDirs += "src/main/res-public"
resources {
srcDirs += [fontDir.getAbsolutePath()]
includes += ["LICENSE_UNICODE", "LICENSE_OFL"]
diff --git a/emoji/emoji/src/main/res-public/values/public_attrs.xml b/emoji/emoji/src/main/res/values/public_attrs.xml
similarity index 100%
rename from emoji/emoji/src/main/res-public/values/public_attrs.xml
rename to emoji/emoji/src/main/res/values/public_attrs.xml
diff --git a/emoji2/emoji2-benchmark/build.gradle b/emoji2/emoji2-benchmark/build.gradle
index 2e0b2d5..ed2ec2f 100644
--- a/emoji2/emoji2-benchmark/build.gradle
+++ b/emoji2/emoji2-benchmark/build.gradle
@@ -58,7 +58,7 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation project(":internal-testutils-runtime")
+ androidTestImplementation(project(":internal-testutils-runtime"))
androidTestImplementation(libs.kotlinStdlib)
}
diff --git a/emoji2/emoji2-bundled/build.gradle b/emoji2/emoji2-bundled/build.gradle
index 7782803..171787cf 100644
--- a/emoji2/emoji2-bundled/build.gradle
+++ b/emoji2/emoji2-bundled/build.gradle
@@ -47,10 +47,10 @@
androidTestImplementation(libs.espressoCore)
androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation project(":internal-testutils-runtime")
+ androidTestImplementation(project(":internal-testutils-runtime"))
// view tests that use font are in this module as well; for licensing reasons
- androidTestImplementation project(":emoji2:emoji2-views")
+ androidTestImplementation(project(":emoji2:emoji2-views"))
}
androidx {
diff --git a/emoji2/emoji2-emojipicker/build.gradle b/emoji2/emoji2-emojipicker/build.gradle
index bdead6a..a5a03eb 100644
--- a/emoji2/emoji2-emojipicker/build.gradle
+++ b/emoji2/emoji2-emojipicker/build.gradle
@@ -54,10 +54,6 @@
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
- sourceSets {
- androidTest.assets.srcDirs += project.rootDir.absolutePath + "/../../golden/emoji2/emoji2-emojipicker"
- }
-
namespace = "androidx.emoji2.emojipicker"
testOptions.unitTests.includeAndroidResources = true
}
@@ -69,4 +65,5 @@
description = "This library provides the latest emoji support and emoji picker UI to input " +
"emoji in current and older devices"
legacyDisableKotlinStrictApiMode = true
+ addGoldenImageAssets()
}
diff --git a/emoji2/emoji2-views-helper/build.gradle b/emoji2/emoji2-views-helper/build.gradle
index 59d5096..5ceae4e 100644
--- a/emoji2/emoji2-views-helper/build.gradle
+++ b/emoji2/emoji2-views-helper/build.gradle
@@ -26,7 +26,7 @@
androidTestImplementation(libs.espressoCore)
androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation project(":internal-testutils-runtime")
+ androidTestImplementation(project(":internal-testutils-runtime"))
}
android {
diff --git a/emoji2/emoji2-views/build.gradle b/emoji2/emoji2-views/build.gradle
index d1145d1..7db1ff8 100644
--- a/emoji2/emoji2-views/build.gradle
+++ b/emoji2/emoji2-views/build.gradle
@@ -27,15 +27,10 @@
androidTestImplementation(libs.espressoCore)
androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation project(":internal-testutils-runtime")
+ androidTestImplementation(project(":internal-testutils-runtime"))
}
android {
- sourceSets {
- main {
- res.srcDirs += 'src/main/res-public'
- }
- }
namespace = "androidx.emoji2.widget"
}
diff --git a/emoji2/emoji2-views/src/main/res-public/values/public_attrs.xml b/emoji2/emoji2-views/src/main/res/values/public_attrs.xml
similarity index 100%
rename from emoji2/emoji2-views/src/main/res-public/values/public_attrs.xml
rename to emoji2/emoji2-views/src/main/res/values/public_attrs.xml
diff --git a/emoji2/emoji2/build.gradle b/emoji2/emoji2/build.gradle
index 083f71c..adecc35 100644
--- a/emoji2/emoji2/build.gradle
+++ b/emoji2/emoji2/build.gradle
@@ -41,7 +41,7 @@
androidTestImplementation(libs.espressoCore)
androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation project(":internal-testutils-runtime")
+ androidTestImplementation(project(":internal-testutils-runtime"))
}
androidx {
diff --git a/glance/glance-appwidget/build.gradle b/glance/glance-appwidget/build.gradle
index d7e8d3c..098cf0f 100644
--- a/glance/glance-appwidget/build.gradle
+++ b/glance/glance-appwidget/build.gradle
@@ -88,11 +88,6 @@
// Use Robolectric 4.+
testOptions.unitTests.includeAndroidResources = true
resourcePrefix "glance_"
-
- sourceSets {
- androidTest.assets.srcDirs += project.rootDir.absolutePath + "/../../golden/glance/glance-appwidget"
- }
-
buildTypes {
debug {
pseudoLocalesEnabled = true
@@ -118,6 +113,7 @@
legacyDisableKotlinStrictApiMode = true
metalavaK2UastEnabled = false
samples(project(":glance:glance-appwidget:glance-appwidget-samples"))
+ addGoldenImageAssets()
}
LayoutGeneratorTask.registerLayoutGenerator(
diff --git a/glance/glance-wear-tiles/build.gradle b/glance/glance-wear-tiles/build.gradle
index da875d3..a21f20d 100644
--- a/glance/glance-wear-tiles/build.gradle
+++ b/glance/glance-wear-tiles/build.gradle
@@ -84,12 +84,7 @@
// protobuf generates unannotated methods
disable "UnknownNullness"
}
-
- sourceSets {
- androidTest.assets.srcDirs += project.rootDir.absolutePath + "/../../golden/glance/glance-wear-tiles"
- }
namespace = "androidx.glance.wear.tiles"
-
}
androidx {
@@ -101,4 +96,5 @@
"Compose-style API."
legacyDisableKotlinStrictApiMode = true
metalavaK2UastEnabled = false
+ addGoldenImageAssets()
}
diff --git a/gradle.properties b/gradle.properties
index ecc3a8f..4442086 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -62,7 +62,7 @@
android.experimental.dependency.excludeLibraryComponentsFromConstraints=true
# Disallow resolving dependencies at configuration time, which is a slight performance problem
android.dependencyResolutionAtConfigurationTime.disallow=true
-android.suppressUnsupportedOptionWarnings=android.suppressUnsupportedOptionWarnings,android.dependencyResolutionAtConfigurationTime.disallow,android.experimental.lint.missingBaselineIsEmptyBaseline,android.lint.printStackTrace,android.lint.baselineOmitLineNumbers,android.experimental.disableCompileSdkChecks,android.overrideVersionCheck,android.r8.maxWorkers,android.experimental.lint.reservedMemoryPerTask,android.experimental.dependency.excludeLibraryComponentsFromConstraints,android.prefabVersion,android.experimental.privacysandboxsdk.plugin.enable,android.experimental.privacysandboxsdk.requireServices,android.lint.useK2Uast
+android.suppressUnsupportedOptionWarnings=android.suppressUnsupportedOptionWarnings,android.dependencyResolutionAtConfigurationTime.disallow,android.experimental.lint.missingBaselineIsEmptyBaseline,android.lint.printStackTrace,android.lint.baselineOmitLineNumbers,android.experimental.disableCompileSdkChecks,android.overrideVersionCheck,android.r8.maxWorkers,android.experimental.lint.reservedMemoryPerTask,android.experimental.dependency.excludeLibraryComponentsFromConstraints,android.prefabVersion,android.experimental.privacysandboxsdk.plugin.enable,android.experimental.privacysandboxsdk.requireServices,android.lint.useK2Uast,android.experimental.skipApksViaBundleIfPossible
# Workaround for b/162074215
android.includeDependencyInfoInApks=false
# Allow multiple r8 tasks at once because otherwise they can make the critical path longer: b/256187923
@@ -94,6 +94,9 @@
# Allow non-shim usage
android.experimental.privacysandboxsdk.requireServices=false
+# Use fast-path APKs from AGP, ensuring that single APK will be used (for FTL configs)
+android.experimental.skipApksViaBundleIfPossible=true
+
# Annotation processors discovery from compile classpath is deprecated
kapt.include.compile.classpath=false
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 918ddde..d554032 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -38,6 +38,7 @@
<trust group="com.google.devsite" reason="developed by our own team, not signed yet"/>
<trust group="com.google.firebase" reason="b/223907608"/>
<trust group="com.google.guava" reasons="b/325084664 https://github.com/gradle/gradle/issues/28862"/>
+ <trust group="com.google.ar" reason="b/378946732"/>
<trust group="com.google.mlkit" reason="b/223907608"/>
<trust group="com.google.prefab" reason="https://github.com/google/prefab/issues/157"/>
<trust group="com.google.testing.platform" reason="b/215430394"/>
diff --git a/graphics/graphics-core/samples/build.gradle b/graphics/graphics-core/samples/build.gradle
index c3edfbf..655493e 100644
--- a/graphics/graphics-core/samples/build.gradle
+++ b/graphics/graphics-core/samples/build.gradle
@@ -27,8 +27,8 @@
dependencies {
implementation(libs.kotlinStdlib)
- compileOnly project(":annotation:annotation-sampled")
- implementation project(":graphics:graphics-core")
+ compileOnly(project(":annotation:annotation-sampled"))
+ implementation(project(":graphics:graphics-core"))
implementation "androidx.annotation:annotation:1.8.1"
}
diff --git a/health/connect/connect-client/samples/build.gradle b/health/connect/connect-client/samples/build.gradle
index 0f3267b..0c314fa 100644
--- a/health/connect/connect-client/samples/build.gradle
+++ b/health/connect/connect-client/samples/build.gradle
@@ -33,8 +33,8 @@
dependencies {
implementation(libs.kotlinStdlib)
- compileOnly project(":annotation:annotation-sampled")
- implementation project(":health:connect:connect-client")
+ compileOnly(project(":annotation:annotation-sampled"))
+ implementation(project(":health:connect:connect-client"))
implementation ("androidx.appcompat:appcompat:1.6.0")
implementation ("androidx.activity:activity:1.6.0")
}
diff --git a/health/connect/connect-testing/build.gradle b/health/connect/connect-testing/build.gradle
index 52cc4f4..7f12554 100644
--- a/health/connect/connect-testing/build.gradle
+++ b/health/connect/connect-testing/build.gradle
@@ -32,7 +32,7 @@
dependencies {
api(libs.kotlinStdlib)
- implementation project(":health:connect:connect-client")
+ implementation(project(":health:connect:connect-client"))
implementation(project(":health:connect:connect-client-proto"))
testImplementation(libs.kotlinTest)
diff --git a/hilt/hilt-navigation-compose/build.gradle b/hilt/hilt-navigation-compose/build.gradle
index 2d97039..63a965d 100644
--- a/hilt/hilt-navigation-compose/build.gradle
+++ b/hilt/hilt-navigation-compose/build.gradle
@@ -45,7 +45,7 @@
dependencies {
implementation(libs.kotlinStdlib)
- api project(":hilt:hilt-navigation")
+ api(project(":hilt:hilt-navigation"))
api("androidx.compose.runtime:runtime:1.0.1")
api("androidx.compose.ui:ui:1.0.1")
api("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
diff --git a/ink/ink-authoring/build.gradle b/ink/ink-authoring/build.gradle
index fe47346..ab75911 100644
--- a/ink/ink-authoring/build.gradle
+++ b/ink/ink-authoring/build.gradle
@@ -18,56 +18,54 @@
import androidx.build.PlatformIdentifier
plugins {
- id("AndroidXPlugin")
- id("com.android.library")
+ id("AndroidXPlugin")
+ id("com.android.library")
}
androidXMultiplatform {
- android()
+ android()
- defaultPlatform(PlatformIdentifier.ANDROID)
+ defaultPlatform(PlatformIdentifier.ANDROID)
- sourceSets {
+ sourceSets {
- androidMain {
- dependencies {
- implementation("androidx.collection:collection:1.4.3")
- implementation("androidx.graphics:graphics-core:1.0.2")
- implementation("androidx.fragment:fragment-ktx:1.3.0")
- implementation("androidx.test.espresso:espresso-idling-resource:3.5.0")
- implementation("androidx.core:core:1.1.0")
- implementation("androidx.core:core-ktx:1.12.0")
- implementation(project(":ink:ink-nativeloader"))
- implementation(project(":ink:ink-geometry"))
- implementation(project(":ink:ink-brush"))
- implementation(project(":ink:ink-strokes"))
- implementation(project(":ink:ink-rendering"))
- }
+ androidMain {
+ dependencies {
+ implementation("androidx.collection:collection:1.4.3")
+ implementation("androidx.graphics:graphics-core:1.0.2")
+ implementation("androidx.fragment:fragment-ktx:1.3.0")
+ implementation("androidx.test.espresso:espresso-idling-resource:3.5.0")
+ implementation("androidx.core:core:1.1.0")
+ implementation("androidx.core:core-ktx:1.12.0")
+ implementation(project(":ink:ink-nativeloader"))
+ implementation(project(":ink:ink-geometry"))
+ implementation(project(":ink:ink-brush"))
+ implementation(project(":ink:ink-strokes"))
+ implementation(project(":ink:ink-rendering"))
+ }
+ }
+
+ androidInstrumentedTest {
+ dependencies {
+ implementation(libs.testExtJunit)
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.espressoCore)
+ implementation(libs.junit)
+ implementation(libs.kotlinTest)
+ implementation(libs.mockitoCore4)
+ implementation(libs.mockitoKotlin4)
+ implementation(libs.dexmakerMockito)
+ implementation(libs.truth)
+ implementation(project(":test:screenshot:screenshot"))
+ }
+ }
}
-
- androidInstrumentedTest {
- dependencies {
- implementation(libs.testExtJunit)
- implementation(libs.testRules)
- implementation(libs.testRunner)
- implementation(libs.espressoCore)
- implementation(libs.junit)
- implementation(libs.kotlinTest)
- implementation(libs.mockitoCore4)
- implementation(libs.mockitoKotlin4)
- implementation(libs.dexmakerMockito)
- implementation(libs.truth)
- implementation(project(":test:screenshot:screenshot"))
- }
- }
- }
}
android {
- namespace = "androidx.ink.authoring"
- compileSdk = 35
- sourceSets.androidTest.assets.srcDirs +=
- project.rootDir.absolutePath + "/../../golden/ink/ink-authoring"
+ namespace = "androidx.ink.authoring"
+ compileSdk = 35
}
androidx {
@@ -75,4 +73,5 @@
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2024"
description = "Author beautiful strokes"
+ addGoldenImageAssets()
}
diff --git a/ink/ink-rendering/build.gradle b/ink/ink-rendering/build.gradle
index 5356b1a..ddce6b9 100644
--- a/ink/ink-rendering/build.gradle
+++ b/ink/ink-rendering/build.gradle
@@ -18,49 +18,46 @@
import androidx.build.PlatformIdentifier
plugins {
- id("AndroidXPlugin")
- id("com.android.library")
+ id("AndroidXPlugin")
+ id("com.android.library")
}
androidXMultiplatform {
- android()
+ android()
- defaultPlatform(PlatformIdentifier.ANDROID)
+ defaultPlatform(PlatformIdentifier.ANDROID)
- sourceSets {
+ sourceSets {
+ androidMain {
+ dependencies {
+ implementation("androidx.collection:collection:1.4.3")
+ implementation("androidx.core:core:1.1.0")
+ implementation("androidx.core:core-ktx:1.12.0")
+ implementation(project(":ink:ink-nativeloader"))
+ implementation(project(":ink:ink-geometry"))
+ implementation(project(":ink:ink-brush"))
+ implementation(project(":ink:ink-strokes"))
+ }
+ }
- androidMain {
- dependencies {
- implementation("androidx.collection:collection:1.4.3")
- implementation("androidx.core:core:1.1.0")
- implementation("androidx.core:core-ktx:1.12.0")
- implementation(project(":ink:ink-nativeloader"))
- implementation(project(":ink:ink-geometry"))
- implementation(project(":ink:ink-brush"))
- implementation(project(":ink:ink-strokes"))
- }
+ androidInstrumentedTest {
+ dependencies {
+ implementation(libs.testExtJunit)
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.espressoCore)
+ implementation(libs.junit)
+ implementation(libs.truth)
+ implementation(project(":core:core-ktx"))
+ implementation(project(":test:screenshot:screenshot"))
+ }
+ }
}
-
- androidInstrumentedTest {
- dependencies {
- implementation(libs.testExtJunit)
- implementation(libs.testRules)
- implementation(libs.testRunner)
- implementation(libs.espressoCore)
- implementation(libs.junit)
- implementation(libs.truth)
- implementation(project(":core:core-ktx"))
- implementation(project(":test:screenshot:screenshot"))
- }
- }
- }
}
android {
- namespace = "androidx.ink.rendering"
- compileSdk = 35
- sourceSets.androidTest.assets.srcDirs +=
- project.rootDir.absolutePath + "/../../golden/ink/ink-rendering"
+ namespace = "androidx.ink.rendering"
+ compileSdk = 35
}
androidx {
@@ -68,4 +65,5 @@
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2024"
description = "Display beautiful strokes"
+ addGoldenImageAssets()
}
diff --git a/javascriptengine/javascriptengine/lint.xml b/javascriptengine/javascriptengine/lint.xml
index ba04352..eee38c97 100644
--- a/javascriptengine/javascriptengine/lint.xml
+++ b/javascriptengine/javascriptengine/lint.xml
@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
- <!-- We cannot cause ClassVerificationFailure in embedding apps -->
- <issue id="ClassVerificationFailure" severity="fatal" />
<!-- Developers need to call our code from Kotlin code, so nullness is important.-->
<issue id="UnknownNullness" severity="fatal" />
</lint>
diff --git a/leanback/leanback-grid/build.gradle b/leanback/leanback-grid/build.gradle
index 0618308..9d9e7ee 100644
--- a/leanback/leanback-grid/build.gradle
+++ b/leanback/leanback-grid/build.gradle
@@ -29,6 +29,7 @@
}
dependencies {
+ api(libs.jspecify)
api("androidx.annotation:annotation:1.8.1")
api("androidx.core:core:1.1.0")
api("androidx.recyclerview:recyclerview:1.3.2")
diff --git a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/BaseGridView.java b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/BaseGridView.java
index 2c302f1..d3ae748 100644
--- a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/BaseGridView.java
+++ b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/BaseGridView.java
@@ -28,12 +28,13 @@
import android.view.View;
import android.view.animation.Interpolator;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.SimpleItemAnimator;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* An abstract base class for vertically and horizontally scrolling lists. The items come
* from the {@link RecyclerView.Adapter} associated with this view.
@@ -181,8 +182,7 @@
* @param dy y distance in pixels.
* @return Interpolator to be used or null for default interpolator.
*/
- @Nullable
- Interpolator configSmoothScrollByInterpolator(int dx, int dy);
+ @Nullable Interpolator configSmoothScrollByInterpolator(int dx, int dy);
}
/**
@@ -236,7 +236,7 @@
*
* @param state Transient state of RecyclerView
*/
- void onLayoutCompleted(@NonNull RecyclerView.State state);
+ void onLayoutCompleted(RecyclerView.@NonNull State state);
}
GridLayoutManager mLayoutManager;
@@ -281,7 +281,7 @@
((SimpleItemAnimator) getItemAnimator()).setSupportsChangeAnimations(false);
super.addRecyclerListener(new RecyclerView.RecyclerListener() {
@Override
- public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
+ public void onViewRecycled(RecyclerView.@NonNull ViewHolder holder) {
mLayoutManager.onChildRecycled(holder);
}
});
@@ -815,7 +815,7 @@
* @param task Task to executed on the ViewHolder at a given position.
*/
@SuppressWarnings("deprecation")
- public void setSelectedPositionSmooth(final int position, @Nullable final ViewHolderTask task) {
+ public void setSelectedPositionSmooth(final int position, final @Nullable ViewHolderTask task) {
if (task != null) {
RecyclerView.ViewHolder vh = findViewHolderForPosition(position);
if (vh == null || hasPendingAdapterUpdates()) {
@@ -843,7 +843,7 @@
* @param task Task to executed on the ViewHolder at a given position.
*/
@SuppressWarnings("deprecation")
- public void setSelectedPosition(final int position, @Nullable final ViewHolderTask task) {
+ public void setSelectedPosition(final int position, final @Nullable ViewHolderTask task) {
if (task != null) {
RecyclerView.ViewHolder vh = findViewHolderForPosition(position);
if (vh == null || hasPendingAdapterUpdates()) {
@@ -925,7 +925,7 @@
}
@Override
- public void setLayoutManager(@Nullable RecyclerView.LayoutManager layout) {
+ public void setLayoutManager(RecyclerView.@Nullable LayoutManager layout) {
if (layout == null) {
super.setLayoutManager(null);
if (mLayoutManager != null) {
@@ -958,7 +958,7 @@
* @param view The view to get offsets.
* @param offsets offsets[0] holds offset of X, offsets[1] holds offset of Y.
*/
- public void getViewSelectedOffsets(@NonNull View view, @NonNull int[] offsets) {
+ public void getViewSelectedOffsets(@NonNull View view, int @NonNull [] offsets) {
mLayoutManager.getViewSelectedOffsets(view, offsets);
}
@@ -972,8 +972,7 @@
}
@Override
- @Nullable
- public View focusSearch(int direction) {
+ public @Nullable View focusSearch(int direction) {
if (isFocused()) {
// focusSearch(int) is called when GridView itself is focused.
// Calling focusSearch(view, int) to get next sibling of current selected child.
@@ -1130,8 +1129,7 @@
*
* @return The unhandled key listener.
*/
- @Nullable
- public OnUnhandledKeyListener getOnUnhandledKeyListener() {
+ public @Nullable OnUnhandledKeyListener getOnUnhandledKeyListener() {
return mOnUnhandledKeyListener;
}
@@ -1290,8 +1288,7 @@
*
* @return Custom behavior for SmoothScrollBy(). Null for default behavior.
*/
- @Nullable
- public SmoothScrollByBehavior getSmoothScrollByBehavior() {
+ public @Nullable SmoothScrollByBehavior getSmoothScrollByBehavior() {
return mSmoothScrollByBehavior;
}
diff --git a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/FacetProvider.java b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/FacetProvider.java
index 8f4ed24..c2cc1c3 100644
--- a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/FacetProvider.java
+++ b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/FacetProvider.java
@@ -15,8 +15,8 @@
*/
package androidx.leanback.widget;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* This is the query interface to supply optional features(aka facets) on an object without the need
@@ -70,6 +70,5 @@
* {@link ItemAlignmentFacet}.
* @return Facet implementation for the facetClass or null if feature not implemented.
*/
- @Nullable
- Object getFacet(@NonNull Class<?> facetClass);
+ @Nullable Object getFacet(@NonNull Class<?> facetClass);
}
diff --git a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/FacetProviderAdapter.java b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/FacetProviderAdapter.java
index f967eaa..c75a71b 100644
--- a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/FacetProviderAdapter.java
+++ b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/FacetProviderAdapter.java
@@ -15,7 +15,7 @@
*/
package androidx.leanback.widget;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Optional interface that implemented by
@@ -35,6 +35,5 @@
* @param type type of the item.
* @return Facet provider for the type.
*/
- @Nullable
- FacetProvider getFacetProvider(int type);
+ @Nullable FacetProvider getFacetProvider(int type);
}
diff --git a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/Grid.java b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/Grid.java
index 2b37bd4..28e2f6a 100644
--- a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/Grid.java
+++ b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/Grid.java
@@ -17,11 +17,12 @@
import android.util.SparseIntArray;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.collection.CircularIntArray;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.PrintWriter;
import java.util.Arrays;
@@ -292,7 +293,7 @@
* Finds the largest or smallest row min edge of visible items,
* the row index is returned in indices[0], the item index is returned in indices[1].
*/
- public final int findRowMin(boolean findLarge, @Nullable int[] indices) {
+ public final int findRowMin(boolean findLarge, int @Nullable [] indices) {
return findRowMin(findLarge, mReversedFlow ? mLastVisibleIndex : mFirstVisibleIndex,
indices);
}
@@ -308,7 +309,7 @@
* Finds the largest or smallest row max edge of visible items, the row index is returned in
* indices[0], the item index is returned in indices[1].
*/
- public final int findRowMax(boolean findLarge, @Nullable int[] indices) {
+ public final int findRowMax(boolean findLarge, int @Nullable [] indices) {
return findRowMax(findLarge, mReversedFlow ? mFirstVisibleIndex : mLastVisibleIndex,
indices);
}
@@ -538,7 +539,7 @@
* Queries items adjacent to the viewport (in the direction of da) into the prefetch registry.
*/
public void collectAdjacentPrefetchPositions(int fromLimit, int da,
- @NonNull RecyclerView.LayoutManager.LayoutPrefetchRegistry layoutPrefetchRegistry) {
+ RecyclerView.LayoutManager.@NonNull LayoutPrefetchRegistry layoutPrefetchRegistry) {
}
public abstract void debugPrint(PrintWriter pw);
diff --git a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/GridLayoutManager.java b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/GridLayoutManager.java
index 2feb9f3..ceb8a93 100644
--- a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/GridLayoutManager.java
+++ b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/GridLayoutManager.java
@@ -44,8 +44,6 @@
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.GridView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.collection.CircularIntArray;
import androidx.core.view.ViewCompat;
@@ -56,6 +54,9 @@
import androidx.recyclerview.widget.RecyclerView.Recycler;
import androidx.recyclerview.widget.RecyclerView.State;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
@@ -1093,7 +1094,7 @@
}
@Override
- public boolean checkLayoutParams(@Nullable RecyclerView.LayoutParams lp) {
+ public boolean checkLayoutParams(RecyclerView.@Nullable LayoutParams lp) {
return lp instanceof LayoutParams;
}
@@ -1114,9 +1115,8 @@
/**
* {@inheritDoc}
*/
- @NonNull
@Override
- public RecyclerView.LayoutParams generateDefaultLayoutParams() {
+ public RecyclerView.@NonNull LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
@@ -1124,9 +1124,8 @@
/**
* {@inheritDoc}
*/
- @NonNull
@Override
- public RecyclerView.LayoutParams generateLayoutParams(@NonNull Context context,
+ public RecyclerView.@NonNull LayoutParams generateLayoutParams(@NonNull Context context,
@NonNull AttributeSet attrs) {
return new LayoutParams(context, attrs);
}
@@ -1134,9 +1133,9 @@
/**
* {@inheritDoc}
*/
- @NonNull
@Override
- public RecyclerView.LayoutParams generateLayoutParams(@NonNull ViewGroup.LayoutParams lp) {
+ public RecyclerView.@NonNull LayoutParams generateLayoutParams(
+ ViewGroup.@NonNull LayoutParams lp) {
if (lp instanceof LayoutParams) {
return new LayoutParams((LayoutParams) lp);
} else if (lp instanceof RecyclerView.LayoutParams) {
@@ -2091,7 +2090,7 @@
}
@Override
- public void removeAndRecycleAllViews(@NonNull RecyclerView.Recycler recycler) {
+ public void removeAndRecycleAllViews(RecyclerView.@NonNull Recycler recycler) {
if (DEBUG) Log.v(TAG, "removeAndRecycleAllViews " + getChildCount());
for (int i = getChildCount() - 1; i >= 0; i--) {
removeAndRecycleViewAt(i, recycler);
@@ -2204,8 +2203,8 @@
// Lays out items based on the current scroll position
@Override
- public void onLayoutChildren(@NonNull RecyclerView.Recycler recycler,
- @NonNull RecyclerView.State state) {
+ public void onLayoutChildren(RecyclerView.@NonNull Recycler recycler,
+ RecyclerView.@NonNull State state) {
if (DEBUG) {
Log.v(getTag(), "layoutChildren start numRows " + mNumRows
+ " inPreLayout " + state.isPreLayout()
@@ -2433,7 +2432,7 @@
@Override
public int scrollHorizontallyBy(int dx, @NonNull Recycler recycler,
- @NonNull RecyclerView.State state) {
+ RecyclerView.@NonNull State state) {
if (DEBUG) Log.v(getTag(), "scrollHorizontallyBy " + dx);
if ((mFlag & PF_LAYOUT_ENABLED) == 0 || !hasDoneFirstLayout()) {
return 0;
@@ -2453,7 +2452,7 @@
@Override
public int scrollVerticallyBy(int dy, @NonNull Recycler recycler,
- @NonNull RecyclerView.State state) {
+ RecyclerView.@NonNull State state) {
if (DEBUG) Log.v(getTag(), "scrollVerticallyBy " + dy);
if ((mFlag & PF_LAYOUT_ENABLED) == 0 || !hasDoneFirstLayout()) {
return 0;
@@ -2810,7 +2809,7 @@
}
@Override
- public void startSmoothScroll(@NonNull RecyclerView.SmoothScroller smoothScroller) {
+ public void startSmoothScroll(RecyclerView.@NonNull SmoothScroller smoothScroller) {
skipSmoothScrollerOnStopInternal();
super.startSmoothScroll(smoothScroller);
if (smoothScroller.isRunning() && smoothScroller instanceof GridLinearSmoothScroller) {
@@ -3255,9 +3254,8 @@
return (mFlag & PF_FOCUS_SEARCH_DISABLED) != 0;
}
- @Nullable
@Override
- public View onInterceptFocusSearch(@Nullable View focused, int direction) {
+ public @Nullable View onInterceptFocusSearch(@Nullable View focused, int direction) {
if ((mFlag & PF_FOCUS_SEARCH_DISABLED) != 0) {
return focused;
}
@@ -3636,8 +3634,8 @@
}
@Override
- public void onAdapterChanged(@Nullable RecyclerView.Adapter oldAdapter,
- @Nullable RecyclerView.Adapter newAdapter) {
+ public void onAdapterChanged(RecyclerView.@Nullable Adapter oldAdapter,
+ RecyclerView.@Nullable Adapter newAdapter) {
if (DEBUG) Log.v(getTag(), "onAdapterChanged to " + newAdapter);
if (oldAdapter != null) {
discardLayoutInfo();
@@ -3716,9 +3714,8 @@
}
}
- @NonNull
@Override
- public Parcelable onSaveInstanceState() {
+ public @NonNull Parcelable onSaveInstanceState() {
if (DEBUG) Log.v(getTag(), "onSaveInstanceState getSelection() " + getSelection());
SavedState ss = new SavedState();
// save selected index
@@ -3759,8 +3756,8 @@
}
@Override
- public int getRowCountForAccessibility(@NonNull RecyclerView.Recycler recycler,
- @NonNull RecyclerView.State state) {
+ public int getRowCountForAccessibility(RecyclerView.@NonNull Recycler recycler,
+ RecyclerView.@NonNull State state) {
if (mOrientation == HORIZONTAL && mGrid != null) {
return mGrid.getNumRows();
}
@@ -3768,8 +3765,8 @@
}
@Override
- public int getColumnCountForAccessibility(@NonNull RecyclerView.Recycler recycler,
- @NonNull RecyclerView.State state) {
+ public int getColumnCountForAccessibility(RecyclerView.@NonNull Recycler recycler,
+ RecyclerView.@NonNull State state) {
if (mOrientation == VERTICAL && mGrid != null) {
return mGrid.getNumRows();
}
@@ -3777,8 +3774,8 @@
}
@Override
- public void onInitializeAccessibilityNodeInfoForItem(@NonNull RecyclerView.Recycler recycler,
- @NonNull RecyclerView.State state, @NonNull View host,
+ public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.@NonNull Recycler recycler,
+ RecyclerView.@NonNull State state, @NonNull View host,
@NonNull AccessibilityNodeInfoCompat info) {
ViewGroup.LayoutParams lp = host.getLayoutParams();
if (mGrid == null || !(lp instanceof LayoutParams)) {
diff --git a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/HorizontalGridView.java b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/HorizontalGridView.java
index 6987c74..260247c 100644
--- a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/HorizontalGridView.java
+++ b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/HorizontalGridView.java
@@ -31,11 +31,12 @@
import android.util.TypedValue;
import android.view.View;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* A {@link android.view.ViewGroup} that shows items in a horizontal scrolling list. The items
* come from
diff --git a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/ItemAlignmentFacet.java b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/ItemAlignmentFacet.java
index 9f26cb9..5645608 100644
--- a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/ItemAlignmentFacet.java
+++ b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/ItemAlignmentFacet.java
@@ -17,7 +17,7 @@
import android.view.View;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Optional facet provided by {@link androidx.recyclerview.widget.RecyclerView.Adapter} or
@@ -198,7 +198,7 @@
* Sets definitions of alignment positions.
*/
public void setAlignmentDefs(
- @SuppressWarnings("ArrayReturn") @NonNull ItemAlignmentDef[] defs) {
+ @SuppressWarnings("ArrayReturn") ItemAlignmentDef @NonNull [] defs) {
if (defs == null || defs.length < 1) {
throw new IllegalArgumentException();
}
@@ -209,8 +209,7 @@
* Returns read only definitions of alignment positions.
*/
@SuppressWarnings("ArrayReturn")
- @NonNull
- public ItemAlignmentDef[] getAlignmentDefs() {
+ public ItemAlignmentDef @NonNull [] getAlignmentDefs() {
return mAlignmentDefs;
}
}
diff --git a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/OnChildLaidOutListener.java b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/OnChildLaidOutListener.java
index 8689dc3..d7d44f9 100644
--- a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/OnChildLaidOutListener.java
+++ b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/OnChildLaidOutListener.java
@@ -18,7 +18,7 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Interface for receiving notification when a child of this
diff --git a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/OnChildSelectedListener.java b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/OnChildSelectedListener.java
index 0d9df1e..752ed81 100644
--- a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/OnChildSelectedListener.java
+++ b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/OnChildSelectedListener.java
@@ -18,8 +18,8 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* Interface for receiving notification when a child of this
diff --git a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/OnChildViewHolderSelectedListener.java b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/OnChildViewHolderSelectedListener.java
index 42e3e57..f3ae760 100644
--- a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/OnChildViewHolderSelectedListener.java
+++ b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/OnChildViewHolderSelectedListener.java
@@ -17,11 +17,12 @@
import android.annotation.SuppressLint;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.widget.ItemAlignmentFacet.ItemAlignmentDef;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Interface for receiving notification when a child of this ViewGroup has been selected.
* There are two methods:
@@ -54,7 +55,7 @@
* 0 if there is no ItemAlignmentDef defined for the item.
*/
public void onChildViewHolderSelected(@NonNull RecyclerView parent,
- @Nullable RecyclerView.ViewHolder child,
+ RecyclerView.@Nullable ViewHolder child,
int position, int subposition) {
}
@@ -71,6 +72,6 @@
* 0 if there is no ItemAlignmentDef defined for the item.
*/
public void onChildViewHolderSelectedAndPositioned(@NonNull RecyclerView parent,
- @Nullable RecyclerView.ViewHolder child, int position, int subposition) {
+ RecyclerView.@Nullable ViewHolder child, int position, int subposition) {
}
}
diff --git a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/SingleRow.java b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/SingleRow.java
index 279d407..8385011 100644
--- a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/SingleRow.java
+++ b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/SingleRow.java
@@ -15,10 +15,11 @@
*/
package androidx.leanback.widget;
-import androidx.annotation.NonNull;
import androidx.collection.CircularIntArray;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+
import java.io.PrintWriter;
/**
@@ -136,7 +137,7 @@
@Override
public void collectAdjacentPrefetchPositions(int fromLimit, int da,
- @NonNull RecyclerView.LayoutManager.LayoutPrefetchRegistry layoutPrefetchRegistry) {
+ RecyclerView.LayoutManager.@NonNull LayoutPrefetchRegistry layoutPrefetchRegistry) {
int indexToPrefetch;
int nearestEdge;
if (mReversedFlow ? da > 0 : da < 0) {
diff --git a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/VerticalGridView.java b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/VerticalGridView.java
index 8f69a4e..52ff29d 100644
--- a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/VerticalGridView.java
+++ b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/VerticalGridView.java
@@ -21,11 +21,12 @@
import android.util.AttributeSet;
import android.util.TypedValue;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* A {@link android.view.ViewGroup} that shows items in a vertically scrolling list. The items
* come from the {@link RecyclerView.Adapter} associated with this view.
diff --git a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/ViewHolderTask.java b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/ViewHolderTask.java
index 0c1b7f3..ec3e70d 100644
--- a/leanback/leanback-grid/src/main/java/androidx/leanback/widget/ViewHolderTask.java
+++ b/leanback/leanback-grid/src/main/java/androidx/leanback/widget/ViewHolderTask.java
@@ -15,9 +15,10 @@
*/
package androidx.leanback.widget;
-import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+
/**
* Interface for schedule task on a ViewHolder.
*/
@@ -25,5 +26,5 @@
/**
* Runs the task.
*/
- void run(@NonNull RecyclerView.ViewHolder viewHolder);
+ void run(RecyclerView.@NonNull ViewHolder viewHolder);
}
diff --git a/leanback/leanback-preference/build.gradle b/leanback/leanback-preference/build.gradle
index 6410111..48313f2 100644
--- a/leanback/leanback-preference/build.gradle
+++ b/leanback/leanback-preference/build.gradle
@@ -13,6 +13,7 @@
}
dependencies {
+ api(libs.jspecify)
api("androidx.annotation:annotation:1.8.1")
implementation("androidx.collection:collection:1.4.2")
api("androidx.appcompat:appcompat:1.0.0")
diff --git a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/BaseLeanbackPreferenceFragmentCompat.java b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/BaseLeanbackPreferenceFragmentCompat.java
index 26550ba..036c9e6 100644
--- a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/BaseLeanbackPreferenceFragmentCompat.java
+++ b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/BaseLeanbackPreferenceFragmentCompat.java
@@ -25,7 +25,6 @@
import android.view.LayoutInflater;
import android.view.ViewGroup;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.fragment.app.Fragment;
import androidx.leanback.widget.VerticalGridView;
@@ -33,6 +32,8 @@
import androidx.preference.PreferenceRecyclerViewAccessibilityDelegate;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.Nullable;
+
/**
* This fragment provides a preference fragment with leanback-style behavior, suitable for
* embedding into broader UI elements.
@@ -41,9 +42,8 @@
private Context mThemedContext;
- @Nullable
@Override
- public Context getContext() {
+ public @Nullable Context getContext() {
if (mThemedContext == null && getActivity() != null) {
final TypedValue tv = new TypedValue();
getActivity().getTheme().resolveAttribute(
diff --git a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackEditTextPreferenceDialogFragmentCompat.java b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackEditTextPreferenceDialogFragmentCompat.java
index e216741..27ed9cd 100644
--- a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackEditTextPreferenceDialogFragmentCompat.java
+++ b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackEditTextPreferenceDialogFragmentCompat.java
@@ -31,10 +31,11 @@
import android.widget.EditText;
import android.widget.TextView;
-import androidx.annotation.NonNull;
import androidx.preference.DialogPreference;
import androidx.preference.EditTextPreference;
+import org.jspecify.annotations.NonNull;
+
/**
* Implemented a dialog to input text.
*/
diff --git a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackListPreferenceDialogFragment.java b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackListPreferenceDialogFragment.java
index 73df627..71093a4 100644
--- a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackListPreferenceDialogFragment.java
+++ b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackListPreferenceDialogFragment.java
@@ -27,8 +27,6 @@
import android.widget.Checkable;
import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.collection.ArraySet;
import androidx.leanback.widget.VerticalGridView;
import androidx.preference.DialogPreference;
@@ -36,6 +34,9 @@
import androidx.preference.MultiSelectListPreference;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
diff --git a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackListPreferenceDialogFragmentCompat.java b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackListPreferenceDialogFragmentCompat.java
index 5dbf27d..1fcd204 100644
--- a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackListPreferenceDialogFragmentCompat.java
+++ b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackListPreferenceDialogFragmentCompat.java
@@ -27,7 +27,6 @@
import android.widget.Checkable;
import android.widget.TextView;
-import androidx.annotation.NonNull;
import androidx.collection.ArraySet;
import androidx.leanback.widget.VerticalGridView;
import androidx.preference.DialogPreference;
@@ -35,6 +34,8 @@
import androidx.preference.MultiSelectListPreference;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
diff --git a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackPreferenceFragmentCompat.java b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackPreferenceFragmentCompat.java
index ae5ce2b..14a4a96 100644
--- a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackPreferenceFragmentCompat.java
+++ b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackPreferenceFragmentCompat.java
@@ -22,7 +22,7 @@
import android.view.ViewGroup;
import android.widget.TextView;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* This fragment provides a fully decorated leanback-style preference fragment, including a
diff --git a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackSettingsFragment.java b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackSettingsFragment.java
index 4f77893..5874e33 100644
--- a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackSettingsFragment.java
+++ b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackSettingsFragment.java
@@ -28,8 +28,6 @@
import android.view.ViewGroup;
import android.widget.Space;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.preference.ListPreference;
import androidx.preference.MultiSelectListPreference;
@@ -37,6 +35,9 @@
import androidx.preference.PreferenceFragment;
import androidx.preference.PreferenceScreen;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* This fragment provides a container for displaying a {@link LeanbackPreferenceFragment}
*
diff --git a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackSettingsFragmentCompat.java b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackSettingsFragmentCompat.java
index c1c1d69..9bb0a6c 100644
--- a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackSettingsFragmentCompat.java
+++ b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackSettingsFragmentCompat.java
@@ -22,7 +22,6 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.EditTextPreference;
@@ -33,6 +32,8 @@
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
+import org.jspecify.annotations.NonNull;
+
/**
* This fragment provides a container for displaying a {@link LeanbackPreferenceFragmentCompat}
*
diff --git a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackSettingsRootView.java b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackSettingsRootView.java
index ddfbf31..e6582be 100644
--- a/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackSettingsRootView.java
+++ b/leanback/leanback-preference/src/main/java/androidx/leanback/preference/LeanbackSettingsRootView.java
@@ -23,9 +23,10 @@
import android.view.KeyEvent;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
/**
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
diff --git a/leanback/leanback-tab/build.gradle b/leanback/leanback-tab/build.gradle
index c490406..9ea6215 100644
--- a/leanback/leanback-tab/build.gradle
+++ b/leanback/leanback-tab/build.gradle
@@ -13,6 +13,7 @@
}
dependencies {
+ api(libs.jspecify)
api("androidx.annotation:annotation:1.8.1")
api("com.google.android.material:material:1.0.0")
api("androidx.viewpager:viewpager:1.0.0")
diff --git a/leanback/leanback-tab/src/androidTest/java/androidx/leanback/tab/app/TabLayoutTestActivity.java b/leanback/leanback-tab/src/androidTest/java/androidx/leanback/tab/app/TabLayoutTestActivity.java
index a944254..24a497d 100644
--- a/leanback/leanback-tab/src/androidTest/java/androidx/leanback/tab/app/TabLayoutTestActivity.java
+++ b/leanback/leanback-tab/src/androidTest/java/androidx/leanback/tab/app/TabLayoutTestActivity.java
@@ -18,7 +18,6 @@
import android.os.Bundle;
-
import androidx.fragment.app.FragmentActivity;
import androidx.leanback.tab.LeanbackViewPager;
import androidx.leanback.tab.test.R;
diff --git a/leanback/leanback-tab/src/androidTest/java/androidx/leanback/tab/app/TestFragment.java b/leanback/leanback-tab/src/androidTest/java/androidx/leanback/tab/app/TestFragment.java
index 36e9f4b..6dfba99 100644
--- a/leanback/leanback-tab/src/androidTest/java/androidx/leanback/tab/app/TestFragment.java
+++ b/leanback/leanback-tab/src/androidTest/java/androidx/leanback/tab/app/TestFragment.java
@@ -20,16 +20,16 @@
import android.view.LayoutInflater;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.leanback.tab.test.R;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
public class TestFragment extends Fragment {
- @Nullable
@Override
- public android.view.View onCreateView(@NonNull LayoutInflater inflater,
+ public android.view.@Nullable View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
return inflater.inflate(R.layout.test_fragment, container, false);
diff --git a/leanback/leanback-tab/src/main/java/androidx/leanback/tab/LeanbackTabLayout.java b/leanback/leanback-tab/src/main/java/androidx/leanback/tab/LeanbackTabLayout.java
index 089c1cd..a9e3eaa 100644
--- a/leanback/leanback-tab/src/main/java/androidx/leanback/tab/LeanbackTabLayout.java
+++ b/leanback/leanback-tab/src/main/java/androidx/leanback/tab/LeanbackTabLayout.java
@@ -23,12 +23,13 @@
import android.view.View;
import android.widget.LinearLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
/**
diff --git a/leanback/leanback-tab/src/main/java/androidx/leanback/tab/LeanbackViewPager.java b/leanback/leanback-tab/src/main/java/androidx/leanback/tab/LeanbackViewPager.java
index bf85ed7..4b0fdc0 100644
--- a/leanback/leanback-tab/src/main/java/androidx/leanback/tab/LeanbackViewPager.java
+++ b/leanback/leanback-tab/src/main/java/androidx/leanback/tab/LeanbackViewPager.java
@@ -21,10 +21,11 @@
import android.view.KeyEvent;
import android.view.MotionEvent;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.viewpager.widget.ViewPager;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* A viewpager with touch and key event handling disabled by default.
*
diff --git a/leanback/leanback/build.gradle b/leanback/leanback/build.gradle
index 2195bb6..9804e69 100644
--- a/leanback/leanback/build.gradle
+++ b/leanback/leanback/build.gradle
@@ -13,6 +13,7 @@
}
dependencies {
+ api(libs.jspecify)
api("androidx.annotation:annotation:1.8.1")
api("androidx.interpolator:interpolator:1.0.0")
api("androidx.core:core:1.1.0")
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseFragmentTest.java
index 2a1b7db..a785fcf 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseFragmentTest.java
@@ -23,6 +23,7 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import android.app.Fragment;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
@@ -38,9 +39,6 @@
import android.widget.EditText;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import android.app.Fragment;
import androidx.leanback.test.R;
import androidx.leanback.testutils.LeakDetector;
import androidx.leanback.testutils.PollingCheck;
@@ -58,6 +56,8 @@
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
@@ -209,10 +209,9 @@
BrowseFragment.MainFragmentAdapter<MyFragment> mMainFragmentAdapter =
new BrowseFragment.MainFragmentAdapter<>(this);
- @Nullable
@Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return new FrameLayout(container.getContext());
}
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseFragmentTestActivity.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseFragmentTestActivity.java
index c12af8e..dff2a16 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseFragmentTestActivity.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseFragmentTestActivity.java
@@ -18,11 +18,11 @@
*/
package androidx.leanback.app;
+import android.app.Activity;
+import android.app.FragmentTransaction;
import android.content.Intent;
import android.os.Bundle;
-import android.app.Activity;
-import android.app.FragmentTransaction;
import androidx.leanback.test.R;
public class BrowseFragmentTestActivity extends Activity {
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseSupportFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseSupportFragmentTest.java
index 07a16db..7cd4214 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseSupportFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseSupportFragmentTest.java
@@ -35,8 +35,6 @@
import android.widget.EditText;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.leanback.test.R;
import androidx.leanback.testutils.LeakDetector;
@@ -55,6 +53,8 @@
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
@@ -206,10 +206,9 @@
BrowseSupportFragment.MainFragmentAdapter<MyFragment> mMainFragmentAdapter =
new BrowseSupportFragment.MainFragmentAdapter<>(this);
- @Nullable
@Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return new FrameLayout(container.getContext());
}
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsFragmentTest.java
index 7893821..ed95ca0 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsFragmentTest.java
@@ -26,6 +26,7 @@
import static org.junit.Assert.assertTrue;
import android.animation.PropertyValuesHolder;
+import android.app.Fragment;
import android.app.UiAutomation;
import android.content.Context;
import android.content.Intent;
@@ -45,8 +46,6 @@
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
-import androidx.annotation.NonNull;
-import android.app.Fragment;
import androidx.leanback.graphics.FitWidthBitmapDrawable;
import androidx.leanback.media.MediaPlayerGlue;
import androidx.leanback.media.PlaybackGlueHost;
@@ -66,6 +65,7 @@
import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
+import org.jspecify.annotations.NonNull;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsSupportFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsSupportFragmentTest.java
index 1db3f5e..697ffd2 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsSupportFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsSupportFragmentTest.java
@@ -42,7 +42,6 @@
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
-import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.leanback.graphics.FitWidthBitmapDrawable;
import androidx.leanback.media.MediaPlayerGlue;
@@ -63,6 +62,7 @@
import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
+import org.jspecify.annotations.NonNull;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsTestFragment.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsTestFragment.java
index 13e5bcd..3e20de0 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsTestFragment.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsTestFragment.java
@@ -23,8 +23,6 @@
import android.os.Handler;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.test.R;
import androidx.leanback.widget.AbstractDetailsDescriptionPresenter;
import androidx.leanback.widget.Action;
@@ -39,6 +37,9 @@
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.SparseArrayObjectAdapter;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Base class provides overview row and some related rows.
*/
@@ -47,9 +48,8 @@
private ArrayObjectAdapter mRowsAdapter;
private PhotoItem mPhotoItem;
private final Presenter mCardPresenter = new Presenter() {
- @NonNull
@Override
- public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
+ public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
ImageCardView cardView = new ImageCardView(getActivity());
cardView.setFocusable(true);
cardView.setFocusableInTouchMode(true);
@@ -94,7 +94,7 @@
new FullWidthDetailsOverviewRowPresenter(new AbstractDetailsDescriptionPresenter() {
@Override
protected void onBindDescription(
- @NonNull AbstractDetailsDescriptionPresenter.ViewHolder vh,
+ AbstractDetailsDescriptionPresenter.@NonNull ViewHolder vh,
@Nullable Object item
) {
vh.getTitle().setText("Funny Movie");
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsTestSupportFragment.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsTestSupportFragment.java
index 7810eea..9be2075 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsTestSupportFragment.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/DetailsTestSupportFragment.java
@@ -20,8 +20,6 @@
import android.os.Handler;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.test.R;
import androidx.leanback.widget.AbstractDetailsDescriptionPresenter;
import androidx.leanback.widget.Action;
@@ -36,6 +34,9 @@
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.SparseArrayObjectAdapter;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Base class provides overview row and some related rows.
*/
@@ -44,9 +45,8 @@
private ArrayObjectAdapter mRowsAdapter;
private PhotoItem mPhotoItem;
private final Presenter mCardPresenter = new Presenter() {
- @NonNull
@Override
- public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
+ public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
ImageCardView cardView = new ImageCardView(getActivity());
cardView.setFocusable(true);
cardView.setFocusableInTouchMode(true);
@@ -91,7 +91,7 @@
new FullWidthDetailsOverviewRowPresenter(new AbstractDetailsDescriptionPresenter() {
@Override
protected void onBindDescription(
- @NonNull AbstractDetailsDescriptionPresenter.ViewHolder vh,
+ AbstractDetailsDescriptionPresenter.@NonNull ViewHolder vh,
@Nullable Object item
) {
vh.getTitle().setText("Funny Movie");
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepFragmentTestActivity.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepFragmentTestActivity.java
index e4c1171..1a28995 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepFragmentTestActivity.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepFragmentTestActivity.java
@@ -17,11 +17,10 @@
package androidx.leanback.app;
+import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
-import android.app.Activity;
-
public class GuidedStepFragmentTestActivity extends Activity {
/**
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepTestFragment.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepTestFragment.java
index a8b9010..b506c9f 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepTestFragment.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepTestFragment.java
@@ -17,17 +17,18 @@
package androidx.leanback.app;
+import android.app.Activity;
+import android.app.FragmentManager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import android.app.Activity;
-import android.app.FragmentManager;
import androidx.leanback.widget.GuidanceStylist.Guidance;
import androidx.leanback.widget.GuidedAction;
+import org.jspecify.annotations.NonNull;
+
import java.util.HashMap;
import java.util.List;
@@ -152,9 +153,8 @@
mProvider.onSaveInstanceState(outState);
}
- @NonNull
@Override
- public Guidance onCreateGuidance(@NonNull Bundle savedInstanceState) {
+ public @NonNull Guidance onCreateGuidance(@NonNull Bundle savedInstanceState) {
Guidance g = mProvider.onCreateGuidance(savedInstanceState);
if (g == null) {
g = new Guidance("", "", "", null);
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepTestSupportFragment.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepTestSupportFragment.java
index f5175c7..418c585 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepTestSupportFragment.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepTestSupportFragment.java
@@ -19,12 +19,13 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.leanback.widget.GuidanceStylist.Guidance;
import androidx.leanback.widget.GuidedAction;
+import org.jspecify.annotations.NonNull;
+
import java.util.HashMap;
import java.util.List;
@@ -149,9 +150,8 @@
mProvider.onSaveInstanceState(outState);
}
- @NonNull
@Override
- public Guidance onCreateGuidance(@NonNull Bundle savedInstanceState) {
+ public @NonNull Guidance onCreateGuidance(@NonNull Bundle savedInstanceState) {
Guidance g = mProvider.onCreateGuidance(savedInstanceState);
if (g == null) {
g = new Guidance("", "", "", null);
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/ListRowDataAdapterTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/ListRowDataAdapterTest.java
index d0a203b6..78d1a2e 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/ListRowDataAdapterTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/ListRowDataAdapterTest.java
@@ -29,7 +29,6 @@
import androidx.leanback.widget.SectionRow;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Test;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/PlaybackFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/PlaybackFragmentTest.java
index 37b9400..1762c1e 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/PlaybackFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/PlaybackFragmentTest.java
@@ -33,8 +33,6 @@
import android.view.KeyEvent;
import android.view.View;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.media.PlaybackControlGlue;
import androidx.leanback.media.PlaybackGlue;
import androidx.leanback.media.PlaybackGlueHost;
@@ -56,6 +54,8 @@
import androidx.test.filters.LargeTest;
import androidx.test.filters.Suppress;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/PlaybackSupportFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/PlaybackSupportFragmentTest.java
index 9e930c4..20be774 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/PlaybackSupportFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/PlaybackSupportFragmentTest.java
@@ -30,8 +30,6 @@
import android.view.KeyEvent;
import android.view.View;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.media.PlaybackControlGlue;
import androidx.leanback.media.PlaybackGlue;
import androidx.leanback.media.PlaybackGlueHost;
@@ -53,6 +51,8 @@
import androidx.test.filters.LargeTest;
import androidx.test.filters.Suppress;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/RowsFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/RowsFragmentTest.java
index 0a84e01..894ce4a 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/RowsFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/RowsFragmentTest.java
@@ -26,6 +26,7 @@
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
+import android.app.Fragment;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
@@ -40,8 +41,6 @@
import android.widget.EditText;
import android.widget.TextView;
-import androidx.annotation.Nullable;
-import android.app.Fragment;
import androidx.leanback.test.R;
import androidx.leanback.testutils.LeakDetector;
import androidx.leanback.testutils.PollingCheck;
@@ -65,6 +64,7 @@
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.testutils.AnimationTest;
+import org.jspecify.annotations.Nullable;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/RowsSupportFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/RowsSupportFragmentTest.java
index 631c3de..67b4e68 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/RowsSupportFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/RowsSupportFragmentTest.java
@@ -37,7 +37,6 @@
import android.widget.EditText;
import android.widget.TextView;
-import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.leanback.test.R;
import androidx.leanback.testutils.LeakDetector;
@@ -62,6 +61,7 @@
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.testutils.AnimationTest;
+import org.jspecify.annotations.Nullable;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchFragmentTest.java
index d671155..ddd7174 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchFragmentTest.java
@@ -20,6 +20,7 @@
import static org.junit.Assert.assertTrue;
+import android.app.Fragment;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
@@ -31,7 +32,6 @@
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
-import android.app.Fragment;
import androidx.leanback.test.R;
import androidx.leanback.testutils.LeakDetector;
import androidx.leanback.testutils.PollingCheck;
@@ -47,11 +47,12 @@
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.testutils.AnimationTest;
-import java.util.Objects;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Objects;
+
@LargeTest
@AnimationTest
@RunWith(AndroidJUnit4.class)
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchSupportFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchSupportFragmentTest.java
index 8be9af1..41e7aea 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchSupportFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchSupportFragmentTest.java
@@ -44,11 +44,12 @@
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.testutils.AnimationTest;
-import java.util.Objects;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Objects;
+
@LargeTest
@AnimationTest
@RunWith(AndroidJUnit4.class)
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/SingleFragmentTestActivity.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SingleFragmentTestActivity.java
index 2f3d994..2a3d328 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/SingleFragmentTestActivity.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SingleFragmentTestActivity.java
@@ -18,13 +18,13 @@
*/
package androidx.leanback.app;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
-import android.app.Fragment;
-import android.app.Activity;
-import android.app.FragmentTransaction;
import androidx.leanback.test.R;
public class SingleFragmentTestActivity extends Activity {
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/StringPresenter.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/StringPresenter.java
index 89c8c72..8166e71 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/StringPresenter.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/StringPresenter.java
@@ -18,17 +18,17 @@
import android.view.ViewGroup;
import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.widget.Presenter;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
public class StringPresenter extends Presenter {
private static final boolean DEBUG = false;
private static final String TAG = "StringPresenter";
- @NonNull
@Override
- public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
+ public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
if (DEBUG) Log.d(TAG, "onCreateViewHolder");
TextView tv = new TextView(parent.getContext());
tv.setFocusable(true);
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridFragmentTest.java
index 632ab55..391af4e 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridFragmentTest.java
@@ -19,6 +19,7 @@
package androidx.leanback.app;
+import android.app.Fragment;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
@@ -28,7 +29,6 @@
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
-import android.app.Fragment;
import androidx.leanback.test.R;
import androidx.leanback.testutils.LeakDetector;
import androidx.leanback.testutils.PollingCheck;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/VideoFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/VideoFragmentTest.java
index 36d4d2d..530df20 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/VideoFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/VideoFragmentTest.java
@@ -32,7 +32,6 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
import androidx.leanback.media.MediaPlayerGlue;
import androidx.leanback.media.PlaybackGlue;
import androidx.leanback.media.PlaybackGlueHost;
@@ -42,6 +41,7 @@
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import org.jspecify.annotations.NonNull;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/VideoSupportFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/VideoSupportFragmentTest.java
index e67fcb6..0985a5f 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/VideoSupportFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/VideoSupportFragmentTest.java
@@ -29,7 +29,6 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
import androidx.leanback.media.MediaPlayerGlue;
import androidx.leanback.media.PlaybackGlue;
import androidx.leanback.media.PlaybackGlueHost;
@@ -39,6 +38,7 @@
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import org.jspecify.annotations.NonNull;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/wizard/GuidedStepAttributesTestFragment.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/wizard/GuidedStepAttributesTestFragment.java
index 292b542..96a6ed4 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/wizard/GuidedStepAttributesTestFragment.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/wizard/GuidedStepAttributesTestFragment.java
@@ -16,11 +16,12 @@
import android.os.Bundle;
-import androidx.annotation.NonNull;
import androidx.leanback.app.GuidedStepFragment;
import androidx.leanback.widget.GuidanceStylist;
import androidx.leanback.widget.GuidedAction;
+import org.jspecify.annotations.NonNull;
+
import java.util.HashMap;
import java.util.List;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/media/MediaControllerAdapterTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/media/MediaControllerAdapterTest.java
index 16b52b0..407f854 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/media/MediaControllerAdapterTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/media/MediaControllerAdapterTest.java
@@ -34,14 +34,14 @@
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.widget.PlaybackControlsRow;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/media/MediaPlayerGlueTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/media/MediaPlayerGlueTest.java
index 7f6fa9e..9b51de0 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/media/MediaPlayerGlueTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/media/MediaPlayerGlueTest.java
@@ -26,13 +26,13 @@
import android.os.Build;
import android.os.SystemClock;
-import androidx.annotation.NonNull;
import androidx.leanback.testutils.PollingCheck;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
+import org.jspecify.annotations.NonNull;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/media/PlaybackGlueTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/media/PlaybackGlueTest.java
index d0a7858..d548aa10 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/media/PlaybackGlueTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/media/PlaybackGlueTest.java
@@ -27,11 +27,11 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import org.jspecify.annotations.NonNull;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridActivity.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridActivity.java
index cfc1991..d56c1fe 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridActivity.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridActivity.java
@@ -26,12 +26,13 @@
import android.view.ViewGroup;
import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.test.R;
import androidx.recyclerview.widget.ConcatAdapter;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java
index f182de2..682c34f 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java
@@ -49,8 +49,6 @@
import android.view.animation.Interpolator;
import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.leanback.test.R;
import androidx.leanback.testutils.PollingCheck;
@@ -65,6 +63,8 @@
import androidx.testutils.AnimationActivityTestRule;
import androidx.testutils.AnimationTest;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.After;
import org.junit.Ignore;
import org.junit.Rule;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ItemBridgeAdapterTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ItemBridgeAdapterTest.java
index 9e0517b..c842a84 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ItemBridgeAdapterTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ItemBridgeAdapterTest.java
@@ -27,14 +27,14 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -51,9 +51,8 @@
public static class BasePresenter extends Presenter {
- @NonNull
@Override
- public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ public @NonNull ViewHolder onCreateViewHolder(ViewGroup parent) {
View view = new View(parent.getContext());
view.setLayoutParams(new ViewGroup.LayoutParams(sViewWidth, sViewHeight));
return new ViewHolder(view);
@@ -157,9 +156,8 @@
}
}
- @NonNull
@Override
- public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
+ public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
View view = mViews.get(mViews.size() - 1);
mViews.remove(mViews.size() - 1);
view.setLayoutParams(new ViewGroup.LayoutParams(sViewWidth, sViewHeight));
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ListRowPresenterTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ListRowPresenterTest.java
index 496b8aab..8d9c64d 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ListRowPresenterTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ListRowPresenterTest.java
@@ -29,14 +29,14 @@
import android.view.ViewGroup;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.leanback.R;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -62,9 +62,8 @@
mHeight = height;
}
- @NonNull
@Override
- public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ public @NonNull ViewHolder onCreateViewHolder(ViewGroup parent) {
View view = new View(parent.getContext());
view.setFocusable(true);
view.setId(R.id.lb_action_button);
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ObjectAdapterTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ObjectAdapterTest.java
index 7598697..b552b70 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ObjectAdapterTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ObjectAdapterTest.java
@@ -31,14 +31,14 @@
import android.view.ViewGroup;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -175,9 +175,8 @@
return oldItem.equals(newItem);
}
- @Nullable
@Override
- public Object getChangePayload(@NonNull AdapterItem oldItem,
+ public @Nullable Object getChangePayload(@NonNull AdapterItem oldItem,
@NonNull AdapterItem newItem) {
Bundle diff = new Bundle();
if (oldItem.getId() != newItem.getId()) {
@@ -222,9 +221,8 @@
mHeight = height;
}
- @NonNull
@Override
- public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ public @NonNull ViewHolder onCreateViewHolder(ViewGroup parent) {
View view = new View(parent.getContext());
view.setFocusable(true);
view.setId(R.id.lb_action_button);
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/PlaybackTransportRowPresenterTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/PlaybackTransportRowPresenterTest.java
index 9cb7606..f640029 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/PlaybackTransportRowPresenterTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/PlaybackTransportRowPresenterTest.java
@@ -36,7 +36,6 @@
import android.view.View;
import android.view.ViewParent;
-import androidx.annotation.NonNull;
import androidx.leanback.media.PlaybackTransportControlGlue;
import androidx.leanback.media.PlayerAdapter;
import androidx.leanback.widget.PlaybackSeekDataProvider.ResultCallback;
@@ -44,6 +43,7 @@
import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
+import org.jspecify.annotations.NonNull;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/PresenterTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/PresenterTest.java
index 554eaef..716acc4 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/PresenterTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/PresenterTest.java
@@ -27,14 +27,14 @@
import android.view.ViewGroup.LayoutParams;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
import androidx.leanback.app.HeadersFragment;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/TestPresenter.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/TestPresenter.java
index 4b30e10..8543178 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/TestPresenter.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/TestPresenter.java
@@ -20,8 +20,8 @@
import android.view.ViewGroup;
import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
@@ -116,9 +116,8 @@
return newList;
}
- @NonNull
@Override
- public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
+ public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
TextView tv = new TextView(parent.getContext());
tv.setFocusable(true);
tv.setFocusableInTouchMode(true);
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/picker/DatePickerActivity.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/picker/DatePickerActivity.java
index f7f49d5..89976ca 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/picker/DatePickerActivity.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/picker/DatePickerActivity.java
@@ -21,7 +21,6 @@
import androidx.leanback.test.R;
-
public class DatePickerActivity extends Activity {
public static final String EXTRA_LAYOUT_RESOURCE_ID = "layoutResourceId";
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/picker/PinPickerTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/picker/PinPickerTest.java
index f22cfd4..8123147 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/picker/PinPickerTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/picker/PinPickerTest.java
@@ -25,7 +25,6 @@
import android.view.KeyEvent;
import android.view.View;
-import androidx.annotation.NonNull;
import androidx.leanback.test.R;
import androidx.leanback.testutils.PollingCheck;
import androidx.recyclerview.widget.RecyclerView;
@@ -33,6 +32,7 @@
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
+import org.jspecify.annotations.NonNull;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/picker/TimePickerActivity.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/picker/TimePickerActivity.java
index da5eb0d..fa136f2 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/picker/TimePickerActivity.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/picker/TimePickerActivity.java
@@ -21,7 +21,6 @@
import androidx.leanback.test.R;
-
public class TimePickerActivity extends Activity {
public static final String EXTRA_LAYOUT_RESOURCE_ID = "layoutResourceId";
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BackgroundManager.java b/leanback/leanback/src/main/java/androidx/leanback/app/BackgroundManager.java
index 3fc4c83..0a63418 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BackgroundManager.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BackgroundManager.java
@@ -37,12 +37,13 @@
import android.view.animation.Interpolator;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.interpolator.view.animation.FastOutLinearInInterpolator;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+
import java.lang.ref.WeakReference;
/**
@@ -209,9 +210,8 @@
return mState;
}
- @NonNull
@Override
- public Drawable mutate() {
+ public @NonNull Drawable mutate() {
if (!mMutated) {
mMutated = true;
mState = new ConstantState(mState);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BaseFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/BaseFragment.java
index 7b50326..0aadb6f 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BaseFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BaseFragment.java
@@ -21,8 +21,6 @@
import android.view.View;
import android.view.ViewTreeObserver;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.transition.TransitionHelper;
import androidx.leanback.transition.TransitionListener;
import androidx.leanback.util.StateMachine;
@@ -30,6 +28,9 @@
import androidx.leanback.util.StateMachine.Event;
import androidx.leanback.util.StateMachine.State;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Base class for leanback Fragments. This class is not intended to be subclassed by apps.
* @deprecated use {@link BaseSupportFragment}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BaseRowFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/BaseRowFragment.java
index 873c50a..584bd2c 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BaseRowFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BaseRowFragment.java
@@ -16,14 +16,12 @@
*/
package androidx.leanback.app;
+import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import android.app.Fragment;
import androidx.leanback.widget.ItemBridgeAdapter;
import androidx.leanback.widget.ListRow;
import androidx.leanback.widget.ObjectAdapter;
@@ -33,6 +31,9 @@
import androidx.leanback.widget.VerticalGridView;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* An internal base class for a fragment containing a list of rows.
* @deprecated use {@link BaseRowSupportFragment}
@@ -67,8 +68,8 @@
}
@Override
- @Nullable
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+ public @Nullable View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ Bundle savedInstanceState) {
View view = inflater.inflate(getLayoutResourceId(), container, false);
mVerticalGridView = findGridViewFromRoot(view);
if (mPendingTransitionPrepare) {
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BaseRowSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/BaseRowSupportFragment.java
index de9ab85..a76748d 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BaseRowSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BaseRowSupportFragment.java
@@ -18,8 +18,6 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.leanback.widget.ItemBridgeAdapter;
import androidx.leanback.widget.ListRow;
@@ -30,6 +28,9 @@
import androidx.leanback.widget.VerticalGridView;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* An internal base class for a fragment containing a list of rows.
*/
@@ -62,9 +63,8 @@
}
@Override
- @Nullable
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(getLayoutResourceId(), container, false);
mVerticalGridView = findGridViewFromRoot(view);
if (mPendingTransitionPrepare) {
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BaseSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/BaseSupportFragment.java
index 1798d4d2..c05382c 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BaseSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BaseSupportFragment.java
@@ -18,8 +18,6 @@
import android.view.View;
import android.view.ViewTreeObserver;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.transition.TransitionHelper;
import androidx.leanback.transition.TransitionListener;
import androidx.leanback.util.StateMachine;
@@ -27,6 +25,9 @@
import androidx.leanback.util.StateMachine.Event;
import androidx.leanback.util.StateMachine.State;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Base class for leanback Fragments. This class is not intended to be subclassed by apps.
*/
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BrandedFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/BrandedFragment.java
index 9808ba9f..da43a4d 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BrandedFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BrandedFragment.java
@@ -16,6 +16,7 @@
*/
package androidx.leanback.app;
+import android.app.Fragment;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.TypedValue;
@@ -23,14 +24,14 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import android.app.Fragment;
import androidx.leanback.R;
import androidx.leanback.widget.SearchOrbView;
import androidx.leanback.widget.TitleHelper;
import androidx.leanback.widget.TitleViewAdapter;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Fragment class for managing search and branding using a view that implements
* {@link TitleViewAdapter.Provider}.
@@ -67,9 +68,8 @@
* @return Title view which must have a descendant with id browse_title_group that implements
* {@link TitleViewAdapter.Provider}, or null for no title view.
*/
- @NonNull
- public View onInflateTitleView(@NonNull LayoutInflater inflater, @Nullable ViewGroup parent,
- @Nullable Bundle savedInstanceState) {
+ public @NonNull View onInflateTitleView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup parent, @Nullable Bundle savedInstanceState) {
TypedValue typedValue = new TypedValue();
boolean found = parent != null && parent.getContext().getTheme().resolveAttribute(
R.attr.browseTitleViewLayout, typedValue, true);
@@ -126,8 +126,7 @@
* Returns the view that implements {@link TitleViewAdapter.Provider}.
* @return The view that implements {@link TitleViewAdapter.Provider}.
*/
- @Nullable
- public View getTitleView() {
+ public @Nullable View getTitleView() {
return mTitleView;
}
@@ -135,8 +134,7 @@
* Returns the {@link TitleViewAdapter} implemented by title view.
* @return The {@link TitleViewAdapter} implemented by title view.
*/
- @Nullable
- public TitleViewAdapter getTitleViewAdapter() {
+ public @Nullable TitleViewAdapter getTitleViewAdapter() {
return mTitleViewAdapter;
}
@@ -221,8 +219,7 @@
* Returns the badge drawable used in the fragment title.
* @return The badge drawable used in the fragment title.
*/
- @Nullable
- public Drawable getBadgeDrawable() {
+ public @Nullable Drawable getBadgeDrawable() {
return mBadgeDrawable;
}
@@ -242,8 +239,7 @@
* Returns the title text for the fragment.
* @return Title text for the fragment.
*/
- @Nullable
- public CharSequence getTitle() {
+ public @Nullable CharSequence getTitle() {
return mTitle;
}
@@ -259,7 +255,7 @@
*
* @param listener The listener to call when the search element is clicked.
*/
- public void setOnSearchClickedListener(@Nullable View.OnClickListener listener) {
+ public void setOnSearchClickedListener(View.@Nullable OnClickListener listener) {
mExternalOnSearchClickedListener = listener;
if (mTitleViewAdapter != null) {
mTitleViewAdapter.setOnSearchClickedListener(listener);
@@ -272,7 +268,7 @@
*
* @param colors Colors used to draw search affordance.
*/
- public void setSearchAffordanceColors(@NonNull SearchOrbView.Colors colors) {
+ public void setSearchAffordanceColors(SearchOrbView.@NonNull Colors colors) {
mSearchAffordanceColors = colors;
mSearchAffordanceColorSet = true;
if (mTitleViewAdapter != null) {
@@ -284,8 +280,7 @@
* Returns the {@link androidx.leanback.widget.SearchOrbView.Colors}
* used to draw the search affordance.
*/
- @Nullable
- public SearchOrbView.Colors getSearchAffordanceColors() {
+ public SearchOrbView.@Nullable Colors getSearchAffordanceColors() {
if (mSearchAffordanceColorSet) {
return mSearchAffordanceColors;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BrandedSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/BrandedSupportFragment.java
index 7222916..58aba27 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BrandedSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BrandedSupportFragment.java
@@ -20,14 +20,15 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.leanback.R;
import androidx.leanback.widget.SearchOrbView;
import androidx.leanback.widget.TitleHelper;
import androidx.leanback.widget.TitleViewAdapter;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Fragment class for managing search and branding using a view that implements
* {@link TitleViewAdapter.Provider}.
@@ -62,9 +63,8 @@
* @return Title view which must have a descendant with id browse_title_group that implements
* {@link TitleViewAdapter.Provider}, or null for no title view.
*/
- @NonNull
- public View onInflateTitleView(@NonNull LayoutInflater inflater, @Nullable ViewGroup parent,
- @Nullable Bundle savedInstanceState) {
+ public @NonNull View onInflateTitleView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup parent, @Nullable Bundle savedInstanceState) {
TypedValue typedValue = new TypedValue();
boolean found = parent != null && parent.getContext().getTheme().resolveAttribute(
R.attr.browseTitleViewLayout, typedValue, true);
@@ -121,8 +121,7 @@
* Returns the view that implements {@link TitleViewAdapter.Provider}.
* @return The view that implements {@link TitleViewAdapter.Provider}.
*/
- @Nullable
- public View getTitleView() {
+ public @Nullable View getTitleView() {
return mTitleView;
}
@@ -130,8 +129,7 @@
* Returns the {@link TitleViewAdapter} implemented by title view.
* @return The {@link TitleViewAdapter} implemented by title view.
*/
- @Nullable
- public TitleViewAdapter getTitleViewAdapter() {
+ public @Nullable TitleViewAdapter getTitleViewAdapter() {
return mTitleViewAdapter;
}
@@ -216,8 +214,7 @@
* Returns the badge drawable used in the fragment title.
* @return The badge drawable used in the fragment title.
*/
- @Nullable
- public Drawable getBadgeDrawable() {
+ public @Nullable Drawable getBadgeDrawable() {
return mBadgeDrawable;
}
@@ -237,8 +234,7 @@
* Returns the title text for the fragment.
* @return Title text for the fragment.
*/
- @Nullable
- public CharSequence getTitle() {
+ public @Nullable CharSequence getTitle() {
return mTitle;
}
@@ -254,7 +250,7 @@
*
* @param listener The listener to call when the search element is clicked.
*/
- public void setOnSearchClickedListener(@Nullable View.OnClickListener listener) {
+ public void setOnSearchClickedListener(View.@Nullable OnClickListener listener) {
mExternalOnSearchClickedListener = listener;
if (mTitleViewAdapter != null) {
mTitleViewAdapter.setOnSearchClickedListener(listener);
@@ -267,7 +263,7 @@
*
* @param colors Colors used to draw search affordance.
*/
- public void setSearchAffordanceColors(@NonNull SearchOrbView.Colors colors) {
+ public void setSearchAffordanceColors(SearchOrbView.@NonNull Colors colors) {
mSearchAffordanceColors = colors;
mSearchAffordanceColorSet = true;
if (mTitleViewAdapter != null) {
@@ -279,8 +275,7 @@
* Returns the {@link androidx.leanback.widget.SearchOrbView.Colors}
* used to draw the search affordance.
*/
- @Nullable
- public SearchOrbView.Colors getSearchAffordanceColors() {
+ public SearchOrbView.@Nullable Colors getSearchAffordanceColors() {
if (mSearchAffordanceColorSet) {
return mSearchAffordanceColors;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BrowseFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/BrowseFragment.java
index 6901095..ee6b4a6 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BrowseFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BrowseFragment.java
@@ -18,6 +18,11 @@
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentManager.BackStackEntry;
+import android.app.FragmentTransaction;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
@@ -31,14 +36,6 @@
import android.view.ViewTreeObserver;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.view.ViewCompat;
-import android.app.Fragment;
-import android.app.Activity;
-import android.app.FragmentManager;
-import android.app.FragmentManager.BackStackEntry;
-import android.app.FragmentTransaction;
import androidx.leanback.R;
import androidx.leanback.transition.TransitionHelper;
import androidx.leanback.transition.TransitionListener;
@@ -61,6 +58,8 @@
import androidx.leanback.widget.VerticalGridView;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.Nullable;
+
import java.util.HashMap;
import java.util.Map;
@@ -1269,8 +1268,8 @@
}
@Override
- @Nullable
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+ public @Nullable View onCreateView(LayoutInflater inflater,@Nullable ViewGroup container,
+ Bundle savedInstanceState) {
if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) {
mHeadersFragment = onCreateHeadersFragment();
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BrowseSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/BrowseSupportFragment.java
index 3753ec5..ea377f8 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BrowseSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BrowseSupportFragment.java
@@ -28,8 +28,6 @@
import android.view.ViewTreeObserver;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
@@ -57,6 +55,9 @@
import androidx.leanback.widget.VerticalGridView;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.HashMap;
import java.util.Map;
@@ -1244,9 +1245,8 @@
}
@Override
- @Nullable
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) {
mHeadersSupportFragment = onCreateHeadersSupportFragment();
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/DetailsBackgroundVideoHelper.java b/leanback/leanback/src/main/java/androidx/leanback/app/DetailsBackgroundVideoHelper.java
index caf176ff..b06b151 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/DetailsBackgroundVideoHelper.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/DetailsBackgroundVideoHelper.java
@@ -20,13 +20,14 @@
import android.animation.ValueAnimator;
import android.graphics.drawable.Drawable;
-import androidx.annotation.NonNull;
import androidx.leanback.media.PlaybackGlue;
import androidx.leanback.widget.DetailsParallax;
import androidx.leanback.widget.Parallax;
import androidx.leanback.widget.ParallaxEffect;
import androidx.leanback.widget.ParallaxTarget;
+import org.jspecify.annotations.NonNull;
+
/**
* Helper class responsible for controlling video playback in {@link DetailsFragment}. This
* takes {@link DetailsParallax}, {@link PlaybackGlue} and a drawable as input.
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/DetailsFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/DetailsFragment.java
index 2d78042..3ae0336 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/DetailsFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/DetailsFragment.java
@@ -19,6 +19,9 @@
*/
package androidx.leanback.app;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
@@ -31,11 +34,6 @@
import android.view.Window;
import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import android.app.Fragment;
-import android.app.Activity;
-import android.app.FragmentTransaction;
import androidx.leanback.R;
import androidx.leanback.transition.TransitionHelper;
import androidx.leanback.transition.TransitionListener;
@@ -54,6 +52,9 @@
import androidx.leanback.widget.RowPresenter;
import androidx.leanback.widget.VerticalGridView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.ref.WeakReference;
/**
@@ -479,8 +480,8 @@
}
@Override
- @Nullable
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+ public @Nullable View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ Bundle savedInstanceState) {
mRootView = (BrowseFrameLayout) inflater.inflate(
R.layout.lb_details_fragment, container, false);
mBackgroundView = mRootView.findViewById(R.id.details_background_view);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/DetailsFragmentBackgroundController.java b/leanback/leanback/src/main/java/androidx/leanback/app/DetailsFragmentBackgroundController.java
index 144d355..a4933e9 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/DetailsFragmentBackgroundController.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/DetailsFragmentBackgroundController.java
@@ -19,15 +19,13 @@
package androidx.leanback.app;
import android.animation.PropertyValuesHolder;
+import android.app.Fragment;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import android.app.Fragment;
import androidx.leanback.R;
import androidx.leanback.graphics.FitWidthBitmapDrawable;
import androidx.leanback.media.PlaybackGlue;
@@ -35,6 +33,9 @@
import androidx.leanback.widget.DetailsParallaxDrawable;
import androidx.leanback.widget.ParallaxTarget;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Controller for DetailsFragment parallax background and embedded video play.
* <p>
@@ -189,7 +190,7 @@
* @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called.
*/
public void enableParallax(@NonNull Drawable coverDrawable, @NonNull Drawable bottomDrawable,
- @Nullable ParallaxTarget.PropertyValuesHolderTarget
+ ParallaxTarget.@Nullable PropertyValuesHolderTarget
coverDrawableParallaxTarget) {
if (mParallaxDrawable != null) {
return;
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/DetailsSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/DetailsSupportFragment.java
index 2b97bfb..3ee572b2 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/DetailsSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/DetailsSupportFragment.java
@@ -28,8 +28,6 @@
import android.view.Window;
import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentTransaction;
@@ -51,6 +49,9 @@
import androidx.leanback.widget.RowPresenter;
import androidx.leanback.widget.VerticalGridView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.lang.ref.WeakReference;
/**
@@ -474,9 +475,8 @@
}
@Override
- @Nullable
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mRootView = (BrowseFrameLayout) inflater.inflate(
R.layout.lb_details_fragment, container, false);
mBackgroundView = mRootView.findViewById(R.id.details_background_view);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/DetailsSupportFragmentBackgroundController.java b/leanback/leanback/src/main/java/androidx/leanback/app/DetailsSupportFragmentBackgroundController.java
index cc32404..2b6127b 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/DetailsSupportFragmentBackgroundController.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/DetailsSupportFragmentBackgroundController.java
@@ -22,8 +22,6 @@
import android.graphics.drawable.Drawable;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.leanback.R;
import androidx.leanback.graphics.FitWidthBitmapDrawable;
@@ -32,6 +30,9 @@
import androidx.leanback.widget.DetailsParallaxDrawable;
import androidx.leanback.widget.ParallaxTarget;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Controller for DetailsSupportFragment parallax background and embedded video play.
* <p>
@@ -184,7 +185,7 @@
* @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called.
*/
public void enableParallax(@NonNull Drawable coverDrawable, @NonNull Drawable bottomDrawable,
- @Nullable ParallaxTarget.PropertyValuesHolderTarget
+ ParallaxTarget.@Nullable PropertyValuesHolderTarget
coverDrawableParallaxTarget) {
if (mParallaxDrawable != null) {
return;
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/ErrorFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/ErrorFragment.java
index d887e5f..642a453 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/ErrorFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/ErrorFragment.java
@@ -29,10 +29,10 @@
import android.widget.ImageView;
import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
+import org.jspecify.annotations.Nullable;
+
/**
* A fragment for displaying an error indication.
* @deprecated use {@link ErrorSupportFragment}
@@ -89,8 +89,7 @@
/**
* Returns the background drawable. May be null if a default is used.
*/
- @Nullable
- public Drawable getBackgroundDrawable() {
+ public @Nullable Drawable getBackgroundDrawable() {
return mBackgroundDrawable;
}
@@ -107,8 +106,7 @@
/**
* Returns the drawable used for the error image.
*/
- @Nullable
- public Drawable getImageDrawable() {
+ public @Nullable Drawable getImageDrawable() {
return mDrawable;
}
@@ -125,8 +123,7 @@
/**
* Returns the error message.
*/
- @Nullable
- public CharSequence getMessage() {
+ public @Nullable CharSequence getMessage() {
return mMessage;
}
@@ -143,8 +140,7 @@
/**
* Returns the button text.
*/
- @Nullable
- public String getButtonText() {
+ public @Nullable String getButtonText() {
return mButtonText;
}
@@ -153,7 +149,7 @@
*
* @param clickListener The click listener for the button.
*/
- public void setButtonClickListener(@Nullable View.OnClickListener clickListener) {
+ public void setButtonClickListener(View.@Nullable OnClickListener clickListener) {
mButtonClickListener = clickListener;
updateButton();
}
@@ -161,14 +157,13 @@
/**
* Returns the button click listener.
*/
- @Nullable
- public View.OnClickListener getButtonClickListener() {
+ public View.@Nullable OnClickListener getButtonClickListener() {
return mButtonClickListener;
}
@Override
- @Nullable
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+ public @Nullable View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.lb_error_fragment, container, false);
mErrorFrame = (ViewGroup) root.findViewById(R.id.error_frame);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/ErrorSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/ErrorSupportFragment.java
index e1afe1e..e0ba349 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/ErrorSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/ErrorSupportFragment.java
@@ -26,10 +26,11 @@
import android.widget.ImageView;
import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* A fragment for displaying an error indication.
*/
@@ -84,8 +85,7 @@
/**
* Returns the background drawable. May be null if a default is used.
*/
- @Nullable
- public Drawable getBackgroundDrawable() {
+ public @Nullable Drawable getBackgroundDrawable() {
return mBackgroundDrawable;
}
@@ -102,8 +102,7 @@
/**
* Returns the drawable used for the error image.
*/
- @Nullable
- public Drawable getImageDrawable() {
+ public @Nullable Drawable getImageDrawable() {
return mDrawable;
}
@@ -120,8 +119,7 @@
/**
* Returns the error message.
*/
- @Nullable
- public CharSequence getMessage() {
+ public @Nullable CharSequence getMessage() {
return mMessage;
}
@@ -138,8 +136,7 @@
/**
* Returns the button text.
*/
- @Nullable
- public String getButtonText() {
+ public @Nullable String getButtonText() {
return mButtonText;
}
@@ -148,7 +145,7 @@
*
* @param clickListener The click listener for the button.
*/
- public void setButtonClickListener(@Nullable View.OnClickListener clickListener) {
+ public void setButtonClickListener(View.@Nullable OnClickListener clickListener) {
mButtonClickListener = clickListener;
updateButton();
}
@@ -156,15 +153,13 @@
/**
* Returns the button click listener.
*/
- @Nullable
- public View.OnClickListener getButtonClickListener() {
+ public View.@Nullable OnClickListener getButtonClickListener() {
return mButtonClickListener;
}
@Override
- @Nullable
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.lb_error_fragment, container, false);
mErrorFrame = (ViewGroup) root.findViewById(R.id.error_frame);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/GuidedStepFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/GuidedStepFragment.java
index 031433f9..a80d629 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/GuidedStepFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/GuidedStepFragment.java
@@ -20,6 +20,11 @@
import android.animation.Animator;
import android.animation.AnimatorSet;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentManager.BackStackEntry;
+import android.app.FragmentTransaction;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
@@ -33,15 +38,8 @@
import android.widget.FrameLayout;
import android.widget.LinearLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.app.ActivityCompat;
-import android.app.Fragment;
-import android.app.Activity;
-import android.app.FragmentManager;
-import android.app.FragmentManager.BackStackEntry;
-import android.app.FragmentTransaction;
import androidx.leanback.R;
import androidx.leanback.transition.TransitionHelper;
import androidx.leanback.widget.DiffCallback;
@@ -54,6 +52,9 @@
import androidx.leanback.widget.NonOverlappingLinearLayout;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.List;
@@ -248,9 +249,8 @@
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public static class DummyFragment extends Fragment {
- @NonNull
@Override
- public View onCreateView(
+ public @NonNull View onCreateView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState
@@ -282,8 +282,7 @@
* a basic GuidanceStylist.
* @return The GuidanceStylist used in this fragment.
*/
- @NonNull
- public GuidanceStylist onCreateGuidanceStylist() {
+ public @NonNull GuidanceStylist onCreateGuidanceStylist() {
return new GuidanceStylist();
}
@@ -292,8 +291,7 @@
* returns a basic GuidedActionsStylist.
* @return The GuidedActionsStylist used in this fragment.
*/
- @NonNull
- public GuidedActionsStylist onCreateActionsStylist() {
+ public @NonNull GuidedActionsStylist onCreateActionsStylist() {
return new GuidedActionsStylist();
}
@@ -302,8 +300,7 @@
* The default implementation returns a basic GuidedActionsStylist.
* @return The GuidedActionsStylist used in this fragment.
*/
- @NonNull
- public GuidedActionsStylist onCreateButtonActionsStylist() {
+ public @NonNull GuidedActionsStylist onCreateButtonActionsStylist() {
GuidedActionsStylist stylist = new GuidedActionsStylist();
stylist.setAsButtonActions();
return stylist;
@@ -695,8 +692,7 @@
* Returns the current GuidedStepFragment on the fragment transaction stack.
* @return The current GuidedStepFragment, if any, on the fragment transaction stack.
*/
- @Nullable
- public static GuidedStepFragment getCurrentGuidedStepFragment(
+ public static @Nullable GuidedStepFragment getCurrentGuidedStepFragment(
@NonNull FragmentManager fm
) {
Fragment f = fm.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT);
@@ -710,8 +706,7 @@
* Returns the GuidanceStylist that displays guidance information for the user.
* @return The GuidanceStylist for this fragment.
*/
- @NonNull
- public GuidanceStylist getGuidanceStylist() {
+ public @NonNull GuidanceStylist getGuidanceStylist() {
return mGuidanceStylist;
}
@@ -719,8 +714,7 @@
* Returns the GuidedActionsStylist that displays the actions the user may take.
* @return The GuidedActionsStylist for this fragment.
*/
- @NonNull
- public GuidedActionsStylist getGuidedActionsStylist() {
+ public @NonNull GuidedActionsStylist getGuidedActionsStylist() {
return mActionsStylist;
}
@@ -728,8 +722,7 @@
* Returns the list of button GuidedActions that the user may take in this fragment.
* @return The list of button GuidedActions for this fragment.
*/
- @NonNull
- public List<GuidedAction> getButtonActions() {
+ public @NonNull List<GuidedAction> getButtonActions() {
return mButtonActions;
}
@@ -738,8 +731,7 @@
* @param id Id of the button action to search.
* @return GuidedAction object or null if not found.
*/
- @Nullable
- public GuidedAction findButtonActionById(long id) {
+ public @Nullable GuidedAction findButtonActionById(long id) {
int index = findButtonActionPositionById(id);
return index >= 0 ? mButtonActions.get(index) : null;
}
@@ -764,8 +756,7 @@
* Returns the GuidedActionsStylist that displays the button actions the user may take.
* @return The GuidedActionsStylist for this fragment.
*/
- @NonNull
- public GuidedActionsStylist getGuidedButtonActionsStylist() {
+ public @NonNull GuidedActionsStylist getGuidedButtonActionsStylist() {
return mButtonActionsStylist;
}
@@ -797,8 +788,7 @@
* @return The View corresponding to the button action at the indicated position, or null if
* that action is not currently onscreen.
*/
- @Nullable
- public View getButtonActionItemView(int position) {
+ public @Nullable View getButtonActionItemView(int position) {
final RecyclerView.ViewHolder holder = mButtonActionsStylist.getActionsGridView()
.findViewHolderForPosition(position);
return holder == null ? null : holder.itemView;
@@ -824,8 +814,7 @@
* Returns the list of GuidedActions that the user may take in this fragment.
* @return The list of GuidedActions for this fragment.
*/
- @NonNull
- public List<GuidedAction> getActions() {
+ public @NonNull List<GuidedAction> getActions() {
return mActions;
}
@@ -834,8 +823,7 @@
* @param id Id of the action to search.
* @return GuidedAction object or null if not found.
*/
- @Nullable
- public GuidedAction findActionById(long id) {
+ public @Nullable GuidedAction findActionById(long id) {
int index = findActionPositionById(id);
return index >= 0 ? mActions.get(index) : null;
}
@@ -898,8 +886,7 @@
* @return The View corresponding to the action at the indicated position, or null if that
* action is not currently onscreen.
*/
- @Nullable
- public View getActionItemView(int position) {
+ public @Nullable View getActionItemView(int position) {
final RecyclerView.ViewHolder holder = mActionsStylist.getActionsGridView()
.findViewHolderForPosition(position);
return holder == null ? null : holder.itemView;
@@ -1008,8 +995,7 @@
* @param savedInstanceState
* @return Created background view or null if no background.
*/
- @Nullable
- public View onCreateBackgroundView(
+ public @Nullable View onCreateBackgroundView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState
@@ -1111,8 +1097,8 @@
* {@inheritDoc}
*/
@Override
- @Nullable
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+ public @Nullable View onCreateView(LayoutInflater inflater,@Nullable ViewGroup container,
+ Bundle savedInstanceState) {
if (DEBUG) Log.v(TAG, "onCreateView");
resolveTheme();
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/GuidedStepSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/GuidedStepSupportFragment.java
index 17ef924..60bd6ba 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/GuidedStepSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/GuidedStepSupportFragment.java
@@ -30,8 +30,6 @@
import android.widget.FrameLayout;
import android.widget.LinearLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
@@ -51,6 +49,9 @@
import androidx.leanback.widget.NonOverlappingLinearLayout;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.List;
@@ -243,9 +244,8 @@
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public static class DummyFragment extends Fragment {
- @NonNull
@Override
- public View onCreateView(
+ public @NonNull View onCreateView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState
@@ -277,8 +277,7 @@
* a basic GuidanceStylist.
* @return The GuidanceStylist used in this fragment.
*/
- @NonNull
- public GuidanceStylist onCreateGuidanceStylist() {
+ public @NonNull GuidanceStylist onCreateGuidanceStylist() {
return new GuidanceStylist();
}
@@ -287,8 +286,7 @@
* returns a basic GuidedActionsStylist.
* @return The GuidedActionsStylist used in this fragment.
*/
- @NonNull
- public GuidedActionsStylist onCreateActionsStylist() {
+ public @NonNull GuidedActionsStylist onCreateActionsStylist() {
return new GuidedActionsStylist();
}
@@ -297,8 +295,7 @@
* The default implementation returns a basic GuidedActionsStylist.
* @return The GuidedActionsStylist used in this fragment.
*/
- @NonNull
- public GuidedActionsStylist onCreateButtonActionsStylist() {
+ public @NonNull GuidedActionsStylist onCreateButtonActionsStylist() {
GuidedActionsStylist stylist = new GuidedActionsStylist();
stylist.setAsButtonActions();
return stylist;
@@ -690,8 +687,7 @@
* Returns the current GuidedStepSupportFragment on the fragment transaction stack.
* @return The current GuidedStepSupportFragment, if any, on the fragment transaction stack.
*/
- @Nullable
- public static GuidedStepSupportFragment getCurrentGuidedStepSupportFragment(
+ public static @Nullable GuidedStepSupportFragment getCurrentGuidedStepSupportFragment(
@NonNull FragmentManager fm
) {
Fragment f = fm.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT);
@@ -705,8 +701,7 @@
* Returns the GuidanceStylist that displays guidance information for the user.
* @return The GuidanceStylist for this fragment.
*/
- @NonNull
- public GuidanceStylist getGuidanceStylist() {
+ public @NonNull GuidanceStylist getGuidanceStylist() {
return mGuidanceStylist;
}
@@ -714,8 +709,7 @@
* Returns the GuidedActionsStylist that displays the actions the user may take.
* @return The GuidedActionsStylist for this fragment.
*/
- @NonNull
- public GuidedActionsStylist getGuidedActionsStylist() {
+ public @NonNull GuidedActionsStylist getGuidedActionsStylist() {
return mActionsStylist;
}
@@ -723,8 +717,7 @@
* Returns the list of button GuidedActions that the user may take in this fragment.
* @return The list of button GuidedActions for this fragment.
*/
- @NonNull
- public List<GuidedAction> getButtonActions() {
+ public @NonNull List<GuidedAction> getButtonActions() {
return mButtonActions;
}
@@ -733,8 +726,7 @@
* @param id Id of the button action to search.
* @return GuidedAction object or null if not found.
*/
- @Nullable
- public GuidedAction findButtonActionById(long id) {
+ public @Nullable GuidedAction findButtonActionById(long id) {
int index = findButtonActionPositionById(id);
return index >= 0 ? mButtonActions.get(index) : null;
}
@@ -759,8 +751,7 @@
* Returns the GuidedActionsStylist that displays the button actions the user may take.
* @return The GuidedActionsStylist for this fragment.
*/
- @NonNull
- public GuidedActionsStylist getGuidedButtonActionsStylist() {
+ public @NonNull GuidedActionsStylist getGuidedButtonActionsStylist() {
return mButtonActionsStylist;
}
@@ -792,8 +783,7 @@
* @return The View corresponding to the button action at the indicated position, or null if
* that action is not currently onscreen.
*/
- @Nullable
- public View getButtonActionItemView(int position) {
+ public @Nullable View getButtonActionItemView(int position) {
final RecyclerView.ViewHolder holder = mButtonActionsStylist.getActionsGridView()
.findViewHolderForPosition(position);
return holder == null ? null : holder.itemView;
@@ -819,8 +809,7 @@
* Returns the list of GuidedActions that the user may take in this fragment.
* @return The list of GuidedActions for this fragment.
*/
- @NonNull
- public List<GuidedAction> getActions() {
+ public @NonNull List<GuidedAction> getActions() {
return mActions;
}
@@ -829,8 +818,7 @@
* @param id Id of the action to search.
* @return GuidedAction object or null if not found.
*/
- @Nullable
- public GuidedAction findActionById(long id) {
+ public @Nullable GuidedAction findActionById(long id) {
int index = findActionPositionById(id);
return index >= 0 ? mActions.get(index) : null;
}
@@ -893,8 +881,7 @@
* @return The View corresponding to the action at the indicated position, or null if that
* action is not currently onscreen.
*/
- @Nullable
- public View getActionItemView(int position) {
+ public @Nullable View getActionItemView(int position) {
final RecyclerView.ViewHolder holder = mActionsStylist.getActionsGridView()
.findViewHolderForPosition(position);
return holder == null ? null : holder.itemView;
@@ -1003,8 +990,7 @@
* @param savedInstanceState
* @return Created background view or null if no background.
*/
- @Nullable
- public View onCreateBackgroundView(
+ public @Nullable View onCreateBackgroundView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState
@@ -1106,9 +1092,8 @@
* {@inheritDoc}
*/
@Override
- @Nullable
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
if (DEBUG) Log.v(TAG, "onCreateView");
resolveTheme();
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/HeadersFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/HeadersFragment.java
index 27ff655..d183e7a 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/HeadersFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/HeadersFragment.java
@@ -28,8 +28,6 @@
import android.view.ViewGroup;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
import androidx.leanback.widget.ClassPresenterSelector;
import androidx.leanback.widget.DividerPresenter;
@@ -44,6 +42,9 @@
import androidx.leanback.widget.VerticalGridView;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* An fragment containing a list of row headers. Implementation must support three types of rows:
* <ul>
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/HeadersSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/HeadersSupportFragment.java
index f601936..2c2b142 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/HeadersSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/HeadersSupportFragment.java
@@ -25,8 +25,6 @@
import android.view.ViewGroup;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
import androidx.leanback.widget.ClassPresenterSelector;
import androidx.leanback.widget.DividerPresenter;
@@ -41,6 +39,9 @@
import androidx.leanback.widget.VerticalGridView;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* An fragment containing a list of row headers. Implementation must support three types of rows:
* <ul>
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/ListRowDataAdapter.java b/leanback/leanback/src/main/java/androidx/leanback/app/ListRowDataAdapter.java
index d4e06ab..fd5152c 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/ListRowDataAdapter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/ListRowDataAdapter.java
@@ -1,9 +1,10 @@
package androidx.leanback.app;
-import androidx.annotation.Nullable;
import androidx.leanback.widget.ObjectAdapter;
import androidx.leanback.widget.Row;
+import org.jspecify.annotations.Nullable;
+
/**
* Wrapper class for {@link ObjectAdapter} used by {@link BrowseFragment} to initialize
* {@link RowsFragment}. We use invisible rows to represent
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/OnboardingFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/OnboardingFragment.java
index a0ec019..28a8b02 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/OnboardingFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/OnboardingFragment.java
@@ -25,6 +25,7 @@
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
+import android.app.Fragment;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
@@ -46,12 +47,12 @@
import android.widget.TextView;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import android.app.Fragment;
import androidx.leanback.R;
import androidx.leanback.widget.PagingIndicator;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.List;
@@ -309,8 +310,8 @@
}
@Override
- @Nullable
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+ public @Nullable View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ Bundle savedInstanceState) {
resolveTheme();
LayoutInflater localInflater = getThemeInflater(inflater);
final ViewGroup view = (ViewGroup) localInflater.inflate(R.layout.lb_onboarding_fragment,
@@ -514,8 +515,7 @@
* Returns the start button text if it's set through
* {@link #setStartButtonText(CharSequence)}}. If no string was set, null is returned.
*/
- @Nullable
- public final CharSequence getStartButtonText() {
+ public final @Nullable CharSequence getStartButtonText() {
return mStartButtonText;
}
@@ -593,8 +593,7 @@
*
* @return The {@link Animator} object which runs the logo animation.
*/
- @Nullable
- protected Animator onCreateLogoAnimation() {
+ protected @Nullable Animator onCreateLogoAnimation() {
return null;
}
@@ -641,8 +640,7 @@
*
* @return The {@link Animator} object which runs the page enter animation.
*/
- @Nullable
- protected Animator onCreateEnterAnimation() {
+ protected @Nullable Animator onCreateEnterAnimation() {
return null;
}
@@ -778,8 +776,7 @@
* Provides the entry animation for description view. This allows users to override the
* default fade and slide animation. Returning null will disable the animation.
*/
- @NonNull
- protected Animator onCreateDescriptionAnimator() {
+ protected @NonNull Animator onCreateDescriptionAnimator() {
return AnimatorInflater.loadAnimator(FragmentUtil.getContext(OnboardingFragment.this),
R.animator.lb_onboarding_description_enter);
}
@@ -788,8 +785,7 @@
* Provides the entry animation for title view. This allows users to override the
* default fade and slide animation. Returning null will disable the animation.
*/
- @NonNull
- protected Animator onCreateTitleAnimator() {
+ protected @NonNull Animator onCreateTitleAnimator() {
return AnimatorInflater.loadAnimator(FragmentUtil.getContext(OnboardingFragment.this),
R.animator.lb_onboarding_title_enter);
}
@@ -817,8 +813,7 @@
*
* @return The title of the page.
*/
- @Nullable
- protected abstract CharSequence getPageTitle(int pageIndex);
+ protected abstract @Nullable CharSequence getPageTitle(int pageIndex);
/**
* Returns the description of the given page.
@@ -827,8 +822,7 @@
*
* @return The description of the page.
*/
- @Nullable
- protected abstract CharSequence getPageDescription(int pageIndex);
+ protected abstract @Nullable CharSequence getPageDescription(int pageIndex);
/**
* Returns the index of the current page.
@@ -850,8 +844,7 @@
*
* @return The background view for the onboarding screen, or {@code null}.
*/
- @Nullable
- protected abstract View onCreateBackgroundView(
+ protected abstract @Nullable View onCreateBackgroundView(
@NonNull LayoutInflater inflater,
@NonNull ViewGroup container
);
@@ -869,8 +862,7 @@
*
* @return The content view for the onboarding screen, or {@code null}.
*/
- @Nullable
- protected abstract View onCreateContentView(
+ protected abstract @Nullable View onCreateContentView(
@NonNull LayoutInflater inflater,
@NonNull ViewGroup container
);
@@ -888,8 +880,7 @@
*
* @return The foreground view for the onboarding screen, or {@code null}.
*/
- @Nullable
- protected abstract View onCreateForegroundView(
+ protected abstract @Nullable View onCreateForegroundView(
@NonNull LayoutInflater inflater,
@NonNull ViewGroup container
);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/OnboardingSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/OnboardingSupportFragment.java
index 92be32e..168643f 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/OnboardingSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/OnboardingSupportFragment.java
@@ -43,12 +43,13 @@
import android.widget.TextView;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.leanback.R;
import androidx.leanback.widget.PagingIndicator;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.List;
@@ -304,9 +305,8 @@
}
@Override
- @Nullable
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
resolveTheme();
LayoutInflater localInflater = getThemeInflater(inflater);
final ViewGroup view = (ViewGroup) localInflater.inflate(R.layout.lb_onboarding_fragment,
@@ -510,8 +510,7 @@
* Returns the start button text if it's set through
* {@link #setStartButtonText(CharSequence)}}. If no string was set, null is returned.
*/
- @Nullable
- public final CharSequence getStartButtonText() {
+ public final @Nullable CharSequence getStartButtonText() {
return mStartButtonText;
}
@@ -589,8 +588,7 @@
*
* @return The {@link Animator} object which runs the logo animation.
*/
- @Nullable
- protected Animator onCreateLogoAnimation() {
+ protected @Nullable Animator onCreateLogoAnimation() {
return null;
}
@@ -637,8 +635,7 @@
*
* @return The {@link Animator} object which runs the page enter animation.
*/
- @Nullable
- protected Animator onCreateEnterAnimation() {
+ protected @Nullable Animator onCreateEnterAnimation() {
return null;
}
@@ -774,8 +771,7 @@
* Provides the entry animation for description view. This allows users to override the
* default fade and slide animation. Returning null will disable the animation.
*/
- @NonNull
- protected Animator onCreateDescriptionAnimator() {
+ protected @NonNull Animator onCreateDescriptionAnimator() {
return AnimatorInflater.loadAnimator(getContext(),
R.animator.lb_onboarding_description_enter);
}
@@ -784,8 +780,7 @@
* Provides the entry animation for title view. This allows users to override the
* default fade and slide animation. Returning null will disable the animation.
*/
- @NonNull
- protected Animator onCreateTitleAnimator() {
+ protected @NonNull Animator onCreateTitleAnimator() {
return AnimatorInflater.loadAnimator(getContext(),
R.animator.lb_onboarding_title_enter);
}
@@ -813,8 +808,7 @@
*
* @return The title of the page.
*/
- @Nullable
- protected abstract CharSequence getPageTitle(int pageIndex);
+ protected abstract @Nullable CharSequence getPageTitle(int pageIndex);
/**
* Returns the description of the given page.
@@ -823,8 +817,7 @@
*
* @return The description of the page.
*/
- @Nullable
- protected abstract CharSequence getPageDescription(int pageIndex);
+ protected abstract @Nullable CharSequence getPageDescription(int pageIndex);
/**
* Returns the index of the current page.
@@ -846,8 +839,7 @@
*
* @return The background view for the onboarding screen, or {@code null}.
*/
- @Nullable
- protected abstract View onCreateBackgroundView(
+ protected abstract @Nullable View onCreateBackgroundView(
@NonNull LayoutInflater inflater,
@NonNull ViewGroup container
);
@@ -865,8 +857,7 @@
*
* @return The content view for the onboarding screen, or {@code null}.
*/
- @Nullable
- protected abstract View onCreateContentView(
+ protected abstract @Nullable View onCreateContentView(
@NonNull LayoutInflater inflater,
@NonNull ViewGroup container
);
@@ -884,8 +875,7 @@
*
* @return The foreground view for the onboarding screen, or {@code null}.
*/
- @Nullable
- protected abstract View onCreateForegroundView(
+ protected abstract @Nullable View onCreateForegroundView(
@NonNull LayoutInflater inflater,
@NonNull ViewGroup container
);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/PlaybackFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/PlaybackFragment.java
index 952bd94..1f5cc11 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/PlaybackFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/PlaybackFragment.java
@@ -21,6 +21,7 @@
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.app.Fragment;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
@@ -37,10 +38,7 @@
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
-import android.app.Fragment;
import androidx.leanback.R;
import androidx.leanback.animation.LogAccelerateInterpolator;
import androidx.leanback.animation.LogDecelerateInterpolator;
@@ -63,6 +61,9 @@
import androidx.leanback.widget.VerticalGridView;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* A fragment for displaying playback controls and related content.
*
@@ -915,8 +916,8 @@
};
@Override
- @Nullable
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+ public @Nullable View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ Bundle savedInstanceState) {
mRootView = inflater.inflate(R.layout.lb_playback_fragment, container, false);
mBackgroundView = mRootView.findViewById(R.id.playback_fragment_background);
mRowsFragment = (RowsFragment) getChildFragmentManager().findFragmentById(
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/PlaybackSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/PlaybackSupportFragment.java
index 11aa41a..1e1ded2 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/PlaybackSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/PlaybackSupportFragment.java
@@ -34,8 +34,6 @@
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.fragment.app.Fragment;
import androidx.leanback.R;
@@ -60,6 +58,9 @@
import androidx.leanback.widget.VerticalGridView;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* A fragment for displaying playback controls and related content.
*
@@ -910,9 +911,8 @@
};
@Override
- @Nullable
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mRootView = inflater.inflate(R.layout.lb_playback_fragment, container, false);
mBackgroundView = mRootView.findViewById(R.id.playback_fragment_background);
mRowsSupportFragment = (RowsSupportFragment) getChildFragmentManager().findFragmentById(
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/RowsFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/RowsFragment.java
index fa9f12c..165d4e4 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/RowsFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/RowsFragment.java
@@ -26,8 +26,6 @@
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
import androidx.leanback.widget.BaseOnItemViewClickedListener;
import androidx.leanback.widget.BaseOnItemViewSelectedListener;
@@ -44,6 +42,9 @@
import androidx.leanback.widget.ViewHolderTask;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
/**
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/RowsSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/RowsSupportFragment.java
index 64fe558..3cb8054 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/RowsSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/RowsSupportFragment.java
@@ -23,8 +23,6 @@
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
import androidx.leanback.widget.BaseOnItemViewClickedListener;
import androidx.leanback.widget.BaseOnItemViewSelectedListener;
@@ -41,6 +39,9 @@
import androidx.leanback.widget.ViewHolderTask;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
/**
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/SearchFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/SearchFragment.java
index 181c6e7..482491e 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/SearchFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/SearchFragment.java
@@ -19,6 +19,7 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import android.Manifest;
+import android.app.Fragment;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
@@ -31,9 +32,6 @@
import android.view.ViewGroup;
import android.view.inputmethod.CompletionInfo;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import android.app.Fragment;
import androidx.leanback.R;
import androidx.leanback.widget.BrowseFrameLayout;
import androidx.leanback.widget.ObjectAdapter;
@@ -48,6 +46,8 @@
import androidx.leanback.widget.SpeechRecognitionCallback;
import androidx.leanback.widget.VerticalGridView;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.List;
@@ -296,8 +296,8 @@
}
@Override
- @Nullable
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+ public @Nullable View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.lb_search_fragment, container, false);
BrowseFrameLayout searchFrame = root.findViewById(R.id.lb_search_frame);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/SearchSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/SearchSupportFragment.java
index 17ffd88..08bd138 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/SearchSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/SearchSupportFragment.java
@@ -28,8 +28,6 @@
import android.view.ViewGroup;
import android.view.inputmethod.CompletionInfo;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.leanback.R;
import androidx.leanback.widget.BrowseFrameLayout;
@@ -45,6 +43,9 @@
import androidx.leanback.widget.SpeechRecognitionCallback;
import androidx.leanback.widget.VerticalGridView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.List;
@@ -291,9 +292,8 @@
}
@Override
- @Nullable
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.lb_search_fragment, container, false);
BrowseFrameLayout searchFrame = root.findViewById(R.id.lb_search_frame);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridFragment.java
index 85a8974..b5cb70c 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridFragment.java
@@ -22,8 +22,6 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
import androidx.leanback.transition.TransitionHelper;
import androidx.leanback.util.StateMachine.State;
@@ -37,6 +35,9 @@
import androidx.leanback.widget.RowPresenter;
import androidx.leanback.widget.VerticalGridPresenter;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* A fragment for creating leanback vertical grids.
*
@@ -97,8 +98,7 @@
/**
* Returns the grid presenter.
*/
- @Nullable
- public VerticalGridPresenter getGridPresenter() {
+ public @Nullable VerticalGridPresenter getGridPresenter() {
return mGridPresenter;
}
@@ -113,8 +113,7 @@
/**
* Returns the object adapter.
*/
- @Nullable
- public ObjectAdapter getAdapter() {
+ public @Nullable ObjectAdapter getAdapter() {
return mAdapter;
}
@@ -187,14 +186,13 @@
/**
* Returns the item clicked listener.
*/
- @Nullable
- public OnItemViewClickedListener getOnItemViewClickedListener() {
+ public @Nullable OnItemViewClickedListener getOnItemViewClickedListener() {
return mOnItemViewClickedListener;
}
@Override
- @Nullable
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+ public @Nullable View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ Bundle savedInstanceState) {
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.lb_vertical_grid_fragment,
container, false);
ViewGroup gridFrame = (ViewGroup) root.findViewById(R.id.grid_frame);
@@ -257,8 +255,7 @@
}
@Override
- @NonNull
- protected Object createEntranceTransition() {
+ protected @NonNull Object createEntranceTransition() {
return TransitionHelper.loadTransition(FragmentUtil.getContext(VerticalGridFragment.this),
R.transition.lb_vertical_grid_entrance_transition);
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridSupportFragment.java
index 4189876..41ccc03 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridSupportFragment.java
@@ -19,8 +19,6 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
import androidx.leanback.transition.TransitionHelper;
import androidx.leanback.util.StateMachine.State;
@@ -34,6 +32,9 @@
import androidx.leanback.widget.RowPresenter;
import androidx.leanback.widget.VerticalGridPresenter;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* A fragment for creating leanback vertical grids.
*
@@ -92,8 +93,7 @@
/**
* Returns the grid presenter.
*/
- @Nullable
- public VerticalGridPresenter getGridPresenter() {
+ public @Nullable VerticalGridPresenter getGridPresenter() {
return mGridPresenter;
}
@@ -108,8 +108,7 @@
/**
* Returns the object adapter.
*/
- @Nullable
- public ObjectAdapter getAdapter() {
+ public @Nullable ObjectAdapter getAdapter() {
return mAdapter;
}
@@ -182,15 +181,13 @@
/**
* Returns the item clicked listener.
*/
- @Nullable
- public OnItemViewClickedListener getOnItemViewClickedListener() {
+ public @Nullable OnItemViewClickedListener getOnItemViewClickedListener() {
return mOnItemViewClickedListener;
}
@Override
- @Nullable
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.lb_vertical_grid_fragment,
container, false);
ViewGroup gridFrame = (ViewGroup) root.findViewById(R.id.grid_frame);
@@ -253,8 +250,7 @@
}
@Override
- @NonNull
- protected Object createEntranceTransition() {
+ protected @NonNull Object createEntranceTransition() {
return TransitionHelper.loadTransition(getContext(),
R.transition.lb_vertical_grid_entrance_transition);
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/VideoFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/VideoFragment.java
index 125240d5..f370a17 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/VideoFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/VideoFragment.java
@@ -23,10 +23,10 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
+import org.jspecify.annotations.Nullable;
+
/**
* Subclass of {@link PlaybackFragment} that is responsible for providing a {@link SurfaceView}
* and rendering video.
@@ -45,8 +45,8 @@
int mState = SURFACE_NOT_CREATED;
@Override
- @Nullable
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+ public @Nullable View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ Bundle savedInstanceState) {
ViewGroup root = (ViewGroup) super.onCreateView(inflater, container, savedInstanceState);
mVideoSurface = (SurfaceView) LayoutInflater.from(FragmentUtil.getContext(VideoFragment.this)).inflate(
R.layout.lb_video_surface, root, false);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/VideoSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/VideoSupportFragment.java
index 2a79f91..8f340b0 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/VideoSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/VideoSupportFragment.java
@@ -20,10 +20,11 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Subclass of {@link PlaybackSupportFragment} that is responsible for providing a {@link SurfaceView}
* and rendering video.
@@ -40,9 +41,8 @@
int mState = SURFACE_NOT_CREATED;
@Override
- @Nullable
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
ViewGroup root = (ViewGroup) super.onCreateView(inflater, container, savedInstanceState);
mVideoSurface = (SurfaceView) LayoutInflater.from(getContext()).inflate(
R.layout.lb_video_surface, root, false);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/graphics/BoundsRule.java b/leanback/leanback/src/main/java/androidx/leanback/graphics/BoundsRule.java
index 55ea9ebd..25b445c 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/graphics/BoundsRule.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/graphics/BoundsRule.java
@@ -17,8 +17,8 @@
import android.graphics.Rect;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* This class contains the rules for updating the bounds of a
@@ -40,8 +40,7 @@
* @param fraction Percentage of parent.
* @return Newly created ValueRule.
*/
- @NonNull
- public static ValueRule inheritFromParent(float fraction) {
+ public static @NonNull ValueRule inheritFromParent(float fraction) {
return new ValueRule(0, fraction);
}
@@ -51,8 +50,7 @@
* @param absoluteValue Absolute value.
* @return Newly created ValueRule.
*/
- @NonNull
- public static ValueRule absoluteValue(int absoluteValue) {
+ public static @NonNull ValueRule absoluteValue(int absoluteValue) {
return new ValueRule(absoluteValue, 0);
}
@@ -63,8 +61,7 @@
* @param value Offset
* @return Newly created ValueRule.
*/
- @NonNull
- public static ValueRule inheritFromParentWithOffset(float fraction, int value) {
+ public static @NonNull ValueRule inheritFromParentWithOffset(float fraction, int value) {
return new ValueRule(value, fraction);
}
@@ -159,18 +156,14 @@
}
/** {@link ValueRule} for left attribute of {@link BoundsRule} */
- @Nullable
- public ValueRule left;
+ public @Nullable ValueRule left;
/** {@link ValueRule} for top attribute of {@link BoundsRule} */
- @Nullable
- public ValueRule top;
+ public @Nullable ValueRule top;
/** {@link ValueRule} for right attribute of {@link BoundsRule} */
- @Nullable
- public ValueRule right;
+ public @Nullable ValueRule right;
/** {@link ValueRule} for bottom attribute of {@link BoundsRule} */
- @Nullable
- public ValueRule bottom;
+ public @Nullable ValueRule bottom;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/graphics/CompositeDrawable.java b/leanback/leanback/src/main/java/androidx/leanback/graphics/CompositeDrawable.java
index 0b0b320..478097e 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/graphics/CompositeDrawable.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/graphics/CompositeDrawable.java
@@ -23,11 +23,12 @@
import android.graphics.drawable.Drawable;
import android.util.Property;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.leanback.graphics.BoundsRule.ValueRule;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
/**
@@ -52,9 +53,8 @@
}
}
- @NonNull
@Override
- public Drawable newDrawable() {
+ public @NonNull Drawable newDrawable() {
return new CompositeDrawable(this);
}
@@ -77,14 +77,12 @@
}
@Override
- @NonNull
- public ConstantState getConstantState() {
+ public @NonNull ConstantState getConstantState() {
return mState;
}
@Override
- @NonNull
- public Drawable mutate() {
+ public @NonNull Drawable mutate() {
if (!mMutated && super.mutate() == this) {
mState = new CompositeState(mState, this, null);
final ArrayList<ChildDrawable> children = mState.mChildren;
@@ -116,16 +114,14 @@
/**
* Returns the {@link Drawable} for the given index.
*/
- @NonNull
- public Drawable getDrawable(int index) {
+ public @NonNull Drawable getDrawable(int index) {
return mState.mChildren.get(index).mDrawable;
}
/**
* Returns the {@link ChildDrawable} at the given index.
*/
- @NonNull
- public ChildDrawable getChildAt(int index) {
+ public @NonNull ChildDrawable getChildAt(int index) {
return mState.mChildren.get(index);
}
@@ -288,16 +284,14 @@
/**
* Returns the instance of {@link BoundsRule}.
*/
- @NonNull
- public BoundsRule getBoundsRule() {
+ public @NonNull BoundsRule getBoundsRule() {
return this.mBoundsRule;
}
/**
* Returns the {@link Drawable}.
*/
- @NonNull
- public Drawable getDrawable() {
+ public @NonNull Drawable getDrawable() {
return mDrawable;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/graphics/FitWidthBitmapDrawable.java b/leanback/leanback/src/main/java/androidx/leanback/graphics/FitWidthBitmapDrawable.java
index 6208b78..798f9c2 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/graphics/FitWidthBitmapDrawable.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/graphics/FitWidthBitmapDrawable.java
@@ -26,9 +26,10 @@
import android.util.IntProperty;
import android.util.Property;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import org.jspecify.annotations.NonNull;
+
/**
* Subclass of {@link Drawable} that can be used to draw a bitmap into a region. Bitmap
* will be scaled to fit the full width of the region and will be aligned to the top left corner.
@@ -57,9 +58,8 @@
mOffset = other.mOffset;
}
- @NonNull
@Override
- public Drawable newDrawable() {
+ public @NonNull Drawable newDrawable() {
return new FitWidthBitmapDrawable(this);
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/media/MediaControllerAdapter.java b/leanback/leanback/src/main/java/androidx/leanback/media/MediaControllerAdapter.java
index c7a1f64..7cf323e 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/media/MediaControllerAdapter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/media/MediaControllerAdapter.java
@@ -34,9 +34,10 @@
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.leanback.widget.PlaybackControlsRow;
+import org.jspecify.annotations.NonNull;
+
/**
* A helper class for implementing a adapter layer for {@link MediaControllerCompat}.
*/
diff --git a/leanback/leanback/src/main/java/androidx/leanback/media/MediaPlayerAdapter.java b/leanback/leanback/src/main/java/androidx/leanback/media/MediaPlayerAdapter.java
index 8bef683..0ece6b8 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/media/MediaPlayerAdapter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/media/MediaPlayerAdapter.java
@@ -23,9 +23,10 @@
import android.os.Handler;
import android.view.SurfaceHolder;
-import androidx.annotation.NonNull;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+
import java.io.IOException;
/**
diff --git a/leanback/leanback/src/main/java/androidx/leanback/media/MediaPlayerGlue.java b/leanback/leanback/src/main/java/androidx/leanback/media/MediaPlayerGlue.java
index 87dc17f..2d3a0da 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/media/MediaPlayerGlue.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/media/MediaPlayerGlue.java
@@ -26,7 +26,6 @@
import android.view.SurfaceHolder;
import android.view.View;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.leanback.widget.Action;
import androidx.leanback.widget.ArrayObjectAdapter;
@@ -36,6 +35,8 @@
import androidx.leanback.widget.Row;
import androidx.leanback.widget.RowPresenter;
+import org.jspecify.annotations.NonNull;
+
import java.io.IOException;
import java.util.List;
diff --git a/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackBannerControlGlue.java b/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackBannerControlGlue.java
index aaa0103..af175c7 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackBannerControlGlue.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackBannerControlGlue.java
@@ -25,7 +25,6 @@
import android.view.View;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.leanback.widget.AbstractDetailsDescriptionPresenter;
import androidx.leanback.widget.Action;
@@ -36,6 +35,8 @@
import androidx.leanback.widget.PlaybackRowPresenter;
import androidx.leanback.widget.RowPresenter;
+import org.jspecify.annotations.NonNull;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -230,7 +231,7 @@
* @param impl Implementation to underlying media player.
*/
public PlaybackBannerControlGlue(@NonNull Context context,
- @NonNull int[] seekSpeeds,
+ int @NonNull [] seekSpeeds,
T impl) {
this(context, seekSpeeds, seekSpeeds, impl);
}
@@ -247,8 +248,8 @@
*/
public PlaybackBannerControlGlue(
@NonNull Context context,
- @NonNull int[] fastForwardSpeeds,
- @NonNull int[] rewindSpeeds,
+ int @NonNull [] fastForwardSpeeds,
+ int @NonNull [] rewindSpeeds,
T impl
) {
super(context, impl);
@@ -321,9 +322,8 @@
}
}
- @NonNull
@Override
- protected PlaybackRowPresenter onCreateRowPresenter() {
+ protected @NonNull PlaybackRowPresenter onCreateRowPresenter() {
final AbstractDetailsDescriptionPresenter detailsPresenter =
new AbstractDetailsDescriptionPresenter() {
@Override
@@ -339,14 +339,14 @@
new PlaybackControlsRowPresenter(detailsPresenter) {
@Override
protected void onBindRowViewHolder(
- @NonNull RowPresenter.ViewHolder vh,
+ RowPresenter.@NonNull ViewHolder vh,
@NonNull Object item
) {
super.onBindRowViewHolder(vh, item);
vh.setOnKeyListener(PlaybackBannerControlGlue.this);
}
@Override
- protected void onUnbindRowViewHolder(@NonNull RowPresenter.ViewHolder vh) {
+ protected void onUnbindRowViewHolder(RowPresenter.@NonNull ViewHolder vh) {
super.onUnbindRowViewHolder(vh);
vh.setOnKeyListener(null);
}
@@ -586,16 +586,14 @@
/**
* Returns the fast forward speeds.
*/
- @NonNull
- public int[] getFastForwardSpeeds() {
+ public int @NonNull [] getFastForwardSpeeds() {
return mFastForwardSpeeds;
}
/**
* Returns the rewind speeds.
*/
- @NonNull
- public int[] getRewindSpeeds() {
+ public int @NonNull [] getRewindSpeeds() {
return mRewindSpeeds;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackBaseControlGlue.java b/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackBaseControlGlue.java
index 8c6e741..b948bef 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackBaseControlGlue.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackBaseControlGlue.java
@@ -24,8 +24,6 @@
import android.view.View;
import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.widget.Action;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.ControlButtonPresenterSelector;
@@ -35,6 +33,9 @@
import androidx.leanback.widget.PlaybackTransportRowPresenter;
import androidx.leanback.widget.Presenter;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.List;
/**
@@ -293,8 +294,7 @@
}
}
- @NonNull
- protected abstract PlaybackRowPresenter onCreateRowPresenter();
+ protected abstract @NonNull PlaybackRowPresenter onCreateRowPresenter();
/**
* Sets the controls to auto hide after a timeout when media is playing.
@@ -358,16 +358,14 @@
/**
* Returns the playback controls row managed by the glue layer.
*/
- @Nullable
- public PlaybackControlsRow getControlsRow() {
+ public @Nullable PlaybackControlsRow getControlsRow() {
return mControlsRow;
}
/**
* Returns the playback controls row Presenter managed by the glue layer.
*/
- @Nullable
- public PlaybackRowPresenter getPlaybackRowPresenter() {
+ public @Nullable PlaybackRowPresenter getPlaybackRowPresenter() {
return mControlsRowPresenter;
}
@@ -523,8 +521,7 @@
/**
* @return The drawable representing cover image.
*/
- @Nullable
- public Drawable getArt() {
+ public @Nullable Drawable getArt() {
return mCover;
}
@@ -546,8 +543,7 @@
/**
* Return The media subtitle.
*/
- @Nullable
- public CharSequence getSubtitle() {
+ public @Nullable CharSequence getSubtitle() {
return mSubtitle;
}
@@ -568,8 +564,7 @@
/**
* Returns the title of the media item.
*/
- @Nullable
- public CharSequence getTitle() {
+ public @Nullable CharSequence getTitle() {
return mTitle;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackControlGlue.java b/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackControlGlue.java
index 3c788c0..4dc42d7 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackControlGlue.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackControlGlue.java
@@ -24,7 +24,6 @@
import android.view.KeyEvent;
import android.view.View;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.leanback.widget.AbstractDetailsDescriptionPresenter;
import androidx.leanback.widget.Action;
@@ -38,6 +37,8 @@
import androidx.leanback.widget.RowPresenter;
import androidx.leanback.widget.SparseArrayObjectAdapter;
+import org.jspecify.annotations.NonNull;
+
import java.lang.ref.WeakReference;
import java.util.List;
@@ -297,14 +298,14 @@
setPlaybackRowPresenter(new PlaybackControlsRowPresenter(detailsPresenter) {
@Override
protected void onBindRowViewHolder(
- @NonNull RowPresenter.ViewHolder vh,
+ RowPresenter.@NonNull ViewHolder vh,
@NonNull Object item
) {
super.onBindRowViewHolder(vh, item);
vh.setOnKeyListener(PlaybackControlGlue.this);
}
@Override
- protected void onUnbindRowViewHolder(@NonNull RowPresenter.ViewHolder vh) {
+ protected void onUnbindRowViewHolder(RowPresenter.@NonNull ViewHolder vh) {
super.onUnbindRowViewHolder(vh);
vh.setOnKeyListener(null);
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackGlue.java b/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackGlue.java
index cd9ef12..7c48c4f 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackGlue.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackGlue.java
@@ -20,8 +20,9 @@
import android.content.Context;
import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
@@ -86,8 +87,7 @@
/**
* Returns the context.
*/
- @NonNull
- public Context getContext() {
+ public @NonNull Context getContext() {
return mContext;
}
@@ -125,8 +125,7 @@
* @return A snapshot of list of PlayerCallbacks set on the Glue.
*/
@SuppressLint("NullableCollection")
- @Nullable
- protected List<PlayerCallback> getPlayerCallbacks() {
+ protected @Nullable List<PlayerCallback> getPlayerCallbacks() {
if (mPlayerCallbacks == null) {
return null;
}
@@ -279,8 +278,7 @@
/**
* @return Associated {@link PlaybackGlueHost} or null if not attached to host.
*/
- @Nullable
- public PlaybackGlueHost getHost() {
+ public @Nullable PlaybackGlueHost getHost() {
return mPlaybackGlueHost;
}
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackTransportControlGlue.java b/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackTransportControlGlue.java
index 34f9320..4e73978 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackTransportControlGlue.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackTransportControlGlue.java
@@ -23,7 +23,6 @@
import android.view.KeyEvent;
import android.view.View;
-import androidx.annotation.NonNull;
import androidx.leanback.widget.AbstractDetailsDescriptionPresenter;
import androidx.leanback.widget.Action;
import androidx.leanback.widget.ArrayObjectAdapter;
@@ -35,6 +34,8 @@
import androidx.leanback.widget.PlaybackTransportRowPresenter;
import androidx.leanback.widget.RowPresenter;
+import org.jspecify.annotations.NonNull;
+
import java.lang.ref.WeakReference;
/**
@@ -154,14 +155,14 @@
PlaybackTransportRowPresenter rowPresenter = new PlaybackTransportRowPresenter() {
@Override
protected void onBindRowViewHolder(
- @NonNull RowPresenter.ViewHolder vh,
+ RowPresenter.@NonNull ViewHolder vh,
@NonNull Object item
) {
super.onBindRowViewHolder(vh, item);
vh.setOnKeyListener(PlaybackTransportControlGlue.this);
}
@Override
- protected void onUnbindRowViewHolder(@NonNull RowPresenter.ViewHolder vh) {
+ protected void onUnbindRowViewHolder(RowPresenter.@NonNull ViewHolder vh) {
super.onUnbindRowViewHolder(vh);
vh.setOnKeyListener(null);
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/media/PlayerAdapter.java b/leanback/leanback/src/main/java/androidx/leanback/media/PlayerAdapter.java
index 38f28b4..6e075d4 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/media/PlayerAdapter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/media/PlayerAdapter.java
@@ -16,8 +16,8 @@
package androidx.leanback.media;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* Base class that wraps underlying media player. The class is used by PlaybackGlue, for example
@@ -129,8 +129,7 @@
* Gets callback for event of PlayerAdapter.
* @return Client for event of PlayerAdapter.
*/
- @Nullable
- public final Callback getCallback() {
+ public final @Nullable Callback getCallback() {
return mCallback;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/media/SurfaceHolderGlueHost.java b/leanback/leanback/src/main/java/androidx/leanback/media/SurfaceHolderGlueHost.java
index e9a48a9..41e3b89 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/media/SurfaceHolderGlueHost.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/media/SurfaceHolderGlueHost.java
@@ -18,7 +18,7 @@
import android.view.SurfaceHolder;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Optional interface to be implemented by any subclass of {@link PlaybackGlueHost} that contains
@@ -31,5 +31,5 @@
/**
* Sets the {@link SurfaceHolder.Callback} on the the host.
*/
- void setSurfaceHolderCallback(@Nullable SurfaceHolder.Callback callback);
+ void setSurfaceHolderCallback(SurfaceHolder.@Nullable Callback callback);
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/transition/TransitionHelper.java b/leanback/leanback/src/main/java/androidx/leanback/transition/TransitionHelper.java
index 7e2f8e4c..eabe2ca 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/transition/TransitionHelper.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/transition/TransitionHelper.java
@@ -35,10 +35,11 @@
import android.view.Window;
import android.view.animation.AnimationUtils;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Helper for view transitions.
*/
@@ -61,16 +62,13 @@
}
}
- @SuppressLint("ClassVerificationFailure")
- @Nullable
- public static Object getSharedElementEnterTransition(@NonNull Window window) {
+ public static @Nullable Object getSharedElementEnterTransition(@NonNull Window window) {
if (Build.VERSION.SDK_INT >= 21) {
return window.getSharedElementEnterTransition();
}
return null;
}
- @SuppressLint("ClassVerificationFailure")
public static void setSharedElementEnterTransition(
@NonNull Window window,
@Nullable Object transition
@@ -80,16 +78,13 @@
}
}
- @SuppressLint("ClassVerificationFailure")
- @Nullable
- public static Object getSharedElementReturnTransition(@NonNull Window window) {
+ public static @Nullable Object getSharedElementReturnTransition(@NonNull Window window) {
if (Build.VERSION.SDK_INT >= 21) {
return window.getSharedElementReturnTransition();
}
return null;
}
- @SuppressLint("ClassVerificationFailure")
public static void setSharedElementReturnTransition(
@NonNull Window window,
@Nullable Object transition
@@ -99,100 +94,79 @@
}
}
- @SuppressLint("ClassVerificationFailure")
- @Nullable
- public static Object getSharedElementExitTransition(@NonNull Window window) {
+ public static @Nullable Object getSharedElementExitTransition(@NonNull Window window) {
if (Build.VERSION.SDK_INT >= 21) {
return window.getSharedElementExitTransition();
}
return null;
}
- @SuppressLint("ClassVerificationFailure")
- @Nullable
- public static Object getSharedElementReenterTransition(@NonNull Window window) {
+ public static @Nullable Object getSharedElementReenterTransition(@NonNull Window window) {
if (Build.VERSION.SDK_INT >= 21) {
return window.getSharedElementReenterTransition();
}
return null;
}
- @SuppressLint("ClassVerificationFailure")
- @Nullable
- public static Object getEnterTransition(@NonNull Window window) {
+ public static @Nullable Object getEnterTransition(@NonNull Window window) {
if (Build.VERSION.SDK_INT >= 21) {
return window.getEnterTransition();
}
return null;
}
- @SuppressLint("ClassVerificationFailure")
public static void setEnterTransition(@NonNull Window window, @Nullable Object transition) {
if (Build.VERSION.SDK_INT >= 21) {
window.setEnterTransition((Transition) transition);
}
}
- @SuppressLint("ClassVerificationFailure")
- @Nullable
- public static Object getReturnTransition(@NonNull Window window) {
+ public static @Nullable Object getReturnTransition(@NonNull Window window) {
if (Build.VERSION.SDK_INT >= 21) {
return window.getReturnTransition();
}
return null;
}
- @SuppressLint("ClassVerificationFailure")
public static void setReturnTransition(@NonNull Window window, @Nullable Object transition) {
if (Build.VERSION.SDK_INT >= 21) {
window.setReturnTransition((Transition) transition);
}
}
- @SuppressLint("ClassVerificationFailure")
- @Nullable
- public static Object getExitTransition(@NonNull Window window) {
+ public static @Nullable Object getExitTransition(@NonNull Window window) {
if (Build.VERSION.SDK_INT >= 21) {
return window.getExitTransition();
}
return null;
}
- @SuppressLint("ClassVerificationFailure")
- @Nullable
- public static Object getReenterTransition(@NonNull Window window) {
+ public static @Nullable Object getReenterTransition(@NonNull Window window) {
if (Build.VERSION.SDK_INT >= 21) {
return window.getReenterTransition();
}
return null;
}
- @SuppressLint("ClassVerificationFailure")
- @Nullable
- public static Object createScene(@NonNull ViewGroup sceneRoot, @Nullable Runnable r) {
+ public static @Nullable Object createScene(@NonNull ViewGroup sceneRoot, @Nullable Runnable r) {
Scene scene = new Scene(sceneRoot);
scene.setEnterAction(r);
return scene;
}
- @SuppressLint("ClassVerificationFailure")
- @NonNull
- public static Object createChangeBounds(boolean reparent) {
+ public static @NonNull Object createChangeBounds(boolean reparent) {
CustomChangeBounds changeBounds = new CustomChangeBounds();
changeBounds.setReparent(reparent);
return changeBounds;
}
- @SuppressLint("ClassVerificationFailure")
- @NonNull
- public static Object createChangeTransform() {
+ public static @NonNull Object createChangeTransform() {
if (Build.VERSION.SDK_INT >= 21) {
return new ChangeTransform();
}
return new TransitionStub();
}
- @SuppressLint("ClassVerificationFailure")
public static void setChangeBoundsStartDelay(
@NonNull Object changeBounds,
@NonNull View view,
@@ -224,42 +198,34 @@
((CustomChangeBounds) changeBounds).setDefaultStartDelay(startDelay);
}
- @SuppressLint("ClassVerificationFailure")
- @NonNull
- public static Object createTransitionSet(boolean sequential) {
+ public static @NonNull Object createTransitionSet(boolean sequential) {
TransitionSet set = new TransitionSet();
set.setOrdering(sequential ? TransitionSet.ORDERING_SEQUENTIAL
: TransitionSet.ORDERING_TOGETHER);
return set;
}
- @NonNull
- public static Object createSlide(int slideEdge) {
+ public static @NonNull Object createSlide(int slideEdge) {
SlideKitkat slide = new SlideKitkat();
slide.setSlideEdge(slideEdge);
return slide;
}
- @SuppressLint("ClassVerificationFailure")
- @NonNull
- public static Object createScale() {
+ public static @NonNull Object createScale() {
if (Build.VERSION.SDK_INT >= 21) {
return new ChangeTransform();
}
return new Scale();
}
- @SuppressLint("ClassVerificationFailure")
public static void addTransition(@NonNull Object transitionSet, @NonNull Object transition) {
((TransitionSet) transitionSet).addTransition((Transition) transition);
}
- @SuppressLint("ClassVerificationFailure")
public static void exclude(@NonNull Object transition, int targetId, boolean exclude) {
((Transition) transition).excludeTarget(targetId, exclude);
}
- @SuppressLint("ClassVerificationFailure")
public static void exclude(
@NonNull Object transition,
@NonNull View targetView,
@@ -268,12 +234,10 @@
((Transition) transition).excludeTarget(targetView, exclude);
}
- @SuppressLint("ClassVerificationFailure")
public static void excludeChildren(@NonNull Object transition, int targetId, boolean exclude) {
((Transition) transition).excludeChildren(targetId, exclude);
}
- @SuppressLint("ClassVerificationFailure")
public static void excludeChildren(
@NonNull Object transition,
@NonNull View targetView,
@@ -282,39 +246,30 @@
((Transition) transition).excludeChildren(targetView, exclude);
}
- @SuppressLint("ClassVerificationFailure")
public static void include(@NonNull Object transition, int targetId) {
((Transition) transition).addTarget(targetId);
}
- @SuppressLint("ClassVerificationFailure")
public static void include(@NonNull Object transition, @NonNull View targetView) {
((Transition) transition).addTarget(targetView);
}
- @SuppressLint("ClassVerificationFailure")
public static void setStartDelay(@NonNull Object transition, long startDelay) {
((Transition) transition).setStartDelay(startDelay);
}
- @SuppressLint("ClassVerificationFailure")
public static void setDuration(@NonNull Object transition, long duration) {
((Transition) transition).setDuration(duration);
}
- @SuppressLint("ClassVerificationFailure")
- @NonNull
- public static Object createAutoTransition() {
+ public static @NonNull Object createAutoTransition() {
return new AutoTransition();
}
- @SuppressLint("ClassVerificationFailure")
- @NonNull
- public static Object createFadeTransition(int fadeMode) {
+ public static @NonNull Object createFadeTransition(int fadeMode) {
return new Fade(fadeMode);
}
- @SuppressLint("ClassVerificationFailure")
public static void addTransitionListener(
@NonNull Object transition,
final @Nullable TransitionListener listener
@@ -352,7 +307,6 @@
t.addListener((Transition.TransitionListener) listener.mImpl);
}
- @SuppressLint("ClassVerificationFailure")
public static void removeTransitionListener(
@NonNull Object transition,
@Nullable TransitionListener listener
@@ -365,12 +319,10 @@
listener.mImpl = null;
}
- @SuppressLint("ClassVerificationFailure")
public static void runTransition(@Nullable Object scene, @Nullable Object transition) {
TransitionManager.go((Scene) scene, (Transition) transition);
}
- @SuppressLint("ClassVerificationFailure")
public static void setInterpolator(
@NonNull Object transition,
@Nullable Object timeInterpolator
@@ -378,14 +330,11 @@
((Transition) transition).setInterpolator((TimeInterpolator) timeInterpolator);
}
- @SuppressLint("ClassVerificationFailure")
public static void addTarget(@NonNull Object transition, @NonNull View view) {
((Transition) transition).addTarget(view);
}
- @SuppressLint("ClassVerificationFailure")
- @Nullable
- public static Object createDefaultInterpolator(@NonNull Context context) {
+ public static @Nullable Object createDefaultInterpolator(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= 21) {
return AnimationUtils.loadInterpolator(context,
android.R.interpolator.fast_out_linear_in);
@@ -393,13 +342,11 @@
return null;
}
- @SuppressLint("ClassVerificationFailure")
- @NonNull
- public static Object loadTransition(@NonNull Context context, int resId) {
+ public static @NonNull Object loadTransition(@NonNull Context context, int resId) {
return TransitionInflater.from(context).inflateTransition(resId);
}
- @SuppressLint({"ReferencesDeprecated", "ClassVerificationFailure"})
+ @SuppressLint("ReferencesDeprecated")
public static void setEnterTransition(
@NonNull Fragment fragment,
@Nullable Object transition
@@ -409,7 +356,7 @@
}
}
- @SuppressLint({"ReferencesDeprecated", "ClassVerificationFailure"})
+ @SuppressLint("ReferencesDeprecated")
public static void setExitTransition(
@NonNull Fragment fragment,
@Nullable Object transition
@@ -419,7 +366,7 @@
}
}
- @SuppressLint({"ReferencesDeprecated", "ClassVerificationFailure"})
+ @SuppressLint("ReferencesDeprecated")
public static void setSharedElementEnterTransition(
@NonNull Fragment fragment,
@Nullable Object transition
@@ -429,7 +376,7 @@
}
}
- @SuppressLint({"ReferencesDeprecated", "ClassVerificationFailure"})
+ @SuppressLint("ReferencesDeprecated")
public static void addSharedElement(
@NonNull FragmentTransaction ft,
@NonNull View view,
@@ -440,18 +387,14 @@
}
}
- @SuppressLint("ClassVerificationFailure")
- @NonNull
- public static Object createFadeAndShortSlide(int edge) {
+ public static @NonNull Object createFadeAndShortSlide(int edge) {
if (Build.VERSION.SDK_INT >= 21) {
return new FadeAndShortSlide(edge);
}
return new TransitionStub();
}
- @SuppressLint("ClassVerificationFailure")
- @NonNull
- public static Object createFadeAndShortSlide(int edge, float distance) {
+ public static @NonNull Object createFadeAndShortSlide(int edge, float distance) {
if (Build.VERSION.SDK_INT >= 21) {
FadeAndShortSlide slide = new FadeAndShortSlide(edge);
slide.setDistance(distance);
@@ -460,7 +403,6 @@
return new TransitionStub();
}
- @SuppressLint("ClassVerificationFailure")
public static void beginDelayedTransition(
@NonNull ViewGroup sceneRoot,
@Nullable Object transitionObject
@@ -471,17 +413,15 @@
}
}
- @SuppressLint("ClassVerificationFailure")
public static void setTransitionGroup(@NonNull ViewGroup viewGroup, boolean transitionGroup) {
if (Build.VERSION.SDK_INT >= 21) {
viewGroup.setTransitionGroup(transitionGroup);
}
}
- @SuppressLint("ClassVerificationFailure")
public static void setEpicenterCallback(
@NonNull Object transition,
- @Nullable final TransitionEpicenterCallback callback
+ final @Nullable TransitionEpicenterCallback callback
) {
if (Build.VERSION.SDK_INT >= 21) {
if (callback == null) {
diff --git a/leanback/leanback/src/main/java/androidx/leanback/util/StateMachine.java b/leanback/leanback/src/main/java/androidx/leanback/util/StateMachine.java
index f84a1fb..6a53c40 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/util/StateMachine.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/util/StateMachine.java
@@ -17,9 +17,10 @@
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
import java.util.ArrayList;
/**
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/AbstractDetailsDescriptionPresenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/AbstractDetailsDescriptionPresenter.java
index 3ddf939..ee14b3a 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/AbstractDetailsDescriptionPresenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/AbstractDetailsDescriptionPresenter.java
@@ -22,10 +22,11 @@
import android.view.ViewTreeObserver;
import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* An abstract {@link Presenter} for rendering a detailed description of an
* item. Typically this Presenter will be used in a {@link FullWidthDetailsOverviewRowPresenter}
@@ -131,18 +132,15 @@
}
}
- @NonNull
- public TextView getTitle() {
+ public @NonNull TextView getTitle() {
return mTitle;
}
- @NonNull
- public TextView getSubtitle() {
+ public @NonNull TextView getSubtitle() {
return mSubtitle;
}
- @NonNull
- public TextView getBody() {
+ public @NonNull TextView getBody() {
return mBody;
}
@@ -155,8 +153,7 @@
}
@Override
- @NonNull
- public final ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
+ public final @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.lb_details_description, parent, false);
return new ViewHolder(v);
@@ -164,7 +161,7 @@
@Override
public final void onBindViewHolder(
- @NonNull Presenter.ViewHolder viewHolder,
+ Presenter.@NonNull ViewHolder viewHolder,
@Nullable Object item
) {
ViewHolder vh = (ViewHolder) viewHolder;
@@ -225,10 +222,10 @@
protected abstract void onBindDescription(@NonNull ViewHolder vh, @NonNull Object item);
@Override
- public void onUnbindViewHolder(@NonNull Presenter.ViewHolder viewHolder) {}
+ public void onUnbindViewHolder(Presenter.@NonNull ViewHolder viewHolder) {}
@Override
- public void onViewAttachedToWindow(@NonNull Presenter.ViewHolder holder) {
+ public void onViewAttachedToWindow(Presenter.@NonNull ViewHolder holder) {
// In case predraw listener was removed in detach, make sure
// we have the proper layout.
ViewHolder vh = (ViewHolder) holder;
@@ -237,7 +234,7 @@
}
@Override
- public void onViewDetachedFromWindow(@NonNull Presenter.ViewHolder holder) {
+ public void onViewDetachedFromWindow(Presenter.@NonNull ViewHolder holder) {
ViewHolder vh = (ViewHolder) holder;
vh.removePreDrawListener();
super.onViewDetachedFromWindow(holder);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/AbstractMediaItemPresenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/AbstractMediaItemPresenter.java
index bb123b4..9d87a7a 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/AbstractMediaItemPresenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/AbstractMediaItemPresenter.java
@@ -27,9 +27,10 @@
import android.widget.TextView;
import android.widget.ViewFlipper;
-import androidx.annotation.NonNull;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+
import java.util.ArrayList;
import java.util.List;
@@ -451,7 +452,7 @@
}
@Override
- protected void onBindRowViewHolder(@NonNull RowPresenter.ViewHolder vh, @NonNull Object item) {
+ protected void onBindRowViewHolder(RowPresenter.@NonNull ViewHolder vh, @NonNull Object item) {
super.onBindRowViewHolder(vh, item);
final ViewHolder mvh = (ViewHolder) vh;
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/AbstractMediaListHeaderPresenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/AbstractMediaListHeaderPresenter.java
index 5a0faba..82013a6 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/AbstractMediaListHeaderPresenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/AbstractMediaListHeaderPresenter.java
@@ -21,9 +21,10 @@
import android.view.ViewGroup;
import android.widget.TextView;
-import androidx.annotation.NonNull;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+
/**
* Abstract presenter class for rendering the header for a list of media items in a playlist.
* The presenter creates a {@link ViewHolder} for the TextView holding the header text.
@@ -105,7 +106,7 @@
}
@Override
- protected void onBindRowViewHolder(@NonNull RowPresenter.ViewHolder vh, @NonNull Object item) {
+ protected void onBindRowViewHolder(RowPresenter.@NonNull ViewHolder vh, @NonNull Object item) {
super.onBindRowViewHolder(vh, item);
onBindMediaListHeaderViewHolder((ViewHolder) vh, item);
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/Action.java b/leanback/leanback/src/main/java/androidx/leanback/widget/Action.java
index 33f49e2..2e7ede9 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/Action.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/Action.java
@@ -16,8 +16,8 @@
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
@@ -110,8 +110,7 @@
/**
* Returns the first line label for this Action.
*/
- @Nullable
- public final CharSequence getLabel1() {
+ public final @Nullable CharSequence getLabel1() {
return mLabel1;
}
@@ -125,8 +124,7 @@
/**
* Returns the second line label for this Action.
*/
- @Nullable
- public final CharSequence getLabel2() {
+ public final @Nullable CharSequence getLabel2() {
return mLabel2;
}
@@ -140,8 +138,7 @@
/**
* Returns the icon drawable for this Action.
*/
- @Nullable
- public final Drawable getIcon() {
+ public final @Nullable Drawable getIcon() {
return mIcon;
}
@@ -167,8 +164,7 @@
}
@Override
- @NonNull
- public String toString(){
+ public @NonNull String toString() {
StringBuilder sb = new StringBuilder();
if (!TextUtils.isEmpty(mLabel1)) {
sb.append(mLabel1);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/ActionPresenterSelector.java b/leanback/leanback/src/main/java/androidx/leanback/widget/ActionPresenterSelector.java
index 30235c0..98fba54 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/ActionPresenterSelector.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/ActionPresenterSelector.java
@@ -20,10 +20,11 @@
import android.view.ViewGroup;
import android.widget.Button;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
class ActionPresenterSelector extends PresenterSelector {
private final Presenter mOneLineActionPresenter = new OneLineActionPresenter();
@@ -61,7 +62,7 @@
abstract static class ActionPresenter extends Presenter {
@Override
public void onBindViewHolder(
- @NonNull Presenter.ViewHolder viewHolder,
+ Presenter.@NonNull ViewHolder viewHolder,
@Nullable Object item
) {
Action action = (Action) item;
@@ -87,7 +88,7 @@
}
@Override
- public void onUnbindViewHolder(@NonNull Presenter.ViewHolder viewHolder) {
+ public void onUnbindViewHolder(Presenter.@NonNull ViewHolder viewHolder) {
ActionViewHolder vh = (ActionViewHolder) viewHolder;
vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
vh.view.setPadding(0, 0, 0, 0);
@@ -96,9 +97,8 @@
}
static class OneLineActionPresenter extends ActionPresenter {
- @NonNull
@Override
- public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ public @NonNull ViewHolder onCreateViewHolder(ViewGroup parent) {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.lb_action_1_line, parent, false);
return new ActionViewHolder(v, parent.getLayoutDirection());
@@ -106,7 +106,7 @@
@Override
public void onBindViewHolder(
- @NonNull Presenter.ViewHolder viewHolder,
+ Presenter.@NonNull ViewHolder viewHolder,
@Nullable Object item
) {
super.onBindViewHolder(viewHolder, item);
@@ -117,9 +117,8 @@
}
static class TwoLineActionPresenter extends ActionPresenter {
- @NonNull
@Override
- public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ public @NonNull ViewHolder onCreateViewHolder(ViewGroup parent) {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.lb_action_2_lines, parent, false);
return new ActionViewHolder(v, parent.getLayoutDirection());
@@ -127,7 +126,7 @@
@Override
public void onBindViewHolder(
- @NonNull Presenter.ViewHolder viewHolder,
+ Presenter.@NonNull ViewHolder viewHolder,
@Nullable Object item
) {
super.onBindViewHolder(viewHolder, item);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/ArrayObjectAdapter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/ArrayObjectAdapter.java
index 38447fe..2a84485 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/ArrayObjectAdapter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/ArrayObjectAdapter.java
@@ -15,11 +15,12 @@
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListUpdateCallback;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -68,9 +69,8 @@
return mItems.size();
}
- @Nullable
@Override
- public Object get(int index) {
+ public @Nullable Object get(int index) {
return mItems.get(index);
}
@@ -215,9 +215,8 @@
/**
* Gets a read-only view of the list of object of this ArrayObjectAdapter.
*/
- @NonNull
@SuppressWarnings("unchecked")
- public <E> List<E> unmodifiableList() {
+ public <E> @NonNull List<E> unmodifiableList() {
// The mUnmodifiableItems will only be created once as long as the content of mItems has not
// been changed.
@@ -280,9 +279,8 @@
itemList.get(newItemPosition));
}
- @Nullable
@Override
- public Object getChangePayload(int oldItemPosition, int newItemPosition) {
+ public @Nullable Object getChangePayload(int oldItemPosition, int newItemPosition) {
return callback.getChangePayload(mOldItems.get(oldItemPosition),
itemList.get(newItemPosition));
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/BrowseFrameLayout.java b/leanback/leanback/src/main/java/androidx/leanback/widget/BrowseFrameLayout.java
index 95a1f3f..6d25e00 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/BrowseFrameLayout.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/BrowseFrameLayout.java
@@ -20,8 +20,8 @@
import android.view.View;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* A ViewGroup for managing focus behavior between overlapping views.
@@ -37,8 +37,7 @@
* Returns the view where focus should be requested given the current focused view and
* the direction of focus search.
*/
- @Nullable
- View onFocusSearch(@Nullable View focused, int direction);
+ @Nullable View onFocusSearch(@Nullable View focused, int direction);
}
/**
@@ -86,8 +85,7 @@
/**
* Returns the {@link OnFocusSearchListener}.
*/
- @Nullable
- public OnFocusSearchListener getOnFocusSearchListener() {
+ public @Nullable OnFocusSearchListener getOnFocusSearchListener() {
return mListener;
}
@@ -101,8 +99,7 @@
/**
* Returns the {@link OnChildFocusListener}.
*/
- @Nullable
- public OnChildFocusListener getOnChildFocusListener() {
+ public @Nullable OnChildFocusListener getOnChildFocusListener() {
return mOnChildFocusListener;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/ClassPresenterSelector.java b/leanback/leanback/src/main/java/androidx/leanback/widget/ClassPresenterSelector.java
index 5402396..9d0dd39 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/ClassPresenterSelector.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/ClassPresenterSelector.java
@@ -13,7 +13,7 @@
*/
package androidx.leanback.widget;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
@@ -61,9 +61,8 @@
return this;
}
- @Nullable
@Override
- public Presenter getPresenter(@Nullable Object item) {
+ public @Nullable Presenter getPresenter(@Nullable Object item) {
if (item == null) {
return null;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/ControlBarPresenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/ControlBarPresenter.java
index 1090d9e..37f29a2 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/ControlBarPresenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/ControlBarPresenter.java
@@ -19,10 +19,11 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* A presenter that assumes a LinearLayout container for a series
* of control buttons backed by objects of type {@link Action}.
@@ -239,16 +240,15 @@
vh.mControlsContainer.setBackgroundColor(color);
}
- @NonNull
@Override
- public Presenter.ViewHolder onCreateViewHolder(ViewGroup parent) {
+ public Presenter.@NonNull ViewHolder onCreateViewHolder(ViewGroup parent) {
View v = LayoutInflater.from(parent.getContext())
.inflate(getLayoutResourceId(), parent, false);
return new ViewHolder(v);
}
@Override
- public void onBindViewHolder(@NonNull Presenter.ViewHolder holder, @Nullable Object item) {
+ public void onBindViewHolder(Presenter.@NonNull ViewHolder holder, @Nullable Object item) {
ViewHolder vh = (ViewHolder) holder;
BoundData data = (BoundData) item;
if (vh.mAdapter != data.adapter) {
@@ -263,7 +263,7 @@
}
@Override
- public void onUnbindViewHolder(@NonNull Presenter.ViewHolder holder) {
+ public void onUnbindViewHolder(Presenter.@NonNull ViewHolder holder) {
ViewHolder vh = (ViewHolder) holder;
if (vh.mAdapter != null) {
vh.mAdapter.unregisterObserver(vh.mDataObserver);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/ControlButtonPresenterSelector.java b/leanback/leanback/src/main/java/androidx/leanback/widget/ControlButtonPresenterSelector.java
index 468b05b..7100070 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/ControlButtonPresenterSelector.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/ControlButtonPresenterSelector.java
@@ -21,10 +21,11 @@
import android.widget.ImageView;
import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Displays primary and secondary controls for a {@link PlaybackControlsRow}.
*
@@ -55,9 +56,8 @@
/**
* Always returns the presenter for primary controls.
*/
- @Nullable
@Override
- public Presenter getPresenter(@Nullable Object item) {
+ public @Nullable Presenter getPresenter(@Nullable Object item) {
return mPrimaryPresenter;
}
@@ -86,9 +86,8 @@
mLayoutResourceId = layoutResourceId;
}
- @NonNull
@Override
- public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ public @NonNull ViewHolder onCreateViewHolder(ViewGroup parent) {
View v = LayoutInflater.from(parent.getContext())
.inflate(mLayoutResourceId, parent, false);
return new ActionViewHolder(v);
@@ -96,7 +95,7 @@
@Override
public void onBindViewHolder(
- @NonNull Presenter.ViewHolder viewHolder,
+ Presenter.@NonNull ViewHolder viewHolder,
@Nullable Object item
) {
Action action = (Action) item;
@@ -119,7 +118,7 @@
}
@Override
- public void onUnbindViewHolder(@NonNull Presenter.ViewHolder viewHolder) {
+ public void onUnbindViewHolder(Presenter.@NonNull ViewHolder viewHolder) {
ActionViewHolder vh = (ActionViewHolder) viewHolder;
vh.mIcon.setImageDrawable(null);
if (vh.mLabel != null) {
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/CursorObjectAdapter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/CursorObjectAdapter.java
index 170de9d..ce8758d 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/CursorObjectAdapter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/CursorObjectAdapter.java
@@ -16,9 +16,10 @@
import android.database.Cursor;
import android.util.LruCache;
-import androidx.annotation.Nullable;
import androidx.leanback.database.CursorMapper;
+import org.jspecify.annotations.Nullable;
+
/**
* An {@link ObjectAdapter} implemented with a {@link Cursor}.
*/
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsOverviewLogoPresenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsOverviewLogoPresenter.java
index 3006565..3aca12f 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsOverviewLogoPresenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsOverviewLogoPresenter.java
@@ -5,10 +5,11 @@
import android.view.ViewGroup;
import android.widget.ImageView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Presenter that responsible to create a ImageView and bind to DetailsOverviewRow. The default
* implementation uses {@link DetailsOverviewRow#getImageDrawable()} and binds to {@link ImageView}.
@@ -31,23 +32,19 @@
*/
public static class ViewHolder extends Presenter.ViewHolder {
- @Nullable
- protected FullWidthDetailsOverviewRowPresenter mParentPresenter;
- @Nullable
- protected FullWidthDetailsOverviewRowPresenter.ViewHolder mParentViewHolder;
+ protected @Nullable FullWidthDetailsOverviewRowPresenter mParentPresenter;
+ protected FullWidthDetailsOverviewRowPresenter.@Nullable ViewHolder mParentViewHolder;
private boolean mSizeFromDrawableIntrinsic;
public ViewHolder(@NonNull View view) {
super(view);
}
- @Nullable
- public FullWidthDetailsOverviewRowPresenter getParentPresenter() {
+ public @Nullable FullWidthDetailsOverviewRowPresenter getParentPresenter() {
return mParentPresenter;
}
- @Nullable
- public FullWidthDetailsOverviewRowPresenter.ViewHolder getParentViewHolder() {
+ public FullWidthDetailsOverviewRowPresenter.@Nullable ViewHolder getParentViewHolder() {
return mParentViewHolder;
}
@@ -90,15 +87,13 @@
* @param parent Parent view.
* @return View created for the logo.
*/
- @NonNull
- public View onCreateView(@NonNull ViewGroup parent) {
+ public @NonNull View onCreateView(@NonNull ViewGroup parent) {
return LayoutInflater.from(parent.getContext())
.inflate(R.layout.lb_fullwidth_details_overview_logo, parent, false);
}
- @NonNull
@Override
- public Presenter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
+ public Presenter.@NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
View view = onCreateView(parent);
ViewHolder vh = new ViewHolder(view);
ViewGroup.LayoutParams lp = view.getLayoutParams();
@@ -115,7 +110,7 @@
* @param parentPresenter
*/
public void setContext(@NonNull ViewHolder viewHolder,
- @Nullable FullWidthDetailsOverviewRowPresenter.ViewHolder parentViewHolder,
+ FullWidthDetailsOverviewRowPresenter.@Nullable ViewHolder parentViewHolder,
@Nullable FullWidthDetailsOverviewRowPresenter parentPresenter) {
viewHolder.mParentViewHolder = parentViewHolder;
viewHolder.mParentPresenter = parentPresenter;
@@ -144,7 +139,7 @@
* @param item DetailsOverviewRow object to bind.
*/
@Override
- public void onBindViewHolder(@NonNull Presenter.ViewHolder viewHolder, @Nullable Object item) {
+ public void onBindViewHolder(Presenter.@NonNull ViewHolder viewHolder, @Nullable Object item) {
DetailsOverviewRow row = (DetailsOverviewRow) item;
ImageView imageView = ((ImageView) viewHolder.view);
imageView.setImageDrawable(row.getImageDrawable());
@@ -178,7 +173,7 @@
}
@Override
- public void onUnbindViewHolder(@NonNull Presenter.ViewHolder viewHolder) {
+ public void onUnbindViewHolder(Presenter.@NonNull ViewHolder viewHolder) {
}
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsOverviewRow.java b/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsOverviewRow.java
index 68bcf18..85b553f 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsOverviewRow.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsOverviewRow.java
@@ -18,8 +18,8 @@
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@@ -185,8 +185,7 @@
/**
* Returns the main item for the details page.
*/
- @NonNull
- public final Object getItem() {
+ public final @NonNull Object getItem() {
return mItem;
}
@@ -232,8 +231,7 @@
* @return The overview's image drawable, or null if no drawable has been
* assigned.
*/
- @Nullable
- public final Drawable getImageDrawable() {
+ public final @Nullable Drawable getImageDrawable() {
return mImageDrawable;
}
@@ -319,8 +317,7 @@
/**
* Returns the {@link ObjectAdapter} for actions.
*/
- @NonNull
- public final ObjectAdapter getActionsAdapter() {
+ public final @NonNull ObjectAdapter getActionsAdapter() {
return mActionsAdapter;
}
@@ -343,8 +340,7 @@
/**
* Returns the Action associated with the given keycode, or null if no associated action exists.
*/
- @Nullable
- public Action getActionForKeyCode(int keyCode) {
+ public @Nullable Action getActionForKeyCode(int keyCode) {
ObjectAdapter adapter = getActionsAdapter();
if (adapter != null) {
for (int i = 0; i < adapter.size(); i++) {
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsOverviewRowPresenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsOverviewRowPresenter.java
index 60cdfa6..e8d3603 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsOverviewRowPresenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsOverviewRowPresenter.java
@@ -29,11 +29,12 @@
import android.widget.ImageView;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Renders a {@link DetailsOverviewRow} to display an overview of an item.
* Typically this row will be the first row in a fragment
@@ -552,7 +553,7 @@
@Override
protected void onBindRowViewHolder(
- @NonNull RowPresenter.ViewHolder holder,
+ RowPresenter.@NonNull ViewHolder holder,
@NonNull Object item
) {
super.onBindRowViewHolder(holder, item);
@@ -567,7 +568,7 @@
}
@Override
- protected void onUnbindRowViewHolder(@NonNull RowPresenter.ViewHolder holder) {
+ protected void onUnbindRowViewHolder(RowPresenter.@NonNull ViewHolder holder) {
ViewHolder vh = (ViewHolder) holder;
DetailsOverviewRow dor = (DetailsOverviewRow) vh.getRow();
dor.removeListener(vh.mListener);
@@ -594,7 +595,7 @@
}
@Override
- protected void onRowViewAttachedToWindow(@NonNull RowPresenter.ViewHolder vh) {
+ protected void onRowViewAttachedToWindow(RowPresenter.@NonNull ViewHolder vh) {
super.onRowViewAttachedToWindow(vh);
if (mDetailsPresenter != null) {
mDetailsPresenter.onViewAttachedToWindow(
@@ -603,7 +604,7 @@
}
@Override
- protected void onRowViewDetachedFromWindow(@NonNull RowPresenter.ViewHolder vh) {
+ protected void onRowViewDetachedFromWindow(RowPresenter.@NonNull ViewHolder vh) {
super.onRowViewDetachedFromWindow(vh);
if (mDetailsPresenter != null) {
mDetailsPresenter.onViewDetachedFromWindow(
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsParallaxDrawable.java b/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsParallaxDrawable.java
index 02c2421..ab4880e 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsParallaxDrawable.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsParallaxDrawable.java
@@ -24,12 +24,13 @@
import android.util.TypedValue;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.leanback.R;
import androidx.leanback.graphics.CompositeDrawable;
import androidx.leanback.graphics.FitWidthBitmapDrawable;
+import org.jspecify.annotations.NonNull;
+
/**
* Helper class responsible for wiring in parallax effect in
* {@link androidx.leanback.app.DetailsFragment}. The default effect will render
@@ -146,16 +147,14 @@
/**
* @return First child which is cover drawable appearing at top.
*/
- @NonNull
- public Drawable getCoverDrawable() {
+ public @NonNull Drawable getCoverDrawable() {
return getChildAt(0).getDrawable();
}
/**
* @return Second child which is ColorDrawable by default.
*/
- @NonNull
- public Drawable getBottomDrawable() {
+ public @NonNull Drawable getBottomDrawable() {
return mBottomDrawable;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/DiffCallback.java b/leanback/leanback/src/main/java/androidx/leanback/widget/DiffCallback.java
index 2d36342..d648454 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/DiffCallback.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/DiffCallback.java
@@ -16,10 +16,11 @@
package androidx.leanback.widget;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.List;
/**
@@ -63,8 +64,7 @@
* @see DiffUtil.Callback#getChangePayload(int, int)
*/
@SuppressWarnings("WeakerAccess")
- @Nullable
- public Object getChangePayload(@NonNull Value oldItem, @NonNull Value newItem) {
+ public @Nullable Object getChangePayload(@NonNull Value oldItem, @NonNull Value newItem) {
return null;
}
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/DividerPresenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/DividerPresenter.java
index 6479739..dd405aa 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/DividerPresenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/DividerPresenter.java
@@ -19,11 +19,12 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* DividerPresenter provides a default presentation for {@link DividerRow} in HeadersFragment.
*/
@@ -42,9 +43,8 @@
mLayoutResourceId = layoutResourceId;
}
- @NonNull
@Override
- public Presenter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
+ public Presenter.@NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
View headerView = LayoutInflater.from(parent.getContext())
.inflate(mLayoutResourceId, parent, false);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/FragmentAnimationProvider.java b/leanback/leanback/src/main/java/androidx/leanback/widget/FragmentAnimationProvider.java
index 8aebed2..08c51a0 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/FragmentAnimationProvider.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/FragmentAnimationProvider.java
@@ -15,7 +15,7 @@
import android.animation.Animator;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
import java.util.List;
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/FullWidthDetailsOverviewRowPresenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/FullWidthDetailsOverviewRowPresenter.java
index f24979f..b40d809 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/FullWidthDetailsOverviewRowPresenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/FullWidthDetailsOverviewRowPresenter.java
@@ -25,11 +25,12 @@
import android.view.ViewGroup.MarginLayoutParams;
import android.widget.FrameLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Renders a {@link DetailsOverviewRow} to display an overview of an item. Typically this row will
* be the first row in a fragment such as the
@@ -576,7 +577,7 @@
@Override
protected void onBindRowViewHolder(
- @NonNull RowPresenter.ViewHolder holder,
+ RowPresenter.@NonNull ViewHolder holder,
@NonNull Object item
) {
super.onBindRowViewHolder(holder, item);
@@ -590,7 +591,7 @@
}
@Override
- protected void onUnbindRowViewHolder(@NonNull RowPresenter.ViewHolder holder) {
+ protected void onUnbindRowViewHolder(RowPresenter.@NonNull ViewHolder holder) {
ViewHolder vh = (ViewHolder) holder;
vh.onUnbind();
mDetailsPresenter.onUnbindViewHolder(vh.mDetailsDescriptionViewHolder);
@@ -614,7 +615,7 @@
}
@Override
- protected void onRowViewAttachedToWindow(@NonNull RowPresenter.ViewHolder vh) {
+ protected void onRowViewAttachedToWindow(RowPresenter.@NonNull ViewHolder vh) {
super.onRowViewAttachedToWindow(vh);
ViewHolder viewHolder = (ViewHolder) vh;
mDetailsPresenter.onViewAttachedToWindow(viewHolder.mDetailsDescriptionViewHolder);
@@ -622,7 +623,7 @@
}
@Override
- protected void onRowViewDetachedFromWindow(@NonNull RowPresenter.ViewHolder vh) {
+ protected void onRowViewDetachedFromWindow(RowPresenter.@NonNull ViewHolder vh) {
super.onRowViewDetachedFromWindow(vh);
ViewHolder viewHolder = (ViewHolder) vh;
mDetailsPresenter.onViewDetachedFromWindow(viewHolder.mDetailsDescriptionViewHolder);
@@ -779,7 +780,7 @@
}
@Override
- public void setEntranceTransitionState(@NonNull RowPresenter.ViewHolder holder,
+ public void setEntranceTransitionState(RowPresenter.@NonNull ViewHolder holder,
boolean afterEntrance) {
super.setEntranceTransitionState(holder, afterEntrance);
if (mParticipatingEntranceTransition) {
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidanceStylist.java b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidanceStylist.java
index ebe9ab3..d3430a3 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidanceStylist.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidanceStylist.java
@@ -22,10 +22,11 @@
import android.widget.ImageView;
import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.List;
/**
@@ -106,8 +107,7 @@
* Returns the title specified when this Guidance was constructed.
* @return The title for this Guidance.
*/
- @Nullable
- public String getTitle() {
+ public @Nullable String getTitle() {
return mTitle;
}
@@ -115,8 +115,7 @@
* Returns the description specified when this Guidance was constructed.
* @return The description for this Guidance.
*/
- @Nullable
- public String getDescription() {
+ public @Nullable String getDescription() {
return mDescription;
}
@@ -124,8 +123,7 @@
* Returns the breadcrumb specified when this Guidance was constructed.
* @return The breadcrumb for this Guidance.
*/
- @Nullable
- public String getBreadcrumb() {
+ public @Nullable String getBreadcrumb() {
return mBreadcrumb;
}
@@ -133,8 +131,7 @@
* Returns the icon drawable specified when this Guidance was constructed.
* @return The icon for this Guidance.
*/
- @Nullable
- public Drawable getIconDrawable() {
+ public @Nullable Drawable getIconDrawable() {
return mIconDrawable;
}
}
@@ -157,8 +154,7 @@
* @param guidance The guidance data for the view.
* @return The view to be added to the caller's view hierarchy.
*/
- @NonNull
- public View onCreateView(
+ public @NonNull View onCreateView(
final @NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@NonNull Guidance guidance
@@ -239,8 +235,7 @@
* Returns the view displaying the title of the guidance.
* @return The text view object for the title.
*/
- @Nullable
- public TextView getTitleView() {
+ public @Nullable TextView getTitleView() {
return mTitleView;
}
@@ -248,8 +243,7 @@
* Returns the view displaying the description of the guidance.
* @return The text view object for the description.
*/
- @Nullable
- public TextView getDescriptionView() {
+ public @Nullable TextView getDescriptionView() {
return mDescriptionView;
}
@@ -257,8 +251,7 @@
* Returns the view displaying the breadcrumb of the guidance.
* @return The text view object for the breadcrumb.
*/
- @Nullable
- public TextView getBreadcrumbView() {
+ public @Nullable TextView getBreadcrumbView() {
return mBreadcrumbView;
}
@@ -266,8 +259,7 @@
* Returns the view displaying the icon of the guidance.
* @return The image view object for the icon.
*/
- @Nullable
- public ImageView getIconView() {
+ public @Nullable ImageView getIconView() {
return mIconView;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedAction.java b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedAction.java
index bb05177..7595eb9 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedAction.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedAction.java
@@ -21,12 +21,13 @@
import android.text.InputType;
import androidx.annotation.DrawableRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.List;
/**
@@ -154,8 +155,7 @@
* Returns Context of this Builder.
* @return Context of this Builder.
*/
- @NonNull
- public Context getContext() {
+ public @NonNull Context getContext() {
return mContext;
}
@@ -556,7 +556,7 @@
* @param hints List of hints for autofill.
* @return The same BuilderBase object.
*/
- public B autofillHints(@Nullable String... hints) {
+ public B autofillHints(String @Nullable ... hints) {
mAutofillHints = hints;
return (B) this;
}
@@ -587,8 +587,7 @@
* Builds the GuidedAction corresponding to this Builder.
* @return The GuidedAction as configured through this Builder.
*/
- @NonNull
- public GuidedAction build() {
+ public @NonNull GuidedAction build() {
GuidedAction action = new GuidedAction();
applyValues(action);
return action;
@@ -632,8 +631,7 @@
* Returns the title of this action.
* @return The title set when this action was built.
*/
- @Nullable
- public CharSequence getTitle() {
+ public @Nullable CharSequence getTitle() {
return getLabel1();
}
@@ -650,8 +648,7 @@
* {@link #getTitle()}.
* @return Optional title text to edit instead of {@link #getTitle()}.
*/
- @Nullable
- public CharSequence getEditTitle() {
+ public @Nullable CharSequence getEditTitle() {
return mEditTitle;
}
@@ -668,8 +665,7 @@
* {@link #getDescription()}.
* @return Optional description text to edit instead of {@link #getDescription()}.
*/
- @Nullable
- public CharSequence getEditDescription() {
+ public @Nullable CharSequence getEditDescription() {
return mEditDescription;
}
@@ -695,8 +691,7 @@
* Returns the description of this action.
* @return The description of this action.
*/
- @Nullable
- public CharSequence getDescription() {
+ public @Nullable CharSequence getDescription() {
return getLabel2();
}
@@ -712,8 +707,7 @@
* Returns the intent associated with this action.
* @return The intent set when this action was built.
*/
- @Nullable
- public Intent getIntent() {
+ public @Nullable Intent getIntent() {
return mIntent;
}
@@ -899,8 +893,7 @@
* @return List of sub actions or null if sub actions list is not enabled.
*/
@SuppressLint("NullableCollection")
- @Nullable
- public List<GuidedAction> getSubActions() {
+ public @Nullable List<GuidedAction> getSubActions() {
return mSubActions;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionAdapter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionAdapter.java
index 01ed098..cca2a79 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionAdapter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionAdapter.java
@@ -25,13 +25,14 @@
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.List;
@@ -212,9 +213,8 @@
mActions.get(newItemPosition));
}
- @Nullable
@Override
- public Object getChangePayload(int oldItemPosition, int newItemPosition) {
+ public @Nullable Object getChangePayload(int oldItemPosition, int newItemPosition) {
return mDiffCallback.getChangePayload(oldActions.get(oldItemPosition),
mActions.get(newItemPosition));
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionAdapterGroup.java b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionAdapterGroup.java
index 57dcece..238facd 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionAdapterGroup.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionAdapterGroup.java
@@ -22,11 +22,12 @@
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.leanback.widget.GuidedActionAdapter.EditListener;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
/**
@@ -57,8 +58,7 @@
}
}
- @Nullable
- public GuidedActionAdapter getNextAdapter(@NonNull GuidedActionAdapter adapter) {
+ public @Nullable GuidedActionAdapter getNextAdapter(@NonNull GuidedActionAdapter adapter) {
for (int i = 0; i < mAdapters.size(); i++) {
Pair<GuidedActionAdapter, GuidedActionAdapter> pair = mAdapters.get(i);
if (pair.first == adapter) {
@@ -126,7 +126,7 @@
public void openIme(
@NonNull GuidedActionAdapter adapter,
- @NonNull GuidedActionsStylist.ViewHolder avh
+ GuidedActionsStylist.@NonNull ViewHolder avh
) {
adapter.getGuidedActionsStylist().setEditingMode(avh, true);
View v = avh.getEditingView();
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionAppCompatEditText.java b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionAppCompatEditText.java
index 0fc46c2..bcb5853 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionAppCompatEditText.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionAppCompatEditText.java
@@ -27,12 +27,13 @@
import android.view.autofill.AutofillValue;
import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.widget.AppCompatEditText;
import androidx.core.widget.TextViewCompat;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* A custom EditText that satisfies the IME key monitoring requirements of GuidedStepFragment.
*/
@@ -127,7 +128,7 @@
*/
@Override
public void setCustomSelectionActionModeCallback(
- @Nullable ActionMode.Callback actionModeCallback) {
+ ActionMode.@Nullable Callback actionModeCallback) {
super.setCustomSelectionActionModeCallback(TextViewCompat
.wrapCustomSelectionActionModeCallback(this, actionModeCallback));
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionDiffCallback.java b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionDiffCallback.java
index 2c56a80..aefe2ac 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionDiffCallback.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionDiffCallback.java
@@ -17,7 +17,7 @@
import android.text.TextUtils;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* DiffCallback used for GuidedActions, see {@link
@@ -31,8 +31,7 @@
* Returns the singleton GuidedActionDiffCallback.
* @return The singleton GuidedActionDiffCallback.
*/
- @NonNull
- public static GuidedActionDiffCallback getInstance() {
+ public static @NonNull GuidedActionDiffCallback getInstance() {
return sInstance;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionEditText.java b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionEditText.java
index 49606dc..766b1f2 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionEditText.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionEditText.java
@@ -29,10 +29,11 @@
import android.widget.EditText;
import android.widget.TextView;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.widget.TextViewCompat;
+import org.jspecify.annotations.NonNull;
+
/**
* A custom EditText that satisfies the IME key monitoring requirements of GuidedStepFragment.
*/
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionsStylist.java b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionsStylist.java
index 469d0fd..7a88493 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionsStylist.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionsStylist.java
@@ -48,8 +48,6 @@
import android.widget.TextView;
import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.content.ContextCompat;
@@ -61,6 +59,9 @@
import androidx.leanback.widget.picker.DatePicker;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
@@ -240,16 +241,14 @@
* Returns the content view within this view holder's view, where title and description are
* shown.
*/
- @Nullable
- public View getContentView() {
+ public @Nullable View getContentView() {
return mContentView;
}
/**
* Returns the title view within this view holder's view.
*/
- @Nullable
- public TextView getTitleView() {
+ public @Nullable TextView getTitleView() {
return mTitleView;
}
@@ -257,16 +256,14 @@
* Convenience method to return an editable version of the title, if possible,
* or null if the title view isn't an EditText.
*/
- @Nullable
- public EditText getEditableTitleView() {
+ public @Nullable EditText getEditableTitleView() {
return (mTitleView instanceof EditText) ? (EditText)mTitleView : null;
}
/**
* Returns the description view within this view holder's view.
*/
- @Nullable
- public TextView getDescriptionView() {
+ public @Nullable TextView getDescriptionView() {
return mDescriptionView;
}
@@ -274,32 +271,28 @@
* Convenience method to return an editable version of the description, if possible,
* or null if the description view isn't an EditText.
*/
- @Nullable
- public EditText getEditableDescriptionView() {
+ public @Nullable EditText getEditableDescriptionView() {
return (mDescriptionView instanceof EditText) ? (EditText)mDescriptionView : null;
}
/**
* Returns the icon view within this view holder's view.
*/
- @Nullable
- public ImageView getIconView() {
+ public @Nullable ImageView getIconView() {
return mIconView;
}
/**
* Returns the checkmark view within this view holder's view.
*/
- @Nullable
- public ImageView getCheckmarkView() {
+ public @Nullable ImageView getCheckmarkView() {
return mCheckmarkView;
}
/**
* Returns the chevron view within this view holder's view.
*/
- @Nullable
- public ImageView getChevronView() {
+ public @Nullable ImageView getChevronView() {
return mChevronView;
}
@@ -344,8 +337,7 @@
* @return Current editing title view or description view or activator view or null if not
* in editing.
*/
- @Nullable
- public View getEditingView() {
+ public @Nullable View getEditingView() {
switch(mEditingMode) {
case EDITING_TITLE:
return mTitleView;
@@ -370,8 +362,7 @@
/**
* @return Currently bound action.
*/
- @Nullable
- public GuidedAction getAction() {
+ public @Nullable GuidedAction getAction() {
return mAction;
}
@@ -382,9 +373,8 @@
}
}
- @Nullable
@Override
- public Object getFacet(@NonNull Class<?> facetClass) {
+ public @Nullable Object getFacet(@NonNull Class<?> facetClass) {
if (facetClass == ItemAlignmentFacet.class) {
return sGuidedActionItemAlignFacet;
}
@@ -457,9 +447,9 @@
* <code>LayoutInflater.inflate</code>.
* @return The view to be added to the caller's view hierarchy.
*/
- @NonNull
@SuppressWarnings("deprecation") /* defaultDisplay */
- public View onCreateView(@NonNull LayoutInflater inflater, final @NonNull ViewGroup container) {
+ public @NonNull View onCreateView(@NonNull LayoutInflater inflater,
+ final @NonNull ViewGroup container) {
TypedArray ta = inflater.getContext().getTheme().obtainStyledAttributes(
R.styleable.LeanbackGuidedStepTheme);
float keylinePercent = ta.getFloat(R.styleable.LeanbackGuidedStepTheme_guidedStepKeyline,
@@ -569,8 +559,7 @@
* Returns the VerticalGridView that displays the list of GuidedActions.
* @return The VerticalGridView for this presenter.
*/
- @Nullable
- public VerticalGridView getActionsGridView() {
+ public @Nullable VerticalGridView getActionsGridView() {
return mActionsGridView;
}
@@ -578,8 +567,7 @@
* Returns the VerticalGridView that displays the sub actions list of an expanded action.
* @return The VerticalGridView that displays the sub actions list of an expanded action.
*/
- @Nullable
- public VerticalGridView getSubActionsGridView() {
+ public @Nullable VerticalGridView getSubActionsGridView() {
return mSubActionsGridView;
}
@@ -669,8 +657,7 @@
* @param parent The view group to be used as the parent of the new view.
* @return The view to be added to the caller's view hierarchy.
*/
- @NonNull
- public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
+ public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View v = inflater.inflate(onProvideItemLayoutId(), parent, false);
return new ViewHolder(v, parent == mSubActionsGridView);
@@ -686,8 +673,7 @@
* @param viewType The viewType returned by {@link #getItemViewType(GuidedAction)}
* @return The view to be added to the caller's view hierarchy.
*/
- @NonNull
- public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == VIEW_TYPE_DEFAULT) {
return onCreateViewHolder(parent);
}
@@ -797,7 +783,7 @@
getActionsGridView().setSelectedPosition(actionIndex, new ViewHolderTask() {
@Override
- public void run(@NonNull RecyclerView.ViewHolder viewHolder) {
+ public void run(RecyclerView.@NonNull ViewHolder viewHolder) {
ViewHolder vh = (ViewHolder) viewHolder;
guidedActionAdapter.mGroup.openIme(guidedActionAdapter, vh);
}
@@ -1408,8 +1394,7 @@
/**
* @return Current expanded GuidedAction or null if not expanded.
*/
- @Nullable
- public GuidedAction getExpandedAction() {
+ public @Nullable GuidedAction getExpandedAction() {
return mExpandedAction;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedDatePickerAction.java b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedDatePickerAction.java
index 39b0708..e4f20ee 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedDatePickerAction.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedDatePickerAction.java
@@ -16,8 +16,8 @@
import android.content.Context;
import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import java.util.Calendar;
import java.util.TimeZone;
@@ -123,8 +123,7 @@
* Builds the GuidedDatePickerAction corresponding to this Builder.
* @return The GuidedDatePickerAction as configured through this Builder.
*/
- @NonNull
- public GuidedDatePickerAction build() {
+ public @NonNull GuidedDatePickerAction build() {
GuidedDatePickerAction action = new GuidedDatePickerAction();
applyDatePickerValues(action);
return action;
@@ -143,8 +142,7 @@
* be used.
* @return Format of showing Date, e.g. "YMD". Returns null if using current locale's default.
*/
- @Nullable
- public String getDatePickerFormat() {
+ public @Nullable String getDatePickerFormat() {
return mDatePickerFormat;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/ImageCardView.java b/leanback/leanback/src/main/java/androidx/leanback/widget/ImageCardView.java
index af5b59a..dc07239 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/ImageCardView.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/ImageCardView.java
@@ -29,11 +29,12 @@
import android.widget.TextView;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* A subclass of {@link BaseCardView} with an {@link ImageView} as its main region. The
* {@link ImageCardView} is highly customizable and can be used for various use-cases by adjusting
@@ -302,8 +303,7 @@
/**
* Returns the main image view.
*/
- @Nullable
- public final ImageView getMainImageView() {
+ public final @Nullable ImageView getMainImageView() {
return mImageView;
}
@@ -369,8 +369,7 @@
/**
* Returns the ImageView drawable.
*/
- @Nullable
- public Drawable getMainImage() {
+ public @Nullable Drawable getMainImage() {
if (mImageView == null) {
return null;
}
@@ -381,8 +380,7 @@
/**
* Returns the info area background drawable.
*/
- @Nullable
- public Drawable getInfoAreaBackground() {
+ public @Nullable Drawable getInfoAreaBackground() {
if (mInfoArea != null) {
return mInfoArea.getBackground();
}
@@ -420,8 +418,7 @@
/**
* Returns the title text.
*/
- @Nullable
- public CharSequence getTitleText() {
+ public @Nullable CharSequence getTitleText() {
if (mTitleView == null) {
return null;
}
@@ -442,8 +439,7 @@
/**
* Returns the content text.
*/
- @Nullable
- public CharSequence getContentText() {
+ public @Nullable CharSequence getContentText() {
if (mContentView == null) {
return null;
}
@@ -469,8 +465,7 @@
/**
* Returns the badge image drawable.
*/
- @Nullable
- public Drawable getBadgeImage() {
+ public @Nullable Drawable getBadgeImage() {
if (mBadgeImage == null) {
return null;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/LeanbackAppCompatViewInflater.java b/leanback/leanback/src/main/java/androidx/leanback/widget/LeanbackAppCompatViewInflater.java
index 1993c8d..cf4fe64 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/LeanbackAppCompatViewInflater.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/LeanbackAppCompatViewInflater.java
@@ -20,16 +20,16 @@
import android.util.AttributeSet;
import android.view.View;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatViewInflater;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/** Inflater that converts leanback non-AppCpmpat views in layout to AppCompat versions. */
public class LeanbackAppCompatViewInflater extends AppCompatViewInflater {
@Override
- @NonNull
- protected View createView(@Nullable Context context, @Nullable String name,
+ protected @NonNull View createView(@Nullable Context context, @Nullable String name,
@Nullable AttributeSet attrs) {
switch (name) {
case "androidx.leanback.widget.GuidedActionEditText":
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/ListRowPresenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/ListRowPresenter.java
index 1a3a86f..f38a411 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/ListRowPresenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/ListRowPresenter.java
@@ -20,13 +20,14 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
import androidx.leanback.system.Settings;
import androidx.leanback.transition.TransitionHelper;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.HashMap;
/**
@@ -87,8 +88,7 @@
* Gets ListRowPresenter that creates this ViewHolder.
* @return ListRowPresenter that creates this ViewHolder.
*/
- @NonNull
- public final ListRowPresenter getListRowPresenter() {
+ public final @NonNull ListRowPresenter getListRowPresenter() {
return mListRowPresenter;
}
@@ -96,8 +96,7 @@
* Gets HorizontalGridView that shows a list of items.
* @return HorizontalGridView that shows a list of items.
*/
- @NonNull
- public final HorizontalGridView getGridView() {
+ public final @NonNull HorizontalGridView getGridView() {
return mGridView;
}
@@ -105,8 +104,7 @@
* Gets ItemBridgeAdapter that creates the list of items.
* @return ItemBridgeAdapter that creates the list of items.
*/
- @NonNull
- public final ItemBridgeAdapter getBridgeAdapter() {
+ public final @NonNull ItemBridgeAdapter getBridgeAdapter() {
return mItemBridgeAdapter;
}
@@ -124,8 +122,7 @@
* @param position Position of the item in adapter.
* @return ViewHolder bounds to the item.
*/
- @Nullable
- public Presenter.ViewHolder getItemViewHolder(int position) {
+ public Presenter.@Nullable ViewHolder getItemViewHolder(int position) {
ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder) mGridView
.findViewHolderForAdapterPosition(position);
if (ibvh == null) {
@@ -134,15 +131,13 @@
return ibvh.getViewHolder();
}
- @Nullable
@Override
- public Presenter.ViewHolder getSelectedItemViewHolder() {
+ public Presenter.@Nullable ViewHolder getSelectedItemViewHolder() {
return getItemViewHolder(getSelectedPosition());
}
- @Nullable
@Override
- public Object getSelectedItem() {
+ public @Nullable Object getSelectedItem() {
ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder) mGridView
.findViewHolderForAdapterPosition(getSelectedPosition());
if (ibvh == null) {
@@ -203,8 +198,7 @@
* Returns optional task to run when the item is selected, null for no task.
* @return Optional task to run when the item is selected, null for no task.
*/
- @Nullable
- public Presenter.ViewHolderTask getItemTask() {
+ public Presenter.@Nullable ViewHolderTask getItemTask() {
return mItemTask;
}
@@ -217,7 +211,7 @@
}
@Override
- public void run(@Nullable Presenter.ViewHolder holder) {
+ public void run(Presenter.@Nullable ViewHolder holder) {
if (holder instanceof ListRowPresenter.ViewHolder) {
HorizontalGridView gridView = ((ListRowPresenter.ViewHolder) holder).getGridView();
androidx.leanback.widget.ViewHolderTask task = null;
@@ -225,7 +219,7 @@
task = new androidx.leanback.widget.ViewHolderTask() {
final Presenter.ViewHolderTask itemTask = mItemTask;
@Override
- public void run(@NonNull RecyclerView.ViewHolder rvh) {
+ public void run(RecyclerView.@NonNull ViewHolder rvh) {
ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder) rvh;
itemTask.run(ibvh.getViewHolder());
}
@@ -673,7 +667,7 @@
@Override
protected void onBindRowViewHolder(
- @NonNull RowPresenter.ViewHolder holder,
+ RowPresenter.@NonNull ViewHolder holder,
@NonNull Object item
) {
super.onBindRowViewHolder(holder, item);
@@ -685,7 +679,7 @@
}
@Override
- protected void onUnbindRowViewHolder(@NonNull RowPresenter.ViewHolder holder) {
+ protected void onUnbindRowViewHolder(RowPresenter.@NonNull ViewHolder holder) {
ViewHolder vh = (ViewHolder) holder;
vh.mGridView.setAdapter(null);
vh.mItemBridgeAdapter.clear();
@@ -864,14 +858,14 @@
}
@Override
- public void freeze(@NonNull RowPresenter.ViewHolder holder, boolean freeze) {
+ public void freeze(RowPresenter.@NonNull ViewHolder holder, boolean freeze) {
ViewHolder vh = (ViewHolder) holder;
vh.mGridView.setScrollEnabled(!freeze);
vh.mGridView.setAnimateChildLayout(!freeze);
}
@Override
- public void setEntranceTransitionState(@NonNull RowPresenter.ViewHolder holder,
+ public void setEntranceTransitionState(RowPresenter.@NonNull ViewHolder holder,
boolean afterEntrance) {
super.setEntranceTransitionState(holder, afterEntrance);
((ViewHolder) holder).mGridView.setChildrenVisibility(
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/MediaItemActionPresenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/MediaItemActionPresenter.java
index 4dfff3e..f87b884 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/MediaItemActionPresenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/MediaItemActionPresenter.java
@@ -19,10 +19,11 @@
import android.view.ViewGroup;
import android.widget.ImageView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* The presenter displaying a custom action in {@link AbstractMediaItemPresenter}.
* This is the default presenter for actions in media rows if no action presenter is provided by the
@@ -48,9 +49,8 @@
}
}
- @NonNull
@Override
- public Presenter.ViewHolder onCreateViewHolder(ViewGroup parent) {
+ public Presenter.@NonNull ViewHolder onCreateViewHolder(ViewGroup parent) {
Context context = parent.getContext();
View actionView = LayoutInflater.from(context)
.inflate(R.layout.lb_row_media_item_action, parent, false);
@@ -58,13 +58,13 @@
}
@Override
- public void onBindViewHolder(@NonNull Presenter.ViewHolder viewHolder, @Nullable Object item) {
+ public void onBindViewHolder(Presenter.@NonNull ViewHolder viewHolder, @Nullable Object item) {
ViewHolder actionViewHolder = (ViewHolder) viewHolder;
MultiActionsProvider.MultiAction action = (MultiActionsProvider.MultiAction) item;
actionViewHolder.getIcon().setImageDrawable(action.getCurrentDrawable());
}
@Override
- public void onUnbindViewHolder(@NonNull Presenter.ViewHolder viewHolder) {
+ public void onUnbindViewHolder(Presenter.@NonNull ViewHolder viewHolder) {
}
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/MediaRowFocusView.java b/leanback/leanback/src/main/java/androidx/leanback/widget/MediaRowFocusView.java
index 3c1cccd..a471ffe 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/MediaRowFocusView.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/MediaRowFocusView.java
@@ -20,10 +20,11 @@
import android.util.AttributeSet;
import android.view.View;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+
/**
* Creates a view for a media item row in a playlist
*/
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/NonOverlappingLinearLayoutWithForeground.java b/leanback/leanback/src/main/java/androidx/leanback/widget/NonOverlappingLinearLayoutWithForeground.java
index 764f54e..b7edce5 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/NonOverlappingLinearLayoutWithForeground.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/NonOverlappingLinearLayoutWithForeground.java
@@ -22,7 +22,7 @@
import android.util.AttributeSet;
import android.widget.LinearLayout;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Implements foreground drawable before M and falls back to M's foreground implementation.
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/ObjectAdapter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/ObjectAdapter.java
index 7009217..5c2546e 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/ObjectAdapter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/ObjectAdapter.java
@@ -18,10 +18,11 @@
import android.annotation.SuppressLint;
import android.database.Observable;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Base class adapter to be used in leanback activities. Provides access to a data model and is
* decoupled from the presentation of the items via {@link PresenterSelector}.
@@ -199,8 +200,7 @@
/**
* Returns the presenter selector for this ObjectAdapter.
*/
- @NonNull
- public final PresenterSelector getPresenterSelector() {
+ public final @NonNull PresenterSelector getPresenterSelector() {
return mPresenterSelector;
}
@@ -327,8 +327,7 @@
/**
* Returns the {@link Presenter} for the given item from the adapter.
*/
- @Nullable
- public final Presenter getPresenter(@NonNull Object item) {
+ public final @Nullable Presenter getPresenter(@NonNull Object item) {
if (mPresenterSelector == null) {
throw new IllegalStateException("Presenter selector must not be null");
}
@@ -343,8 +342,7 @@
/**
* Returns the item for the given position.
*/
- @Nullable
- public abstract Object get(int position);
+ public abstract @Nullable Object get(int position);
/**
* Returns the id for the given position.
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/OnActionClickedListener.java b/leanback/leanback/src/main/java/androidx/leanback/widget/OnActionClickedListener.java
index 919c545..3c52243 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/OnActionClickedListener.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/OnActionClickedListener.java
@@ -13,7 +13,7 @@
*/
package androidx.leanback.widget;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Interface for receiving notification when an {@link Action} is clicked.
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/PageRow.java b/leanback/leanback/src/main/java/androidx/leanback/widget/PageRow.java
index 05129a5..7a5c5e6 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/PageRow.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/PageRow.java
@@ -13,7 +13,7 @@
*/
package androidx.leanback.widget;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* Used to represent content spanning full page.
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/PagingIndicator.java b/leanback/leanback/src/main/java/androidx/leanback/widget/PagingIndicator.java
index 270e9c2..54628ad 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/PagingIndicator.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/PagingIndicator.java
@@ -40,12 +40,13 @@
import android.view.animation.DecelerateInterpolator;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.core.view.ViewCompat;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+
/**
* A page indicator with dots.
*/
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackControlsPresenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackControlsPresenter.java
index 5d81a3c..4bd3650 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackControlsPresenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackControlsPresenter.java
@@ -28,11 +28,12 @@
import android.widget.TextView;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
import androidx.leanback.util.MathUtil;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* A presenter for a control bar that supports "more actions",
* and toggling the set of controls between primary and secondary
@@ -313,16 +314,15 @@
vh.mTotalTime.setLayoutParams(lp);
}
- @NonNull
@Override
- public Presenter.ViewHolder onCreateViewHolder(ViewGroup parent) {
+ public Presenter.@NonNull ViewHolder onCreateViewHolder(ViewGroup parent) {
View v = LayoutInflater.from(parent.getContext())
.inflate(getLayoutResourceId(), parent, false);
return new ViewHolder(v);
}
@Override
- public void onBindViewHolder(@NonNull Presenter.ViewHolder holder, @Nullable Object item) {
+ public void onBindViewHolder(Presenter.@NonNull ViewHolder holder, @Nullable Object item) {
ViewHolder vh = (ViewHolder) holder;
BoundData data = (BoundData) item;
@@ -338,7 +338,7 @@
}
@Override
- public void onUnbindViewHolder(@NonNull Presenter.ViewHolder holder) {
+ public void onUnbindViewHolder(Presenter.@NonNull ViewHolder holder) {
super.onUnbindViewHolder(holder);
ViewHolder vh = (ViewHolder) holder;
if (vh.mMoreActionsAdapter != null) {
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackControlsRowPresenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackControlsRowPresenter.java
index 1052f81..ee14611 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackControlsRowPresenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackControlsRowPresenter.java
@@ -27,13 +27,14 @@
import android.widget.LinearLayout;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.leanback.R;
import androidx.leanback.widget.ControlBarPresenter.OnControlClickedListener;
import androidx.leanback.widget.ControlBarPresenter.OnControlSelectedListener;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* A PlaybackControlsRowPresenter renders a {@link PlaybackControlsRow} to display a
* series of playback control buttons. Typically this row will be the first row in a fragment
@@ -237,8 +238,7 @@
/**
* Returns the listener for {@link Action} click events.
*/
- @Nullable
- public OnActionClickedListener getOnActionClickedListener() {
+ public @Nullable OnActionClickedListener getOnActionClickedListener() {
return mOnActionClickedListener;
}
@@ -315,7 +315,7 @@
}
@Override
- public void onReappear(@NonNull RowPresenter.ViewHolder rowViewHolder) {
+ public void onReappear(RowPresenter.@NonNull ViewHolder rowViewHolder) {
showPrimaryActions((ViewHolder) rowViewHolder);
}
@@ -336,9 +336,8 @@
return context.getResources().getColor(R.color.lb_playback_progress_color_no_theme);
}
- @NonNull
@Override
- protected RowPresenter.ViewHolder createRowViewHolder(@NonNull ViewGroup parent) {
+ protected RowPresenter.@NonNull ViewHolder createRowViewHolder(@NonNull ViewGroup parent) {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.lb_playback_controls_row, parent, false);
ViewHolder vh = new ViewHolder(v, mDescriptionPresenter);
@@ -380,7 +379,7 @@
@Override
protected void onBindRowViewHolder(
- @NonNull RowPresenter.ViewHolder holder,
+ RowPresenter.@NonNull ViewHolder holder,
@NonNull Object item
) {
super.onBindRowViewHolder(holder, item);
@@ -458,7 +457,7 @@
}
@Override
- protected void onUnbindRowViewHolder(@NonNull RowPresenter.ViewHolder holder) {
+ protected void onUnbindRowViewHolder(RowPresenter.@NonNull ViewHolder holder) {
ViewHolder vh = (ViewHolder) holder;
PlaybackControlsRow row = (PlaybackControlsRow) vh.getRow();
@@ -473,7 +472,7 @@
}
@Override
- protected void onRowViewSelected(@NonNull RowPresenter.ViewHolder vh, boolean selected) {
+ protected void onRowViewSelected(RowPresenter.@NonNull ViewHolder vh, boolean selected) {
super.onRowViewSelected(vh, selected);
if (selected) {
((ViewHolder) vh).dispatchItemSelection();
@@ -481,7 +480,7 @@
}
@Override
- protected void onRowViewAttachedToWindow(@NonNull RowPresenter.ViewHolder vh) {
+ protected void onRowViewAttachedToWindow(RowPresenter.@NonNull ViewHolder vh) {
super.onRowViewAttachedToWindow(vh);
if (mDescriptionPresenter != null) {
mDescriptionPresenter.onViewAttachedToWindow(
@@ -490,7 +489,7 @@
}
@Override
- protected void onRowViewDetachedFromWindow(@NonNull RowPresenter.ViewHolder vh) {
+ protected void onRowViewDetachedFromWindow(RowPresenter.@NonNull ViewHolder vh) {
super.onRowViewDetachedFromWindow(vh);
if (mDescriptionPresenter != null) {
mDescriptionPresenter.onViewDetachedFromWindow(
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackControlsRowView.java b/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackControlsRowView.java
index 2f26df9..e14ff0f 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackControlsRowView.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackControlsRowView.java
@@ -23,7 +23,7 @@
import android.view.View;
import android.widget.LinearLayout;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* A LinearLayout that preserves the focused child view.
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackRowPresenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackRowPresenter.java
index b68a69a..e766367 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackRowPresenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackRowPresenter.java
@@ -2,7 +2,7 @@
import android.view.View;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* Subclass of {@link RowPresenter} that can define the desired behavior when the view
@@ -24,6 +24,6 @@
/**
* Provides hook to update the UI when the view reappears.
*/
- public void onReappear(@NonNull RowPresenter.ViewHolder rowViewHolder) {
+ public void onReappear(RowPresenter.@NonNull ViewHolder rowViewHolder) {
}
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackTransportRowPresenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackTransportRowPresenter.java
index 6a3f78a..2d29295 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackTransportRowPresenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/PlaybackTransportRowPresenter.java
@@ -28,11 +28,12 @@
import android.widget.TextView;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
import androidx.leanback.R;
import androidx.leanback.widget.ControlBarPresenter.OnControlClickedListener;
import androidx.leanback.widget.ControlBarPresenter.OnControlSelectedListener;
+import org.jspecify.annotations.NonNull;
+
import java.util.Arrays;
/**
@@ -718,7 +719,7 @@
@Override
protected void onBindRowViewHolder(
- @NonNull RowPresenter.ViewHolder holder,
+ RowPresenter.@NonNull ViewHolder holder,
@NonNull Object item
) {
super.onBindRowViewHolder(holder, item);
@@ -760,7 +761,7 @@
}
@Override
- protected void onUnbindRowViewHolder(@NonNull RowPresenter.ViewHolder holder) {
+ protected void onUnbindRowViewHolder(RowPresenter.@NonNull ViewHolder holder) {
ViewHolder vh = (ViewHolder) holder;
PlaybackControlsRow row = (PlaybackControlsRow) vh.getRow();
@@ -821,7 +822,7 @@
}
@Override
- protected void onRowViewAttachedToWindow(@NonNull RowPresenter.ViewHolder vh) {
+ protected void onRowViewAttachedToWindow(RowPresenter.@NonNull ViewHolder vh) {
super.onRowViewAttachedToWindow(vh);
if (mDescriptionPresenter != null) {
mDescriptionPresenter.onViewAttachedToWindow(
@@ -830,7 +831,7 @@
}
@Override
- protected void onRowViewDetachedFromWindow(@NonNull RowPresenter.ViewHolder vh) {
+ protected void onRowViewDetachedFromWindow(RowPresenter.@NonNull ViewHolder vh) {
super.onRowViewDetachedFromWindow(vh);
if (mDescriptionPresenter != null) {
mDescriptionPresenter.onViewDetachedFromWindow(
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/Presenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/Presenter.java
index 5ec356b..163b1cc 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/Presenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/Presenter.java
@@ -16,10 +16,11 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.collection.ArrayMap;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.List;
import java.util.Map;
@@ -122,8 +123,7 @@
/**
* Creates a new {@link View}.
*/
- @NonNull
- public abstract ViewHolder onCreateViewHolder(@NonNull ViewGroup parent);
+ public abstract @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent);
/**
* Binds a {@link View} to an item.
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/PresenterSelector.java b/leanback/leanback/src/main/java/androidx/leanback/widget/PresenterSelector.java
index 3dae168..00f969e 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/PresenterSelector.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/PresenterSelector.java
@@ -15,7 +15,7 @@
import android.annotation.SuppressLint;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A PresenterSelector is used to obtain a {@link Presenter} for a given Object.
@@ -25,16 +25,14 @@
/**
* Returns a presenter for the given item.
*/
- @Nullable
- public abstract Presenter getPresenter(@Nullable Object item);
+ public abstract @Nullable Presenter getPresenter(@Nullable Object item);
/**
* Returns an array of all possible presenters. The returned array should
* not be modified.
*/
@SuppressLint("NullableCollection")
- @Nullable
- public Presenter[] getPresenters() {
+ public Presenter @Nullable [] getPresenters() {
return null;
}
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/RowContainerView.java b/leanback/leanback/src/main/java/androidx/leanback/widget/RowContainerView.java
index 976a909..6ba742f 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/RowContainerView.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/RowContainerView.java
@@ -24,9 +24,10 @@
import android.widget.LinearLayout;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+
/**
* RowContainerView wraps header and user defined row view
*/
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/RowHeaderPresenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/RowHeaderPresenter.java
index 386c59a..e994f56 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/RowHeaderPresenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/RowHeaderPresenter.java
@@ -22,11 +22,12 @@
import android.view.ViewGroup;
import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* RowHeaderPresenter provides a default presentation for {@link HeaderItem} using a
* {@link RowHeaderView} and optionally a TextView for description. If a subclass creates its own
@@ -123,9 +124,8 @@
}
}
- @NonNull
@Override
- public Presenter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
+ public Presenter.@NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
View root = LayoutInflater.from(parent.getContext())
.inflate(mLayoutResourceId, parent, false);
@@ -137,7 +137,7 @@
}
@Override
- public void onBindViewHolder(@NonNull Presenter.ViewHolder viewHolder, @Nullable Object item) {
+ public void onBindViewHolder(Presenter.@NonNull ViewHolder viewHolder, @Nullable Object item) {
HeaderItem headerItem = item == null ? null : ((Row) item).getHeaderItem();
RowHeaderPresenter.ViewHolder vh = (RowHeaderPresenter.ViewHolder)viewHolder;
if (headerItem == null) {
@@ -170,7 +170,7 @@
}
@Override
- public void onUnbindViewHolder(@NonNull Presenter.ViewHolder viewHolder) {
+ public void onUnbindViewHolder(Presenter.@NonNull ViewHolder viewHolder) {
RowHeaderPresenter.ViewHolder vh = (ViewHolder)viewHolder;
if (vh.mTitleView != null) {
vh.mTitleView.setText(null);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/RowPresenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/RowPresenter.java
index c02f3d9..d1924cd 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/RowPresenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/RowPresenter.java
@@ -16,11 +16,12 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.app.HeadersFragment;
import androidx.leanback.graphics.ColorOverlayDimmer;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* An abstract {@link Presenter} that renders an Object in RowsFragment, the object can be
* subclass {@link Row} or a generic one. When the object is not {@link Row} class,
@@ -297,8 +298,7 @@
* Return {@link ViewHolder} of currently selected item inside a row ViewHolder.
* @return The selected item's ViewHolder.
*/
- @Nullable
- public Presenter.ViewHolder getSelectedItemViewHolder() {
+ public Presenter.@Nullable ViewHolder getSelectedItemViewHolder() {
return null;
}
@@ -306,8 +306,7 @@
* Return currently selected item inside a row ViewHolder.
* @return The selected item.
*/
- @Nullable
- public Object getSelectedItem() {
+ public @Nullable Object getSelectedItem() {
return null;
}
}
@@ -354,8 +353,7 @@
* @param parent The parent View for the Row's view holder.
* @return A ViewHolder for the Row's View.
*/
- @NonNull
- protected abstract ViewHolder createRowViewHolder(@NonNull ViewGroup parent);
+ protected abstract @NonNull ViewHolder createRowViewHolder(@NonNull ViewGroup parent);
/**
* Returns true if the Row view should clip its children. The clipChildren
@@ -607,7 +605,7 @@
@Override
public final void onBindViewHolder(
- @NonNull Presenter.ViewHolder viewHolder,
+ Presenter.@NonNull ViewHolder viewHolder,
@Nullable Object item
) {
onBindRowViewHolder(getRowViewHolder(viewHolder), item);
@@ -628,7 +626,7 @@
}
@Override
- public final void onUnbindViewHolder(@NonNull Presenter.ViewHolder viewHolder) {
+ public final void onUnbindViewHolder(Presenter.@NonNull ViewHolder viewHolder) {
onUnbindRowViewHolder(getRowViewHolder(viewHolder));
}
@@ -646,7 +644,7 @@
}
@Override
- public final void onViewAttachedToWindow(@NonNull Presenter.ViewHolder holder) {
+ public final void onViewAttachedToWindow(Presenter.@NonNull ViewHolder holder) {
onRowViewAttachedToWindow(getRowViewHolder(holder));
}
@@ -660,7 +658,7 @@
}
@Override
- public final void onViewDetachedFromWindow(@NonNull Presenter.ViewHolder holder) {
+ public final void onViewDetachedFromWindow(Presenter.@NonNull ViewHolder holder) {
onRowViewDetachedFromWindow(getRowViewHolder(holder));
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/SearchOrbView.java b/leanback/leanback/src/main/java/androidx/leanback/widget/SearchOrbView.java
index 870bbf5..7691623 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/SearchOrbView.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/SearchOrbView.java
@@ -33,11 +33,12 @@
import android.widget.ImageView;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* <p>A widget that draws a search affordance, represented by a round background and an icon.</p>
*
@@ -273,8 +274,7 @@
*
* @return the drawable used as the icon
*/
- @Nullable
- public Drawable getOrbIcon() {
+ public @Nullable Drawable getOrbIcon() {
return mIconDrawable;
}
@@ -335,8 +335,7 @@
/**
* Returns the {@link Colors} used to display the search orb.
*/
- @Nullable
- public Colors getOrbColors() {
+ public @Nullable Colors getOrbColors() {
return mColors;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/SeekBar.java b/leanback/leanback/src/main/java/androidx/leanback/widget/SeekBar.java
index b1db6eb..043d3a7 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/SeekBar.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/SeekBar.java
@@ -28,11 +28,12 @@
import android.util.AttributeSet;
import android.view.View;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+
/**
* Replacement of SeekBar, has two bar heights and two thumb size when focused/not_focused.
* The widget does not deal with KeyEvent, it's client's responsibility to set a key listener.
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/ShadowOverlayContainer.java b/leanback/leanback/src/main/java/androidx/leanback/widget/ShadowOverlayContainer.java
index d46de1a..135ee63 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/ShadowOverlayContainer.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/ShadowOverlayContainer.java
@@ -24,9 +24,10 @@
import android.widget.FrameLayout;
import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+
/**
* Provides an SDK version-independent wrapper to support shadows, color overlays, and rounded
* corners. It's not always preferred to create a ShadowOverlayContainer, use
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/ShadowOverlayHelper.java b/leanback/leanback/src/main/java/androidx/leanback/widget/ShadowOverlayHelper.java
index 63a6da7..58f161e 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/ShadowOverlayHelper.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/ShadowOverlayHelper.java
@@ -23,7 +23,6 @@
import androidx.leanback.R;
import androidx.leanback.system.Settings;
-
/**
* ShadowOverlayHelper is a helper class for shadow, overlay color and rounded corner.
* There are many choices to implement Shadow, overlay color.
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/SinglePresenterSelector.java b/leanback/leanback/src/main/java/androidx/leanback/widget/SinglePresenterSelector.java
index 5c5f746..e1be2b3 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/SinglePresenterSelector.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/SinglePresenterSelector.java
@@ -13,8 +13,8 @@
*/
package androidx.leanback.widget;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* A {@link PresenterSelector} that always returns the same {@link Presenter}.
@@ -31,15 +31,13 @@
mPresenter = presenter;
}
- @Nullable
@Override
- public Presenter getPresenter(@Nullable Object item) {
+ public @Nullable Presenter getPresenter(@Nullable Object item) {
return mPresenter;
}
- @NonNull
@Override
- public Presenter[] getPresenters() {
+ public Presenter @NonNull [] getPresenters() {
return new Presenter[]{mPresenter};
}
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/SparseArrayObjectAdapter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/SparseArrayObjectAdapter.java
index edc6233..48787a6 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/SparseArrayObjectAdapter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/SparseArrayObjectAdapter.java
@@ -2,7 +2,7 @@
import android.util.SparseArray;
-import androidx.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* An {@link ObjectAdapter} implemented with a {@link android.util.SparseArray}.
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/StreamingTextView.java b/leanback/leanback/src/main/java/androidx/leanback/widget/StreamingTextView.java
index 3727721..4bd9ae3 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/StreamingTextView.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/StreamingTextView.java
@@ -34,10 +34,11 @@
import android.widget.EditText;
import android.widget.TextView;
-import androidx.annotation.NonNull;
import androidx.core.widget.TextViewCompat;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+
import java.util.List;
import java.util.Random;
import java.util.regex.Matcher;
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/TitleView.java b/leanback/leanback/src/main/java/androidx/leanback/widget/TitleView.java
index f96033f..c22fe19 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/TitleView.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/TitleView.java
@@ -26,10 +26,11 @@
import android.widget.ImageView;
import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Title view for a leanback fragment.
*/
@@ -126,8 +127,7 @@
/**
* Returns the title text.
*/
- @Nullable
- public CharSequence getTitle() {
+ public @Nullable CharSequence getTitle() {
return mTextView.getText();
}
@@ -143,8 +143,7 @@
/**
* Returns the badge drawable.
*/
- @Nullable
- public Drawable getBadgeDrawable() {
+ public @Nullable Drawable getBadgeDrawable() {
return mBadgeView.getDrawable();
}
@@ -160,23 +159,21 @@
/**
* Returns the view for the search affordance.
*/
- @NonNull
- public View getSearchAffordanceView() {
+ public @NonNull View getSearchAffordanceView() {
return mSearchOrbView;
}
/**
* Sets the {@link SearchOrbView.Colors} used to draw the search affordance.
*/
- public void setSearchAffordanceColors(@NonNull SearchOrbView.Colors colors) {
+ public void setSearchAffordanceColors(SearchOrbView.@NonNull Colors colors) {
mSearchOrbView.setOrbColors(colors);
}
/**
* Returns the {@link SearchOrbView.Colors} used to draw the search affordance.
*/
- @Nullable
- public SearchOrbView.Colors getSearchAffordanceColors() {
+ public SearchOrbView.@Nullable Colors getSearchAffordanceColors() {
return mSearchOrbView.getOrbColors();
}
@@ -226,8 +223,7 @@
}
@Override
- @NonNull
- public TitleViewAdapter getTitleViewAdapter() {
+ public @NonNull TitleViewAdapter getTitleViewAdapter() {
return mTitleViewAdapter;
}
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/TitleViewAdapter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/TitleViewAdapter.java
index f04143e..0b1a63b 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/TitleViewAdapter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/TitleViewAdapter.java
@@ -16,7 +16,7 @@
import android.graphics.drawable.Drawable;
import android.view.View;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* This class allows a customized widget class to implement {@link TitleViewAdapter.Provider}
@@ -106,7 +106,7 @@
*
* @param colors Colors used to draw search affordance.
*/
- public void setSearchAffordanceColors(@NonNull SearchOrbView.Colors colors) {
+ public void setSearchAffordanceColors(SearchOrbView.@NonNull Colors colors) {
}
/**
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/VerticalGridPresenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/VerticalGridPresenter.java
index 3c3811b..188c4ef 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/VerticalGridPresenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/VerticalGridPresenter.java
@@ -19,12 +19,13 @@
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.leanback.R;
import androidx.leanback.system.Settings;
import androidx.leanback.transition.TransitionHelper;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* A presenter that renders objects in a {@link VerticalGridView}.
*/
@@ -88,8 +89,7 @@
mGridView = view;
}
- @NonNull
- public VerticalGridView getGridView() {
+ public @NonNull VerticalGridView getGridView() {
return mGridView;
}
}
@@ -233,9 +233,8 @@
return mUseFocusDimmer;
}
- @NonNull
@Override
- public final ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
+ public final @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) {
ViewHolder vh = createGridViewHolder(parent);
vh.mInitialized = false;
vh.mItemBridgeAdapter = new VerticalGridItemBridgeAdapter();
@@ -249,8 +248,7 @@
/**
* Subclass may override this to inflate a different layout.
*/
- @NonNull
- protected ViewHolder createGridViewHolder(@NonNull ViewGroup parent) {
+ protected @NonNull ViewHolder createGridViewHolder(@NonNull ViewGroup parent) {
View root = LayoutInflater.from(parent.getContext()).inflate(
R.layout.lb_vertical_grid, parent, false);
return new ViewHolder((VerticalGridView) root.findViewById(R.id.browse_grid));
@@ -336,13 +334,12 @@
*
* @return The options to be used for shadow, overlay and rounded corner.
*/
- @NonNull
- protected ShadowOverlayHelper.Options createShadowOverlayOptions() {
+ protected ShadowOverlayHelper.@NonNull Options createShadowOverlayOptions() {
return ShadowOverlayHelper.Options.DEFAULT;
}
@Override
- public void onBindViewHolder(@NonNull Presenter.ViewHolder viewHolder, @Nullable Object item) {
+ public void onBindViewHolder(Presenter.@NonNull ViewHolder viewHolder, @Nullable Object item) {
if (DEBUG) Log.v(TAG, "onBindViewHolder " + item);
ViewHolder vh = (ViewHolder) viewHolder;
vh.mItemBridgeAdapter.setAdapter((ObjectAdapter) item);
@@ -350,7 +347,7 @@
}
@Override
- public void onUnbindViewHolder(@NonNull Presenter.ViewHolder viewHolder) {
+ public void onUnbindViewHolder(Presenter.@NonNull ViewHolder viewHolder) {
if (DEBUG) Log.v(TAG, "onUnbindViewHolder");
ViewHolder vh = (ViewHolder) viewHolder;
vh.mItemBridgeAdapter.setAdapter(null);
@@ -368,8 +365,7 @@
/**
* Returns the item selected listener.
*/
- @Nullable
- public final OnItemViewSelectedListener getOnItemViewSelectedListener() {
+ public final @Nullable OnItemViewSelectedListener getOnItemViewSelectedListener() {
return mOnItemViewSelectedListener;
}
@@ -386,8 +382,7 @@
/**
* Returns the item clicked listener.
*/
- @Nullable
- public final OnItemViewClickedListener getOnItemViewClickedListener() {
+ public final @Nullable OnItemViewClickedListener getOnItemViewClickedListener() {
return mOnItemViewClickedListener;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/picker/Picker.java b/leanback/leanback/src/main/java/androidx/leanback/widget/picker/Picker.java
index 8f6d9c2..82e59b1 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/picker/Picker.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/picker/Picker.java
@@ -31,14 +31,15 @@
import androidx.annotation.IdRes;
import androidx.annotation.LayoutRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.leanback.R;
import androidx.leanback.widget.OnChildViewHolderSelectedListener;
import androidx.leanback.widget.VerticalGridView;
import androidx.recyclerview.widget.RecyclerView;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -122,8 +123,7 @@
*
* @return The list of separators populated between the picker column fields.
*/
- @NonNull
- public final List<CharSequence> getSeparators() {
+ public final @NonNull List<CharSequence> getSeparators() {
return mSeparators;
}
@@ -232,8 +232,7 @@
* @param colIndex Index of PickerColumn.
* @return PickerColumn at colIndex or null if {@link #setColumns(List)} is not called yet.
*/
- @Nullable
- public PickerColumn getColumnAt(int colIndex) {
+ public @Nullable PickerColumn getColumnAt(int colIndex) {
if (mColumns == null) {
return null;
}
diff --git a/libraryversions.toml b/libraryversions.toml
index 7c87027..9b39bca 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -24,7 +24,7 @@
COMPOSE_MATERIAL3 = "1.4.0-alpha05"
COMPOSE_MATERIAL3_ADAPTIVE = "1.1.0-alpha08"
COMPOSE_MATERIAL3_COMMON = "1.0.0-alpha01"
-# Adding a comment to prevent merge conflicts
+COMPOSE_MATERIAL3_XR = "1.0.0-alpha01"
COMPOSE_RUNTIME = "1.8.0-beta01"
CONSTRAINTLAYOUT = "2.2.0-beta01"
CONSTRAINTLAYOUT_COMPOSE = "1.1.0-beta01"
@@ -180,6 +180,10 @@
WINDOW_SIDECAR = "1.0.0-rc01"
WORK = "2.10.0-rc01"
XR = "1.0.0-alpha01"
+XR_ARCORE = "1.0.0-alpha01"
+XR_COMPOSE = "1.0.0-alpha01"
+XR_RUNTIME = "1.0.0-alpha01"
+XR_SCENECORE = "1.0.0-alpha01"
[groups]
ACTIVITY = { group = "androidx.activity", atomicGroupVersion = "versions.ACTIVITY" }
@@ -209,6 +213,7 @@
COMPOSE_MATERIAL3 = { group = "androidx.compose.material3", atomicGroupVersion = "versions.COMPOSE_MATERIAL3" }
COMPOSE_MATERIAL3_ADAPTIVE = { group = "androidx.compose.material3.adaptive", atomicGroupVersion = "versions.COMPOSE_MATERIAL3_ADAPTIVE" }
COMPOSE_MATERIAL3_ADAPTIVE_NAVIGATION_SUITE = { group = "androidx.compose.material3", atomicGroupVersion = "versions.COMPOSE_MATERIAL3", overrideInclude = [ ":compose:material3:material3-adaptive-navigation-suite" ] }
+COMPOSE_MATERIAL3_XR = { group = "androidx.xr.compose.material3", atomicGroupVersion = "versions.COMPOSE_MATERIAL3_XR" }
COMPOSE_RUNTIME = { group = "androidx.compose.runtime", atomicGroupVersion = "versions.COMPOSE" }
COMPOSE_UI = { group = "androidx.compose.ui", atomicGroupVersion = "versions.COMPOSE" }
CONCURRENT = { group = "androidx.concurrent", atomicGroupVersion = "versions.FUTURES" }
@@ -312,3 +317,7 @@
WINDOW_SIDECAR = { group = "androidx.window.sidecar", atomicGroupVersion = "versions.WINDOW_SIDECAR" }
WORK = { group = "androidx.work", atomicGroupVersion = "versions.WORK" }
XR = { group = "androidx.xr", atomicGroupVersion = "versions.XR" }
+XR_ARCORE = { group = "androidx.xr.arcore", atomicGroupVersion = "versions.XR_ARCORE" }
+XR_COMPOSE = { group = "androidx.xr.compose", atomicGroupVersion = "versions.XR_COMPOSE" }
+XR_RUNTIME = { group = "androidx.xr.runtime", atomicGroupVersion = "versions.XR_RUNTIME" }
+XR_SCENECORE = { group = "androidx.xr.scenecore", atomicGroupVersion = "versions.XR_SCENECORE" }
diff --git a/lifecycle/lifecycle-runtime-compose/samples/build.gradle b/lifecycle/lifecycle-runtime-compose/samples/build.gradle
index 2468e8b..9e758aea 100644
--- a/lifecycle/lifecycle-runtime-compose/samples/build.gradle
+++ b/lifecycle/lifecycle-runtime-compose/samples/build.gradle
@@ -35,7 +35,7 @@
compileOnly(project(":annotation:annotation-sampled"))
implementation(libs.kotlinStdlib)
- implementation project(":lifecycle:lifecycle-runtime-compose")
+ implementation(project(":lifecycle:lifecycle-runtime-compose"))
implementation "androidx.compose.material:material:1.0.1"
}
diff --git a/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle b/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle
index 683b8c6..21c677f 100644
--- a/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle
@@ -36,9 +36,9 @@
dependencies {
compileOnly(project(":annotation:annotation-sampled"))
implementation(libs.kotlinStdlib)
- implementation project(":lifecycle:lifecycle-common-java8")
- implementation project(":lifecycle:lifecycle-viewmodel-compose")
- implementation project(":lifecycle:lifecycle-viewmodel-savedstate")
+ implementation(project(":lifecycle:lifecycle-common-java8"))
+ implementation(project(":lifecycle:lifecycle-viewmodel-compose"))
+ implementation(project(":lifecycle:lifecycle-viewmodel-savedstate"))
}
androidx {
diff --git a/lifecycle/lifecycle-viewmodel-navigation3/build.gradle b/lifecycle/lifecycle-viewmodel-navigation3/build.gradle
index 2baa415..165f1a5 100644
--- a/lifecycle/lifecycle-viewmodel-navigation3/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-navigation3/build.gradle
@@ -69,7 +69,7 @@
dependencies {
implementation("androidx.activity:activity-compose:1.10.0-beta01")
implementation("androidx.compose.ui:ui:1.7.5")
- implementation project(":navigation3:navigation3")
+ implementation(project(":navigation3:navigation3"))
}
}
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
index 5f097d1..47ad93c 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
@@ -55,7 +55,7 @@
commonTest {
dependencies {
- implementation project(":lifecycle:lifecycle-runtime")
+ implementation(project(":lifecycle:lifecycle-runtime"))
implementation(libs.kotlinCoroutinesTest)
}
}
@@ -84,9 +84,9 @@
androidInstrumentedTest {
dependsOn(jvmTest)
dependencies {
- implementation project(":lifecycle:lifecycle-livedata-core")
+ implementation(project(":lifecycle:lifecycle-livedata-core"))
implementation ("androidx.fragment:fragment:1.3.0")
- implementation project(":internal-testutils-runtime")
+ implementation(project(":internal-testutils-runtime"))
implementation(project(":lifecycle:lifecycle-viewmodel"))
implementation(libs.truth)
implementation(libs.testExtJunit)
diff --git a/lint-checks/integration-tests/lint-baseline.xml b/lint-checks/integration-tests/lint-baseline.xml
index 35eb074c..b1db38c 100644
--- a/lint-checks/integration-tests/lint-baseline.xml
+++ b/lint-checks/integration-tests/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.8.0-alpha06" type="baseline" client="gradle" dependencies="false" name="AGP (8.8.0-alpha06)" variant="all" version="8.8.0-alpha06">
+<issues format="6" by="lint 8.9.0-alpha01" type="baseline" client="gradle" dependencies="false" name="AGP (8.9.0-alpha01)" variant="all" version="8.9.0-alpha01">
<issue
id="MissingClass"
@@ -11,15 +11,6 @@
</issue>
<issue
- id="NewApi"
- message="Call requires API level 23 (current min is 21): `android.view.View#getAccessibilityClassName`"
- errorLine1=" return view.getAccessibilityClassName();"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/ClassVerificationFailureFromJava.java"/>
- </issue>
-
- <issue
id="BanKeepAnnotation"
message="Uses @Keep annotation"
errorLine1="@Keep"
@@ -146,6 +137,60 @@
</issue>
<issue
+ id="JSpecifyNullness"
+ message="Switch nullness annotation to JSpecify"
+ errorLine1=" static boolean recreate(@NonNull final Activity activity) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/sample/core/app/ActivityRecreator.java"/>
+ </issue>
+
+ <issue
+ id="JSpecifyNullness"
+ message="Switch nullness annotation to JSpecify"
+ errorLine1=" LifecycleCheckCallbacks(@NonNull Activity aboutToRecreate) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/sample/core/app/ActivityRecreator.java"/>
+ </issue>
+
+ <issue
+ id="JSpecifyNullness"
+ message="Switch nullness annotation to JSpecify"
+ errorLine1=" static boolean recreate(@NonNull final Activity activity) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/sample/core/app/ActivityRecreatorChecked.java"/>
+ </issue>
+
+ <issue
+ id="JSpecifyNullness"
+ message="Switch nullness annotation to JSpecify"
+ errorLine1=" LifecycleCheckCallbacks(@NonNull Activity aboutToRecreate) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/sample/core/app/ActivityRecreatorChecked.java"/>
+ </issue>
+
+ <issue
+ id="JSpecifyNullness"
+ message="Switch nullness annotation to JSpecify"
+ errorLine1=" protected ParcelableUsageJava(@NonNull Parcel in) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/ParcelableUsageJava.java"/>
+ </issue>
+
+ <issue
+ id="JSpecifyNullness"
+ message="Switch nullness annotation to JSpecify"
+ errorLine1=" public void writeToParcel(@NonNull Parcel dest, int flags) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/ParcelableUsageJava.java"/>
+ </issue>
+
+ <issue
id="LongLogTag"
message="The logging tag can be at most 23 characters, was 24 (ActivityRecreatorChecked)"
errorLine1=" Log.e(LOG_TAG, "Exception while invoking performStopActivity", t);"
@@ -370,409 +415,4 @@
file="src/main/java/androidx/RestrictToTestsAnnotationUsageKotlin.kt"/>
</issue>
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 21"
- errorLine1=" @RequiresApi(21)"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/sample/appcompat/widget/ActionBarBackgroundDrawable.java"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 21"
- errorLine1=" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeCallToThis.java"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 21"
- errorLine1=" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeCallToThis.java"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 21"
- errorLine1=" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeCallToThis.java"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 19"
- errorLine1=" @RequiresApi(19)"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeMethodWithQualifiedClass.java"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 21"
- errorLine1=" if (Build.VERSION.SDK_INT >= 21) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeReferenceWithExistingClassJava.java"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 21"
- errorLine1=" @RequiresApi(21)"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeReferenceWithExistingFix.java"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 21"
- errorLine1=" @RequiresApi(21)"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeReferenceWithExistingFix.java"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 21"
- errorLine1=" @RequiresApi(21)"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeReferenceWithExistingFix.java"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 21"
- errorLine1=" if (Build.VERSION.SDK_INT >= 17) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeStaticMethodReferenceJava.java"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 21"
- errorLine1=" if (Build.VERSION.SDK_INT >= 21) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeVoidMethodReferenceJava.java"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 21"
- errorLine1=" if (Build.VERSION.SDK_INT >= 17) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/ClassVerificationFailureFromJava.java"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 21"
- errorLine1=" if (Build.VERSION.SDK_INT >= 19) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/sample/core/widget/ListViewCompat.java"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 21"
- errorLine1=" if (Build.VERSION.SDK_INT >= 19) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/sample/core/widget/ListViewCompat.java"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 21"
- errorLine1=" if (Build.VERSION.SDK_INT >= 19) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/sample/core/widget/ListViewCompatKotlin.kt"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 21"
- errorLine1=" return if (Build.VERSION.SDK_INT >= 19) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/sample/core/widget/ListViewCompatKotlin.kt"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 19"
- errorLine1="@RequiresApi(19)"
- errorLine2="~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/RequiresApiJava.java"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 19"
- errorLine1="@RequiresApi(19)"
- errorLine2="~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/RequiresApiKotlin.kt"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 19"
- errorLine1=" @RequiresApi(19)"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/RequiresApiKotlin.kt"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 19"
- errorLine1="@RequiresApi(19)"
- errorLine2="~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/RequiresApiKotlin.kt"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 19 from outer annotation (`@RequiresApi(19)`)"
- errorLine1=" @RequiresApi(16)"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/RequiresApiKotlin.kt"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 16"
- errorLine1="@RequiresApi(16)"
- errorLine2="~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/RequiresApiKotlin.kt"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 16"
- errorLine1=" @RequiresApi(16)"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/RequiresApiKotlin.kt"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 16"
- errorLine1="@RequiresApi(16)"
- errorLine2="~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/RequiresApiKotlin.kt"/>
- </issue>
-
- <issue
- id="ObsoleteSdkInt"
- message="Unnecessary; `SDK_INT` is always >= 16"
- errorLine1=" @RequiresApi(16)"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/RequiresApiKotlin.kt"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public void callVarArgsMethodNoArgs(BaseAdapter adapter) {"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixOnUnsafeCallWithImplicitVarArgsCast.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public void callVarArgsMethodOneArg(BaseAdapter adapter, CharBuffer vararg) {"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixOnUnsafeCallWithImplicitVarArgsCast.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public void callVarArgsMethodOneArg(BaseAdapter adapter, CharBuffer vararg) {"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixOnUnsafeCallWithImplicitVarArgsCast.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public void callVarArgsMethodManyArgs(BaseAdapter adapter, CharBuffer vararg1,"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixOnUnsafeCallWithImplicitVarArgsCast.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public void callVarArgsMethodManyArgs(BaseAdapter adapter, CharBuffer vararg1,"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixOnUnsafeCallWithImplicitVarArgsCast.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" CharBuffer vararg2, CharBuffer vararg3) {"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixOnUnsafeCallWithImplicitVarArgsCast.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" CharBuffer vararg2, CharBuffer vararg3) {"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixOnUnsafeCallWithImplicitVarArgsCast.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public void unsafeReferenceOnCastObject(Object secretDisplayCutout) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeCallOnCast.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public void castReceiver(Notification.MessagingStyle style, Notification.Builder builder) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeCallWithImplicitParamCast.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public void castReceiver(Notification.MessagingStyle style, Notification.Builder builder) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeCallWithImplicitParamCast.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public void castParameter(Notification.Builder builder, Notification.CarExtender extender) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeCallWithImplicitParamCast.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public void castParameter(Notification.Builder builder, Notification.CarExtender extender) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeCallWithImplicitParamCast.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public Drawable createAdaptiveIconDrawableReturnDrawable() {"
- errorLine2=" ~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeCallWithImplicitReturnCast.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public AdaptiveIconDrawable createAndReturnAdaptiveIconDrawable() {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeCallWithImplicitReturnCast.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public Object methodReturnsIconAsObject() {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeCallWithImplicitReturnCast.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public Icon methodReturnsIconAsIcon() {"
- errorLine2=" ~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeCallWithImplicitReturnCast.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public Notification.DecoratedCustomViewStyle callQualifiedConstructor() {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeConstructorQualifiedClass.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public PrintAttributes.Builder unsafeReferenceWithQualifiedClasses("
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeMethodWithQualifiedClass.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" PrintAttributes.Builder builder,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeMethodWithQualifiedClass.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" PrintAttributes.MediaSize mediaSize"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/AutofixUnsafeMethodWithQualifiedClass.java"/>
- </issue>
-
</issues>
diff --git a/lint-checks/integration-tests/src/main/java/androidx/AutofixOnUnsafeCallWithImplicitVarArgsCast.java b/lint-checks/integration-tests/src/main/java/androidx/AutofixOnUnsafeCallWithImplicitVarArgsCast.java
deleted file mode 100644
index 2128027..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/AutofixOnUnsafeCallWithImplicitVarArgsCast.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx;
-
-import android.widget.BaseAdapter;
-
-import androidx.annotation.RequiresApi;
-
-import java.nio.CharBuffer;
-
-/**
- * Contains unsafe calls to a method with a variable number of arguments which are implicitly cast.
- */
-@SuppressWarnings("unused")
-public class AutofixOnUnsafeCallWithImplicitVarArgsCast {
- /**
- * Calls the vararg method with no args.
- */
- @RequiresApi(27)
- public void callVarArgsMethodNoArgs(BaseAdapter adapter) {
- adapter.setAutofillOptions();
- }
-
- /**
- *Calls the vararg method with one args.
- */
- @RequiresApi(27)
- public void callVarArgsMethodOneArg(BaseAdapter adapter, CharBuffer vararg) {
- adapter.setAutofillOptions(vararg);
- }
-
- /**
- * Calls the vararg method with multiple args.
- */
- @RequiresApi(27)
- public void callVarArgsMethodManyArgs(BaseAdapter adapter, CharBuffer vararg1,
- CharBuffer vararg2, CharBuffer vararg3) {
- adapter.setAutofillOptions(vararg1, vararg2, vararg3);
- }
-}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallOnCast.java b/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallOnCast.java
deleted file mode 100644
index c3d7f33..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallOnCast.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright 2022 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
- *
- * http://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 androidx;
-
-import android.os.Build;
-import android.view.DisplayCutout;
-
-/**
- * Test class containing unsafe reference on cast object.
- */
-@SuppressWarnings("unused")
-public class AutofixUnsafeCallOnCast {
- /**
- * Method making unsafe reference on cast object.
- */
- public void unsafeReferenceOnCastObject(Object secretDisplayCutout) {
- if (Build.VERSION.SDK_INT >= 28) {
- ((DisplayCutout) secretDisplayCutout).getSafeInsetTop();
- }
- }
-}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallToThis.java b/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallToThis.java
deleted file mode 100644
index 6697751..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallToThis.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2022 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
- *
- * http://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 androidx;
-
-import android.os.Build;
-import android.view.ViewGroup;
-
-/**
- * Test class containing unsafe references to a parent's instance method.
- */
-@SuppressWarnings("unused")
-public abstract class AutofixUnsafeCallToThis extends ViewGroup {
- /*
- * Constructor to prevent complication error.
- */
- public AutofixUnsafeCallToThis() {
- super(null);
- }
-
- /**
- * Method making the unsafe reference on an implicit this.
- */
- public void unsafeReferenceOnImplicitThis() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- getClipToPadding();
- }
- }
-
- /**
- * Method making the unsafe reference on an explicit this.
- */
- public void unsafeReferenceOnExplicitThis() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- this.getClipToPadding();
- }
- }
-
- /**
- * Method making the unsafe reference on an explicit super.
- */
- public void unsafeReferenceOnExplicitSuper() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- super.getClipToPadding();
- }
- }
-}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallWithImplicitParamCast.java b/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallWithImplicitParamCast.java
deleted file mode 100644
index cc20718..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallWithImplicitParamCast.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx;
-
-import android.app.Notification;
-
-import androidx.annotation.RequiresApi;
-
-/**
- * Tests to ensure the generated lint fix does not leave in implicit casts from a new argument type.
- */
-@SuppressWarnings("unused")
-public class AutofixUnsafeCallWithImplicitParamCast {
- /**
- * This uses the Notification.MessagingStyle type, but setBuilder is defined on
- * Notification.Style, and the two classes were introduced in different API levels.
- */
- @RequiresApi(24)
- public void castReceiver(Notification.MessagingStyle style, Notification.Builder builder) {
- style.setBuilder(builder);
- }
-
- /**
- * This uses Notification.CarExtender, but extend is defined with Notification.Extender as a
- * parameter, and the two classes were introduced at different API levels.
- */
- @RequiresApi(23)
- public void castParameter(Notification.Builder builder, Notification.CarExtender extender) {
- builder.extend(extender);
- }
-}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallWithImplicitReturnCast.java b/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallWithImplicitReturnCast.java
deleted file mode 100644
index 0eed5cb..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeCallWithImplicitReturnCast.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright 2022 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
- *
- * http://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 androidx;
-
-import android.app.Notification;
-import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.Icon;
-
-import androidx.annotation.RequiresApi;
-
-/**
- * Tests to ensure the generated lint fix does not leave in implicit casts from a new return type.
- */
-@SuppressWarnings("unused")
-public abstract class AutofixUnsafeCallWithImplicitReturnCast {
- /**
- * This method creates an AdaptiveIconDrawable and implicitly casts it to Drawable.
- */
- @RequiresApi(26)
- public Drawable createAdaptiveIconDrawableReturnDrawable() {
- return new AdaptiveIconDrawable(null, null);
- }
-
- /**
- * This method also creates an AdaptiveIconDrawable but does not cast it to Drawable.
- */
- @RequiresApi(26)
- public AdaptiveIconDrawable createAndReturnAdaptiveIconDrawable() {
- return new AdaptiveIconDrawable(null, null);
- }
-
- /**
- * This calls a method returning an Icon and implicitly casts it to Object.
- */
- @RequiresApi(26)
- public Object methodReturnsIconAsObject() {
- return Icon.createWithAdaptiveBitmap(null);
- }
-
- /**
- * This calls a method returning an Icon and returns it as an Icon.
- */
- @RequiresApi(26)
- public Icon methodReturnsIconAsIcon() {
- return Icon.createWithAdaptiveBitmap(null);
- }
-
- /**
- * This uses the constructed value as Notification.Style in a method call.
- */
- @RequiresApi(24)
- public void methodUsesStyleAsParam() {
- useStyle(new Notification.DecoratedCustomViewStyle());
- }
-
- /**
- * This is here so there's a method to use the DecoratedCustomViewStyle as a Style.
- */
- static boolean useStyle(Notification.Style style) {
- return false;
- }
-}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeConstructorQualifiedClass.java b/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeConstructorQualifiedClass.java
deleted file mode 100644
index fbf96f9..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeConstructorQualifiedClass.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2022 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
- *
- * http://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 androidx;
-
-import android.app.Notification;
-
-import androidx.annotation.RequiresApi;
-
-/**
- * Class containing an unsafe constructor reference that uses a qualified type.
- */
-public class AutofixUnsafeConstructorQualifiedClass {
- /**
- * In the generated fix, the constructor call should not be `new DecoratedCustomViewStyle()`.
- */
- @RequiresApi(24)
- public Notification.DecoratedCustomViewStyle callQualifiedConstructor() {
- return new Notification.DecoratedCustomViewStyle();
- }
-}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeConstructorReferenceJava.java b/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeConstructorReferenceJava.java
deleted file mode 100644
index 6aac186..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeConstructorReferenceJava.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright 2021 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
- *
- * http://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 androidx;
-
-import android.content.Context;
-import android.os.Build;
-import android.view.View;
-import android.view.accessibility.AccessibilityNodeInfo;
-
-/**
- * Test class containing unsafe method references.
- */
-@SuppressWarnings("unused")
-public class AutofixUnsafeConstructorReferenceJava {
-
- /**
- * Unsafe reference to a new API with an SDK_INT check that satisfies the NewApi lint.
- */
- void unsafeReferenceWithSdkCheck(Context context) {
- if (Build.VERSION.SDK_INT >= 30) {
- AccessibilityNodeInfo node = new AccessibilityNodeInfo(new View(context), 1);
- }
- }
-}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeGenericMethodReferenceJava.java b/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeGenericMethodReferenceJava.java
deleted file mode 100644
index 5628007..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeGenericMethodReferenceJava.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright 2021 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
- *
- * http://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 androidx;
-
-import android.content.Context;
-import android.os.Build;
-
-/**
- * Test class containing unsafe method references.
- */
-@SuppressWarnings("unused")
-public class AutofixUnsafeGenericMethodReferenceJava {
-
- /**
- * Unsafe reference to a generically-typed method with an SDK_INT check that satisfies the
- * NewApi lint.
- */
- <T> T getSystemService(Context context, Class<T> serviceClass) {
- if (Build.VERSION.SDK_INT >= 23) {
- return context.getSystemService(serviceClass);
- }
- return null;
- }
-}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeMethodWithQualifiedClass.java b/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeMethodWithQualifiedClass.java
deleted file mode 100644
index 134b28f..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeMethodWithQualifiedClass.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2022 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
- *
- * http://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 androidx;
-
-import android.print.PrintAttributes;
-
-import androidx.annotation.RequiresApi;
-
-/**
- * Test class containing unsafe method reference that uses qualified class names.
- */
-@SuppressWarnings("unused")
-public class AutofixUnsafeMethodWithQualifiedClass {
- /**
- * This method call:
- * - has a receiver of type PrintAttributes.Builder
- * - takes param of type PrintAttributes.MediaSize
- * - has return type PrintAttributes.Builder
- * In the generated fix, all three types should appear as qualified class names.
- */
- @RequiresApi(19)
- public PrintAttributes.Builder unsafeReferenceWithQualifiedClasses(
- PrintAttributes.Builder builder,
- PrintAttributes.MediaSize mediaSize
- ) {
- return builder.setMediaSize(mediaSize);
- }
-}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeReferenceWithExistingClassJava.java b/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeReferenceWithExistingClassJava.java
deleted file mode 100644
index eca9596..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeReferenceWithExistingClassJava.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2021 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
- *
- * http://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 androidx;
-
-import android.content.res.ColorStateList;
-import android.os.Build;
-import android.view.View;
-
-import androidx.annotation.RequiresApi;
-
-/**
- * Test class containing unsafe method references.
- */
-@SuppressWarnings("unused")
-public class AutofixUnsafeReferenceWithExistingClassJava {
-
- /**
- * Unsafe reference to a new API with an SDK_INT check that satisfies the NewApi lint.
- */
- void unsafeReferenceWithSdkCheck(View view) {
- if (Build.VERSION.SDK_INT >= 21) {
- view.setBackgroundTintList(new ColorStateList(null, null));
- }
- }
-
- @RequiresApi(23)
- static class Api23Impl {
- private Api23Impl() {
- // This class is not instantiable.
- }
- }
-}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeReferenceWithExistingFix.java b/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeReferenceWithExistingFix.java
deleted file mode 100644
index efa821b..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeReferenceWithExistingFix.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright 2022 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
- *
- * http://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 androidx;
-
-import android.content.res.ColorStateList;
-import android.graphics.drawable.Drawable;
-import android.view.View;
-
-import androidx.annotation.DoNotInline;
-import androidx.annotation.RequiresApi;
-
-/**
- * Test class containing unsafe method references.
- */
-@SuppressWarnings("unused")
-public class AutofixUnsafeReferenceWithExistingFix {
-
- /**
- * Unsafe reference to a new API with an already existing fix method in Api21Impl.
- */
- @RequiresApi(21)
- void unsafeReferenceFixMethodExists(View view) {
- view.setBackgroundTintList(new ColorStateList(null, null));
- }
-
- /**
- * Unsafe reference to a new API without an existing fix method, but requiring API 21.
- */
- @RequiresApi(21)
- void unsafeReferenceFixClassExists(Drawable drawable) {
- drawable.getOutline(null);
- }
-
- @RequiresApi(21)
- static class Api21Impl {
- private Api21Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static void setBackgroundTintList(View view, ColorStateList tint) {
- view.setBackgroundTintList(tint);
- }
- }
-}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeStaticMethodReferenceJava.java b/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeStaticMethodReferenceJava.java
deleted file mode 100644
index 4aec0ab..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeStaticMethodReferenceJava.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2021 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
- *
- * http://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 androidx;
-
-import android.os.Build;
-import android.view.View;
-
-/**
- * Test class containing unsafe method references.
- */
-@SuppressWarnings("unused")
-public class AutofixUnsafeStaticMethodReferenceJava {
-
- /**
- * Unsafe reference to a new API with an SDK_INT check that satisfies the NewApi lint.
- */
- int unsafeReferenceWithSdkCheck() {
- if (Build.VERSION.SDK_INT >= 17) {
- return View.generateViewId();
- }
- return -1;
- }
-}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeVoidMethodReferenceJava.java b/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeVoidMethodReferenceJava.java
deleted file mode 100644
index ec43f28..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeVoidMethodReferenceJava.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2021 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
- *
- * http://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 androidx;
-
-import android.content.res.ColorStateList;
-import android.os.Build;
-import android.view.View;
-
-/**
- * Test class containing unsafe method references.
- */
-@SuppressWarnings("unused")
-public class AutofixUnsafeVoidMethodReferenceJava {
-
- /**
- * Unsafe reference to a new API with an SDK_INT check that satisfies the NewApi lint.
- */
- void unsafeReferenceWithSdkCheck(View view) {
- if (Build.VERSION.SDK_INT >= 21) {
- view.setBackgroundTintList(new ColorStateList(null, null));
- }
- }
-}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/ClassVerificationFailureFromJava.java b/lint-checks/integration-tests/src/main/java/androidx/ClassVerificationFailureFromJava.java
deleted file mode 100644
index 9aaa657..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/ClassVerificationFailureFromJava.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright 2021 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
- *
- * http://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 androidx;
-
-import android.content.res.ColorStateList;
-import android.os.Build;
-import android.view.View;
-
-import androidx.annotation.RequiresApi;
-
-/**
- * Test class containing class verification failure scenarios.
- */
-@SuppressWarnings("unused")
-public class ClassVerificationFailureFromJava {
-
- /**
- * Unsafe reference to a new API with an SDK_INT check that satisfies the NewApi lint.
- */
- void unsafeReferenceWithSdkCheck(View view) {
- if (Build.VERSION.SDK_INT > 23) {
- ColorStateList tint = new ColorStateList(null, null);
- view.setBackgroundTintList(tint);
- }
- }
-
- /**
- * Unsafe static reference to a new API with an SDK_INT check that satisfies the NewApi lint.
- */
- int unsafeStaticReferenceWithSdkCheck() {
- if (Build.VERSION.SDK_INT >= 17) {
- return View.generateViewId();
- } else {
- return -1;
- }
- }
-
- /**
- * Unsafe reference to a new API whose auto-fix collides with the existing Api28Impl class.
- */
- CharSequence unsafeReferenceWithAutoFixCollision(View view) {
- return view.getAccessibilityClassName();
- }
-
- /**
- * Safe reference to a new API on a static inner class.
- */
- CharSequence safeGetAccessibilityPaneTitle(View view) {
- if (Build.VERSION.SDK_INT >= 28) {
- return Api28Impl.getAccessibilityPaneTitle(view);
- } else {
- return null;
- }
- }
-
- @RequiresApi(28)
- static class Api28Impl {
-
- private Api28Impl() {
- // Not instantiable.
- }
-
- public static CharSequence getAccessibilityPaneTitle(View view) {
- return view.getAccessibilityPaneTitle();
- }
- }
-}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/RequiresApiJava.java b/lint-checks/integration-tests/src/main/java/androidx/RequiresApiJava.java
deleted file mode 100644
index f90b49b..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/RequiresApiJava.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2021 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
- *
- * http://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 androidx;
-
-import androidx.annotation.RequiresApi;
-
-/**
- * Character.isSurrogate requires API 19. Prior to addressing b/202415535, lint did not detect the
- * presence of @RequiresApi annotation in an outer class, and incorrectly flagged MyStaticClass
- * as needing a @RequiresApi annotation.
- */
-@RequiresApi(19)
-final class RequiresApiJava {
-
- // RequiresApi annotation should not be needed here; already present in containing class
- // @RequiresApi(19)
- public static final class MyStaticClass {
-
- private MyStaticClass() {
- // Not instantiable.
- }
-
- static int myStaticMethod(char c) {
- Character.isSurrogate(c);
- return 0;
- }
- }
-}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/RequiresApiKotlin.kt b/lint-checks/integration-tests/src/main/java/androidx/RequiresApiKotlin.kt
deleted file mode 100644
index a8cf4d0..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/RequiresApiKotlin.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright 2021 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
- *
- * http://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 androidx
-
-import androidx.annotation.RequiresApi
-
-/*
- * Character.isSurrogate requires API 19. Prior to addressing b/202415535, the
- * ClassVerificationFailure detector only checked for the @RequiresApi annotation on a method's
- * immediate containing class, and did not factor in @RequiresApi annotations on outer class(es).
- *
- * This sample file covers various cases of nested @RequiresApi usage.
- */
-
-// RequiresApi annotation not needed on MyStaticClass since it's already present in containing class
-@RequiresApi(19)
-internal class RequiresApiKotlinOuter19Passes {
- object MyStaticClass {
- fun MyStaticMethod(c: Char): Int {
- Character.isSurrogate(c)
- return 0
- }
- }
-}
-
-// @RequiresApi declaration on MyStaticMethod's immediate containing class
-internal class RequiresApiKotlinInner19Passes {
- @RequiresApi(19)
- object MyStaticClass {
- fun MyStaticMethod(c: Char): Int {
- Character.isSurrogate(c)
- return 0
- }
- }
-}
-
-// Even though MyStaticClass declares a @RequiresApi that is too low, the outer containing class's
-// definition of 19 will override it.
-@RequiresApi(19)
-internal class RequiresApiKotlinInner16Outer19Passes {
- @RequiresApi(16)
- object MyStaticClass {
- fun MyStaticMethod(c: Char): Int {
- Character.isSurrogate(c)
- return 0
- }
- }
-}
-
-internal class RequiresApiKotlinNoAnnotationFails {
- object MyStaticClass {
- fun MyStaticMethod(c: Char): Int {
- Character.isSurrogate(c)
- return 0
- }
- }
-}
-
-@RequiresApi(16)
-internal class RequiresApiKotlinOuter16Fails {
- object MyStaticClass {
- fun MyStaticMethod(c: Char): Int {
- Character.isSurrogate(c)
- return 0
- }
- }
-}
-
-internal class RequiresApiKotlinInner16Fails {
- @RequiresApi(16)
- object MyStaticClass {
- fun MyStaticMethod(c: Char): Int {
- Character.isSurrogate(c)
- return 0
- }
- }
-}
-
-@RequiresApi(16)
-internal class RequiresApiKotlinInner16Outer16Fails {
- @RequiresApi(16)
- object MyStaticClass {
- fun MyStaticMethod(c: Char): Int {
- Character.isSurrogate(c)
- return 0
- }
- }
-}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/sample/appcompat/widget/ActionBarBackgroundDrawable.java b/lint-checks/integration-tests/src/main/java/androidx/sample/appcompat/widget/ActionBarBackgroundDrawable.java
deleted file mode 100644
index 6fe87e2..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/sample/appcompat/widget/ActionBarBackgroundDrawable.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2015 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
- *
- * http://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 androidx.sample.appcompat.widget;
-
-import android.graphics.Canvas;
-import android.graphics.ColorFilter;
-import android.graphics.Outline;
-import android.graphics.PixelFormat;
-import android.graphics.drawable.Drawable;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-
-@SuppressWarnings({"unused", "deprecation"})
-class ActionBarBackgroundDrawable extends Drawable {
-
- final ActionBarContainerStub mContainer;
-
- ActionBarBackgroundDrawable(ActionBarContainerStub container) {
- mContainer = container;
- }
-
- @Override
- public void draw(@NonNull Canvas canvas) {
- if (mContainer.mIsSplit) {
- if (mContainer.mSplitBackground != null) {
- mContainer.mSplitBackground.draw(canvas);
- }
- } else {
- if (mContainer.mBackground != null) {
- mContainer.mBackground.draw(canvas);
- }
- if (mContainer.mStackedBackground != null && mContainer.mIsStacked) {
- mContainer.mStackedBackground.draw(canvas);
- }
- }
- }
-
- @Override
- public void setAlpha(int alpha) {
- }
-
- @Override
- public void setColorFilter(ColorFilter cf) {
- }
-
- @Override
- public int getOpacity() {
- return PixelFormat.UNKNOWN;
- }
-
- @Override
- @RequiresApi(21)
- public void getOutline(@NonNull Outline outline) {
- if (mContainer.mIsSplit) {
- if (mContainer.mSplitBackground != null) {
- mContainer.mSplitBackground.getOutline(outline);
- }
- } else {
- // ignore the stacked background for shadow casting
- if (mContainer.mBackground != null) {
- mContainer.mBackground.getOutline(outline);
- }
- }
- }
-
- // Stub implementation to avoid appcompat dependency.
- static class ActionBarContainerStub {
- Drawable mBackground;
- Drawable mSplitBackground;
- Drawable mStackedBackground;
-
- boolean mIsSplit;
- boolean mIsStacked;
- }
-}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/sample/core/widget/ListViewCompat.java b/lint-checks/integration-tests/src/main/java/androidx/sample/core/widget/ListViewCompat.java
deleted file mode 100644
index 216ef20..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/sample/core/widget/ListViewCompat.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright 2021 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
- *
- * http://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 androidx.sample.core.widget;
-
-import android.os.Build;
-import android.view.View;
-import android.widget.ListView;
-
-import androidx.annotation.NonNull;
-
-/**
- * Helper for accessing features in {@link ListView}
- */
-public class ListViewCompat {
-
- /**
- * Scrolls the list items within the view by a specified number of pixels.
- *
- * @param listView the list to scroll
- * @param y the amount of pixels to scroll by vertically
- */
- public static void scrollListBy(@NonNull ListView listView, int y) {
- if (Build.VERSION.SDK_INT >= 19) {
- // Call the framework version directly
- listView.scrollListBy(y);
- } else {
- // provide backport on earlier versions
- final int firstPosition = listView.getFirstVisiblePosition();
- if (firstPosition == ListView.INVALID_POSITION) {
- return;
- }
-
- final View firstView = listView.getChildAt(0);
- if (firstView == null) {
- return;
- }
-
- final int newTop = firstView.getTop() - y;
- listView.setSelectionFromTop(firstPosition, newTop);
- }
- }
-
- /**
- * Check if the items in the list can be scrolled in a certain direction.
- *
- * @param direction Negative to check scrolling up, positive to check
- * scrolling down.
- * @return true if the list can be scrolled in the specified direction,
- * false otherwise.
- * @see #scrollListBy(ListView, int)
- */
- public static boolean canScrollList(@NonNull ListView listView, int direction) {
- if (Build.VERSION.SDK_INT >= 19) {
- // Call the framework version directly
- return listView.canScrollList(direction);
- } else {
- // provide backport on earlier versions
- final int childCount = listView.getChildCount();
- if (childCount == 0) {
- return false;
- }
-
- final int firstPosition = listView.getFirstVisiblePosition();
- if (direction > 0) {
- final int lastBottom = listView.getChildAt(childCount - 1).getBottom();
- final int lastPosition = firstPosition + childCount;
- return lastPosition < listView.getCount()
- || (lastBottom > listView.getHeight() - listView.getListPaddingBottom());
- } else {
- final int firstTop = listView.getChildAt(0).getTop();
- return firstPosition > 0 || firstTop < listView.getListPaddingTop();
- }
- }
- }
-
- private ListViewCompat() {}
-}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/sample/core/widget/ListViewCompatKotlin.kt b/lint-checks/integration-tests/src/main/java/androidx/sample/core/widget/ListViewCompatKotlin.kt
deleted file mode 100644
index d5e62a0..0000000
--- a/lint-checks/integration-tests/src/main/java/androidx/sample/core/widget/ListViewCompatKotlin.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright 2021 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
- *
- * http://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 androidx.sample.core.widget
-
-import android.os.Build
-import android.widget.ListView
-
-@Suppress("unused")
-object ListViewCompatKotlin {
- /**
- * Scrolls the list items within the view by a specified number of pixels.
- *
- * @param listView the list to scroll
- * @param y the amount of pixels to scroll by vertically
- */
- fun scrollListBy(listView: ListView, y: Int) {
- if (Build.VERSION.SDK_INT >= 19) {
- // Call the framework version directly
- listView.scrollListBy(y)
- } else {
- // provide backport on earlier versions
- val firstPosition = listView.firstVisiblePosition
- if (firstPosition == ListView.INVALID_POSITION) {
- return
- }
- val firstView = listView.getChildAt(0) ?: return
- val newTop = firstView.top - y
- listView.setSelectionFromTop(firstPosition, newTop)
- }
- }
-
- /**
- * Check if the items in the list can be scrolled in a certain direction.
- *
- * @param direction Negative to check scrolling up, positive to check scrolling down.
- * @return true if the list can be scrolled in the specified direction, false otherwise.
- * @see .scrollListBy
- */
- fun canScrollList(listView: ListView, direction: Int): Boolean {
- return if (Build.VERSION.SDK_INT >= 19) {
- // Call the framework version directly
- listView.canScrollList(direction)
- } else {
- // provide backport on earlier versions
- val childCount = listView.childCount
- if (childCount == 0) {
- return false
- }
- val firstPosition = listView.firstVisiblePosition
- if (direction > 0) {
- val lastBottom = listView.getChildAt(childCount - 1).bottom
- val lastPosition = firstPosition + childCount
- lastPosition < listView.count ||
- lastBottom > listView.height - listView.listPaddingBottom
- } else {
- val firstTop = listView.getChildAt(0).top
- firstPosition > 0 || firstTop < listView.listPaddingTop
- }
- }
- }
-}
diff --git a/media/media/build.gradle b/media/media/build.gradle
index 6643b06..ea92180 100644
--- a/media/media/build.gradle
+++ b/media/media/build.gradle
@@ -47,12 +47,6 @@
buildFeatures {
aidl = true
}
- sourceSets {
- main.java.srcDirs += [
- ]
- main.res.srcDirs += "src/main/res-public"
- }
-
buildTypes.configureEach {
consumerProguardFiles "proguard-rules.pro"
}
diff --git a/media/media/src/main/res-public/values/public_styles.xml b/media/media/src/main/res/values/public_styles.xml
similarity index 100%
rename from media/media/src/main/res-public/values/public_styles.xml
rename to media/media/src/main/res/values/public_styles.xml
diff --git a/metrics/integration-tests/janktest/build.gradle b/metrics/integration-tests/janktest/build.gradle
index 7b24dd4..6a93329 100644
--- a/metrics/integration-tests/janktest/build.gradle
+++ b/metrics/integration-tests/janktest/build.gradle
@@ -30,11 +30,11 @@
dependencies {
implementation(libs.kotlinStdlib)
- implementation project(":recyclerview:recyclerview")
- implementation project(":navigation:navigation-fragment-ktx")
- implementation project(":navigation:navigation-ui-ktx")
+ implementation(project(":recyclerview:recyclerview"))
+ implementation(project(":navigation:navigation-fragment-ktx"))
+ implementation(project(":navigation:navigation-ui-ktx"))
implementation("androidx.appcompat:appcompat:1.1.0")
- implementation project(":metrics:metrics-performance")
+ implementation(project(":metrics:metrics-performance"))
implementation("com.google.android.material:material:1.2.1")
implementation("androidx.constraintlayout:constraintlayout:2.0.1")
}
diff --git a/navigation/integration-tests/safeargs-testapp/buildSrc/build.gradle b/navigation/integration-tests/safeargs-testapp/buildSrc/build.gradle
index f2f9b2f..54f4282 100644
--- a/navigation/integration-tests/safeargs-testapp/buildSrc/build.gradle
+++ b/navigation/integration-tests/safeargs-testapp/buildSrc/build.gradle
@@ -37,7 +37,7 @@
apply plugin: "java"
dependencies {
- compile project(":moar-buildSrc")
+ api(project(":moar-buildSrc"))
}
tasks["build"].dependsOn unzip
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index c5f237a..fcb4f68 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -94,12 +94,10 @@
description = "Compose integration with Navigation"
samples(project(":navigation:navigation-compose:navigation-compose-samples"))
kotlinTarget = KotlinTarget.KOTLIN_1_9
+ addGoldenImageAssets()
}
android {
compileSdk = 35
- sourceSets.androidTest.assets.srcDirs +=
- project.rootDir.absolutePath + "/../../golden/navigation/navigation-compose"
-
namespace = "androidx.navigation.compose"
}
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHostController.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHostController.kt
index baeeaa4..0eb9377 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHostController.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHostController.kt
@@ -17,7 +17,6 @@
package androidx.navigation.compose
import android.content.Context
-import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
@@ -76,7 +75,7 @@
/** Saver to save and restore the NavController across config change and process death. */
private fun NavControllerSaver(context: Context): Saver<NavHostController, *> =
- Saver<NavHostController, Bundle>(
+ Saver(
save = { it.saveState() },
restore = { createNavController(context).apply { restoreState(it) } }
)
diff --git a/navigation/navigation-runtime-truth/build.gradle b/navigation/navigation-runtime-truth/build.gradle
deleted file mode 100644
index de14d57..0000000
--- a/navigation/navigation-runtime-truth/build.gradle
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2019 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
- *
- * http://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.
- */
-
-/**
- * This file was created using the `create_project.py` script located in the
- * `<AndroidX root>/development/project-creator` directory.
- *
- * Please use that script when creating a new project, rather than copying an existing project and
- * modifying its settings.
- */
-import androidx.build.Publish
-
-plugins {
- id("AndroidXPlugin")
- id("com.android.library")
- id("org.jetbrains.kotlin.android")
-}
-
-dependencies {
- api(project(":navigation:navigation-runtime"))
- api(libs.truth)
- api(libs.kotlinStdlib)
-
- implementation(project(":navigation:navigation-common"))
-
- androidTestImplementation(project(":internal-testutils-truth"))
- androidTestImplementation(project(":internal-testutils-navigation"), {
- exclude group: "androidx.navigation", module: "navigation-common"
- })
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testRules)
-}
-
-androidx {
- name = "Navigation Runtime Truth"
- publish = Publish.NONE
- inceptionYear = "2019"
- description = "Android Navigation-Runtime-Truth"
-}
-
-android {
- namespace = "androidx.navigation.truth"
-}
diff --git a/navigation/navigation-runtime-truth/src/androidTest/AndroidManifest.xml b/navigation/navigation-runtime-truth/src/androidTest/AndroidManifest.xml
deleted file mode 100644
index 23f87a3..0000000
--- a/navigation/navigation-runtime-truth/src/androidTest/AndroidManifest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2017 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
- ~
- ~ http://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.
- -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android">
-</manifest>
diff --git a/navigation/navigation-runtime-truth/src/androidTest/java/androidx/navigation/truth/NavControllerSubjectTest.kt b/navigation/navigation-runtime-truth/src/androidTest/java/androidx/navigation/truth/NavControllerSubjectTest.kt
deleted file mode 100644
index 998ffde..0000000
--- a/navigation/navigation-runtime-truth/src/androidTest/java/androidx/navigation/truth/NavControllerSubjectTest.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2019 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
- *
- * http://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 androidx.navigation.truth
-
-import androidx.navigation.NavController
-import androidx.navigation.plusAssign
-import androidx.navigation.truth.NavControllerSubject.Companion.assertThat
-import androidx.navigation.truth.test.R
-import androidx.test.annotation.UiThreadTest
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.filters.SmallTest
-import androidx.testutils.TestNavigator
-import androidx.testutils.assertThrows
-import org.junit.Before
-import org.junit.Test
-
-@SmallTest
-class NavControllerSubjectTest {
- private lateinit var navController: NavController
-
- @UiThreadTest
- @Before
- fun setUp() {
- navController =
- NavController(ApplicationProvider.getApplicationContext()).apply {
- navigatorProvider += TestNavigator()
- setGraph(R.navigation.test_graph)
- }
- }
-
- @Test
- fun testIsCurrentDestination() {
- assertThat(navController).isCurrentDestination(R.id.start_test)
- }
-
- @Test
- fun testIsCurrentDestinationFailure() {
- with(assertThrows { assertThat(navController).isCurrentDestination(R.id.second_test) }) {
- factValue("expected id").isEqualTo("0x${R.id.second_test.toString(16)}")
- factValue("but was")
- .isEqualTo("0x${navController.currentDestination?.id?.toString(16)}")
- factValue("current destination is")
- .isEqualTo(navController.currentDestination.toString())
- }
- }
-
- @Test
- fun testIsGraph() {
- assertThat(navController).isGraph(R.id.test_graph)
- }
-
- @Test
- fun testIsGraphFailure() {
- with(assertThrows { assertThat(navController).isGraph(R.id.second_test_graph) }) {
- factValue("expected id").isEqualTo("0x${R.id.second_test_graph.toString(16)}")
- factValue("but was").isEqualTo("0x${navController.graph.id.toString(16)}")
- factValue("current graph is").isEqualTo(navController.graph.toString())
- }
- }
-}
diff --git a/navigation/navigation-runtime-truth/src/androidTest/res/navigation/test_graph.xml b/navigation/navigation-runtime-truth/src/androidTest/res/navigation/test_graph.xml
deleted file mode 100644
index 5ac317b..0000000
--- a/navigation/navigation-runtime-truth/src/androidTest/res/navigation/test_graph.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2019 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
-
- http://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.
- -->
-<navigation
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- app:startDestination="@+id/start_test"
- android:id="@+id/test_graph"
- android:label="test_label">
-
- <test android:id="@+id/start_test">
- <action android:id="@+id/second" app:destination="@+id/second_test" />
- </test>
-
- <test android:id="@+id/second_test" />
-</navigation>
diff --git a/navigation/navigation-runtime-truth/src/main/java/androidx/navigation/truth/NavControllerSubject.kt b/navigation/navigation-runtime-truth/src/main/java/androidx/navigation/truth/NavControllerSubject.kt
deleted file mode 100644
index 8e23a53..0000000
--- a/navigation/navigation-runtime-truth/src/main/java/androidx/navigation/truth/NavControllerSubject.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright 2019 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
- *
- * http://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 androidx.navigation.truth
-
-import android.annotation.SuppressLint
-import androidx.annotation.IdRes
-import androidx.navigation.NavController
-import com.google.common.truth.Fact.fact
-import com.google.common.truth.FailureMetadata
-import com.google.common.truth.Subject
-import com.google.common.truth.Truth.assertAbout
-
-/** A Truth Subject for making assertions about [NavController]. */
-class NavControllerSubject
-private constructor(metadata: FailureMetadata, private val actual: NavController) :
- Subject(metadata, actual) {
-
- /**
- * Assert that the [NavController] has the given current destination in its
- * [androidx.navigation.NavGraph].
- *
- * @param navDest The ID resource of a [androidx.navigation.NavDestination]
- */
- fun isCurrentDestination(@IdRes navDest: Int) {
- val actualDest = actual.currentDestination?.id
- if (actualDest != navDest) {
- failWithoutActual(
- fact("expected id", "0x${navDest.toString(16)}"),
- fact("but was", "0x${actualDest?.toString(16)}"),
- fact("current destination is", actual.currentDestination)
- )
- }
- }
-
- /**
- * Assert that the [NavController] has the given [androidx.navigation.NavGraph] as its current
- * graph.
- *
- * @param navGraph The ID resource of a [androidx.navigation.NavGraph]
- */
- fun isGraph(@IdRes navGraph: Int) {
- val actualGraph = actual.graph.id
- if (actualGraph != navGraph) {
- failWithoutActual(
- fact("expected id", "0x${navGraph.toString(16)}"),
- fact("but was", "0x${actualGraph.toString(16)}"),
- fact("current graph is", actual.graph)
- )
- }
- }
-
- companion object {
- @SuppressLint("MemberVisibilityCanBePrivate")
- val factory =
- Factory<NavControllerSubject, NavController> { metadata, actual ->
- NavControllerSubject(metadata, actual)
- }
-
- @JvmStatic
- fun assertThat(actual: NavController): NavControllerSubject {
- return assertAbout(factory).that(actual)
- }
- }
-}
diff --git a/navigation/navigation-runtime-truth/src/main/res/navigation/second_test_graph.xml b/navigation/navigation-runtime-truth/src/main/res/navigation/second_test_graph.xml
deleted file mode 100644
index 504c3f2..0000000
--- a/navigation/navigation-runtime-truth/src/main/res/navigation/second_test_graph.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2019 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
-
- http://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.
- -->
-
-<navigation xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/second_test_graph">
-
-</navigation>
\ No newline at end of file
diff --git a/navigation3/navigation3/samples/build.gradle b/navigation3/navigation3/samples/build.gradle
index cd517e3..4bc9f73 100644
--- a/navigation3/navigation3/samples/build.gradle
+++ b/navigation3/navigation3/samples/build.gradle
@@ -48,8 +48,8 @@
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
implementation(libs.kotlinSerializationCore)
implementation("androidx.savedstate:savedstate-ktx:1.3.0-alpha05")
- implementation project(":lifecycle:lifecycle-viewmodel-navigation3")
- implementation project(":navigation3:navigation3")
+ implementation(project(":lifecycle:lifecycle-viewmodel-navigation3"))
+ implementation(project(":navigation3:navigation3"))
}
diff --git a/paging/paging-compose/build.gradle b/paging/paging-compose/build.gradle
index 01ca1bd..41d4097 100644
--- a/paging/paging-compose/build.gradle
+++ b/paging/paging-compose/build.gradle
@@ -54,7 +54,7 @@
commonTest {
dependencies {
- implementation project(":compose:ui:ui-tooling")
+ implementation(project(":compose:ui:ui-tooling"))
implementation(project(":compose:test-utils"))
implementation(project(":internal-testutils-paging"))
}
diff --git a/pdf/integration-tests/testapp/src/main/kotlin/androidx/pdf/testapp/MainActivityV2.kt b/pdf/integration-tests/testapp/src/main/kotlin/androidx/pdf/testapp/MainActivityV2.kt
index 1fc9c41..353d61c 100644
--- a/pdf/integration-tests/testapp/src/main/kotlin/androidx/pdf/testapp/MainActivityV2.kt
+++ b/pdf/integration-tests/testapp/src/main/kotlin/androidx/pdf/testapp/MainActivityV2.kt
@@ -38,7 +38,12 @@
@VisibleForTesting
var filePicker: ActivityResultLauncher<String> =
registerForActivityResult(GetContent()) { uri: Uri? ->
- uri?.let { pdfViewerFragment?.documentUri = uri }
+ uri?.let {
+ if (pdfViewerFragment == null) {
+ setPdfView()
+ }
+ pdfViewerFragment?.documentUri = uri
+ }
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -54,9 +59,6 @@
val getContentButton: MaterialButton = findViewById(R.id.launch_button)
getContentButton.setOnClickListener { filePicker.launch(MIME_TYPE_PDF) }
- if (savedInstanceState == null) {
- setPdfView()
- }
}
private fun setPdfView() {
diff --git a/pdf/pdf-document-service/src/androidTest/kotlin/androidx/pdf/SandboxedPdfDocumentTest.kt b/pdf/pdf-document-service/src/androidTest/kotlin/androidx/pdf/SandboxedPdfDocumentTest.kt
index f40d303..dbc7510 100644
--- a/pdf/pdf-document-service/src/androidTest/kotlin/androidx/pdf/SandboxedPdfDocumentTest.kt
+++ b/pdf/pdf-document-service/src/androidTest/kotlin/androidx/pdf/SandboxedPdfDocumentTest.kt
@@ -33,6 +33,7 @@
import junit.framework.TestCase.assertFalse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@@ -151,6 +152,21 @@
}
}
+ @Test
+ fun searchDocument_fullDocumentSearch_withSinglePageResults() = runTest {
+ withDocument(PDF_DOCUMENT) { document ->
+ val query = "pages are all the same size"
+ val pageRange = 0..2
+
+ val results = document.searchDocument(query, pageRange)
+
+ // Assert sparse array doesn't contain empty result lists
+ assertEquals(1, results.size())
+ // Assert single result on first page
+ assertEquals(1, results[0].size)
+ }
+ }
+
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 13)
@Test
fun getSelectionBounds_returnsPageSelection() = runTest {
diff --git a/pdf/pdf-document-service/src/main/kotlin/androidx/pdf/SandboxedPdfDocument.kt b/pdf/pdf-document-service/src/main/kotlin/androidx/pdf/SandboxedPdfDocument.kt
index 774bb7a..097d35b 100644
--- a/pdf/pdf-document-service/src/main/kotlin/androidx/pdf/SandboxedPdfDocument.kt
+++ b/pdf/pdf-document-service/src/main/kotlin/androidx/pdf/SandboxedPdfDocument.kt
@@ -88,11 +88,14 @@
pageRange: IntRange
): SparseArray<List<PageMatchBounds>> {
return withDocument { document ->
- pageRange
- .map { pageNum ->
- document.searchPageText(pageNum, query).map { it.toContentClass() }
+ SparseArray<List<PageMatchBounds>>(pageRange.last + 1).apply {
+ pageRange.forEach { pageNum ->
+ document
+ .searchPageText(pageNum, query)
+ .takeIf { it.isNotEmpty() }
+ ?.let { put(pageNum, it.map { result -> result.toContentClass() }) }
}
- .toSparseArray(pageRange)
+ }
}
}
diff --git a/pdf/pdf-viewer-fragment/src/androidTest/assets/corrupted.pdf b/pdf/pdf-viewer-fragment/src/androidTest/assets/corrupted.pdf
new file mode 100644
index 0000000..1b76c16
--- /dev/null
+++ b/pdf/pdf-viewer-fragment/src/androidTest/assets/corrupted.pdf
Binary files differ
diff --git a/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/coroutines/FlowExtensions.kt b/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/coroutines/FlowExtensions.kt
index ecb464f..82c5714 100644
--- a/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/coroutines/FlowExtensions.kt
+++ b/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/coroutines/FlowExtensions.kt
@@ -16,15 +16,26 @@
package androidx.pdf.viewer.coroutines
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
-suspend fun <T> Flow<T>.toListDuring(durationInMillis: Long): List<T> = coroutineScope {
+internal suspend fun <T> Flow<T>.toListDuring(durationInMillis: Long): List<T> = coroutineScope {
val result = mutableListOf<T>()
val job = launch { [email protected](result::add) }
delay(durationInMillis)
job.cancel()
return@coroutineScope result
}
+
+internal suspend fun <T> Flow<T>.collectTill(
+ result: MutableList<T>,
+ predicate: suspend (value: T) -> Boolean
+) {
+ collect { value ->
+ result.add(value)
+ if (predicate(value)) throw CancellationException()
+ }
+}
diff --git a/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/fragment/PdfDocumentViewModelTest.kt b/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/fragment/PdfDocumentViewModelTest.kt
index e1ca79d..206433a 100644
--- a/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/fragment/PdfDocumentViewModelTest.kt
+++ b/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/fragment/PdfDocumentViewModelTest.kt
@@ -16,8 +16,10 @@
package androidx.pdf.viewer.fragment
+import android.net.Uri
import androidx.lifecycle.SavedStateHandle
import androidx.pdf.SandboxedPdfLoader
+import androidx.pdf.viewer.coroutines.collectTill
import androidx.pdf.viewer.coroutines.toListDuring
import androidx.pdf.viewer.fragment.TestUtils.openFileAsUri
import androidx.pdf.viewer.fragment.model.PdfFragmentUiState
@@ -110,6 +112,92 @@
assertTrue(pdfViewModel.fragmentUiScreenState.value !is PdfFragmentUiState.Loading)
}
- // TODO: Add tests for password-protected pdf and corrupted pdf after test-artifact b/379743760
+ @Test
+ fun test_pdfDocumentViewModel_loadDocumentFailure_corruptedPdf() = runTest {
+ val documentUri = openFileAsUri(appContext, CORRUPTED_PDF)
+ val uiStates = mutableListOf<PdfFragmentUiState>()
+ // start collecting Ui states
+ val collectJob = launch {
+ pdfDocumentViewModel.fragmentUiScreenState.collectTill(uiStates) { state ->
+ state is PdfFragmentUiState.DocumentError
+ }
+ }
+ // load pdf document
+ pdfDocumentViewModel.loadDocument(documentUri, null)
+
+ // wait till collection is completed
+ collectJob.join()
+
+ // Since we've selected a corrupted unprotected pdf,
+ // ideally there should only 2 states transitions.
+ assertTrue(uiStates.size == 2)
+ // Assert the first state emitted was loading
+ assertTrue(uiStates.first() is PdfFragmentUiState.Loading)
+ // Assert the last state emitted was Document error
+ assertTrue(
+ pdfDocumentViewModel.fragmentUiScreenState.value is PdfFragmentUiState.DocumentError
+ )
+ }
+
+ @Test
+ fun test_pdfDocumentViewModel_loadDocumentFailure_invalidUriPath() = runTest {
+ val documentUri =
+ Uri.parse("file:///data/data/com.example.app/invalid/path/to/nonexistent/file.pdf")
+
+ val uiStates = mutableListOf<PdfFragmentUiState>()
+ // start collecting Ui states
+ val collectJob = launch {
+ pdfDocumentViewModel.fragmentUiScreenState.collectTill(uiStates) { state ->
+ state is PdfFragmentUiState.DocumentError
+ }
+ }
+ // load pdf document
+ pdfDocumentViewModel.loadDocument(documentUri, null)
+
+ // wait till collection is completed
+ collectJob.join()
+
+ // Since we've selected a invalid Uri Path,
+ // ideally there should only 2 states transitions.
+ assertTrue(uiStates.size == 2)
+ // Assert the first state emitted was loading
+ assertTrue(uiStates.first() is PdfFragmentUiState.Loading)
+ // Assert the last state emitted was Document error
+ assertTrue(
+ pdfDocumentViewModel.fragmentUiScreenState.value is PdfFragmentUiState.DocumentError
+ )
+ }
+
+ @Test
+ fun test_pdfDocumentViewModel_loadDocumentFailure_invalidUriScheme() = runTest {
+ val documentUri = Uri.parse("xyz://path/to/sample.pdf")
+
+ val uiStates = mutableListOf<PdfFragmentUiState>()
+ // start collecting Ui states
+ val collectJob = launch {
+ pdfDocumentViewModel.fragmentUiScreenState.collectTill(uiStates) { state ->
+ state is PdfFragmentUiState.DocumentError
+ }
+ }
+ // load pdf document
+ pdfDocumentViewModel.loadDocument(documentUri, null)
+
+ // wait till collection is completed
+ collectJob.join()
+
+ // Since we've selected a invalid Uri Scheme,
+ // ideally there should only 2 states transitions.
+ assertTrue(uiStates.size == 2)
+ // Assert the first state emitted was loading
+ assertTrue(uiStates.first() is PdfFragmentUiState.Loading)
+ // Assert the last state emitted was Document error
+ assertTrue(
+ pdfDocumentViewModel.fragmentUiScreenState.value is PdfFragmentUiState.DocumentError
+ )
+ }
+
+ companion object {
+ private const val CORRUPTED_PDF = "corrupted.pdf"
+ }
}
diff --git a/pdf/pdf-viewer-fragment/src/main/kotlin/androidx/pdf/viewer/fragment/PdfViewerFragmentV2.kt b/pdf/pdf-viewer-fragment/src/main/kotlin/androidx/pdf/viewer/fragment/PdfViewerFragmentV2.kt
index c057893..8d3c566 100644
--- a/pdf/pdf-viewer-fragment/src/main/kotlin/androidx/pdf/viewer/fragment/PdfViewerFragmentV2.kt
+++ b/pdf/pdf-viewer-fragment/src/main/kotlin/androidx/pdf/viewer/fragment/PdfViewerFragmentV2.kt
@@ -21,6 +21,10 @@
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.LinearLayout.GONE
+import android.widget.LinearLayout.VISIBLE
+import android.widget.ProgressBar
+import android.widget.TextView
import androidx.annotation.CallSuper
import androidx.annotation.RestrictTo
import androidx.fragment.app.Fragment
@@ -128,6 +132,8 @@
}
private lateinit var pdfView: PdfView
+ private lateinit var errorView: TextView
+ private lateinit var loadingView: ProgressBar
override fun onCreateView(
inflater: LayoutInflater,
@@ -137,6 +143,8 @@
super.onCreateView(inflater, container, savedInstanceState)
val root = inflater.inflate(R.layout.pdf_viewer_fragment, container, false)
pdfView = root.findViewById(R.id.pdfView)
+ errorView = root.findViewById(R.id.errorTextView)
+ loadingView = root.findViewById(R.id.pdfLoadingProgressBar)
return root
}
@@ -163,25 +171,37 @@
private suspend fun collectFragmentUiScreenState() {
documentViewModel.fragmentUiScreenState.collect { uiState ->
when (uiState) {
- is Loading -> {
- // TODO: Implement loading view b/379226011
- // Hide all views except loading progress bar
- // Show progress bar
- }
- is PasswordRequested -> {
- // TODO: Implement password dialog b/373252814
- // Utilize retry param to show incorrect password on PasswordDialog
- }
- is DocumentLoaded -> {
- onLoadDocumentSuccess()
- pdfView.pdfDocument = uiState.pdfDocument
- // TODO: Implement PdfView b/379053734
- }
- is DocumentError -> {
- onLoadDocumentError(uiState.exception)
- // TODO: Implement error view b/379055053
- }
+ is Loading -> handleLoading()
+ is PasswordRequested -> handlePasswordRequested()
+ is DocumentLoaded -> handleDocumentLoaded(uiState)
+ is DocumentError -> handleDocumentError(uiState)
}
}
}
+
+ private fun handleLoading() {
+ setViewVisibility(pdfView = GONE, loadingView = VISIBLE, errorView = GONE)
+ }
+
+ private fun handlePasswordRequested() {
+ setViewVisibility(pdfView = GONE, loadingView = GONE, errorView = GONE)
+ // Utilize retry param to show incorrect password on PasswordDialog
+ }
+
+ private fun handleDocumentLoaded(uiState: DocumentLoaded) {
+ onLoadDocumentSuccess()
+ pdfView.pdfDocument = uiState.pdfDocument
+ setViewVisibility(pdfView = VISIBLE, loadingView = GONE, errorView = GONE)
+ }
+
+ private fun handleDocumentError(uiState: DocumentError) {
+ onLoadDocumentError(uiState.exception)
+ setViewVisibility(pdfView = GONE, loadingView = GONE, errorView = VISIBLE)
+ }
+
+ private fun setViewVisibility(pdfView: Int, loadingView: Int, errorView: Int) {
+ this.pdfView.visibility = pdfView
+ this.loadingView.visibility = loadingView
+ this.errorView.visibility = errorView
+ }
}
diff --git a/pdf/pdf-viewer-fragment/src/main/res/layout/pdf_viewer_fragment.xml b/pdf/pdf-viewer-fragment/src/main/res/layout/pdf_viewer_fragment.xml
index 4527500..e91c0c6 100644
--- a/pdf/pdf-viewer-fragment/src/main/res/layout/pdf_viewer_fragment.xml
+++ b/pdf/pdf-viewer-fragment/src/main/res/layout/pdf_viewer_fragment.xml
@@ -28,4 +28,30 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
+ <!-- ProgressBar for loading -->
+ <ProgressBar
+ android:id="@+id/pdfLoadingProgressBar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:visibility="gone"
+ android:indeterminate="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@+id/errorTextView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/error_cannot_open_pdf"
+ android:textColor="?attr/colorOnSurface"
+ android:textAppearance="?attr/textAppearanceBodyMedium"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ android:visibility="gone" />
+
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/build.gradle b/pdf/pdf-viewer/build.gradle
index 7581404..499d012 100644
--- a/pdf/pdf-viewer/build.gradle
+++ b/pdf/pdf-viewer/build.gradle
@@ -30,6 +30,7 @@
implementation(libs.kotlinStdlib)
implementation("androidx.exifinterface:exifinterface:1.3.2")
implementation("androidx.core:core:1.13.0")
+ implementation("androidx.customview:customview:1.2.0-alpha02")
implementation("androidx.annotation:annotation:1.7.0")
implementation("com.google.android.material:material:1.11.0")
implementation("com.google.errorprone:error_prone_annotations:2.30.0")
@@ -59,6 +60,7 @@
androidTestImplementation(libs.espressoCore)
androidTestImplementation(libs.kotlinCoroutinesTest)
androidTestImplementation(libs.mockitoKotlin4)
+ androidTestImplementation(libs.espressoIntents)
}
android {
diff --git a/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/search/SearchRepositoryTest.kt b/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/search/SearchRepositoryTest.kt
index 1b39c3ec..4c0c976 100644
--- a/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/search/SearchRepositoryTest.kt
+++ b/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/search/SearchRepositoryTest.kt
@@ -17,11 +17,9 @@
package androidx.pdf.search
import android.util.SparseArray
-import androidx.core.util.isEmpty
-import androidx.core.util.isNotEmpty
import androidx.pdf.content.PageMatchBounds
-import androidx.pdf.search.model.SearchResults
-import androidx.pdf.search.model.SelectedSearchResult
+import androidx.pdf.search.model.NoQuery
+import androidx.pdf.search.model.QueryResults
import androidx.pdf.view.FakePdfDocument
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
@@ -59,38 +57,42 @@
with(SearchRepository(fakePdfDocument)) {
// search document
- searchDocument(query = "test", currentVisiblePage = 5)
+ produceSearchResults(query = "test", currentVisiblePage = 5)
- val results = searchResults.value
+ var results = queryResults.value as QueryResults.Matched
// Assert results exists on 3 pages
- assertEquals(3, results.results.size())
- assertEquals(5, selectedSearchResult.value?.pageNum)
- assertEquals(0, selectedSearchResult.value?.currentIndex)
+ assertEquals(3, results.resultBounds.size())
+ assertEquals(5, results.queryResultsIndex.pageNum)
+ assertEquals(0, results.queryResultsIndex.resultBoundsIndex)
// fetch next result
- next()
+ produceNextResult()
+ results = queryResults.value as QueryResults.Matched
// Assert selectedSearchResult point to next result on same page
- assertEquals(5, selectedSearchResult.value?.pageNum)
- assertEquals(1, selectedSearchResult.value?.currentIndex)
+ assertEquals(5, results.queryResultsIndex.pageNum)
+ assertEquals(1, results.queryResultsIndex.resultBoundsIndex)
// fetch next result
- next()
+ produceNextResult()
+ results = queryResults.value as QueryResults.Matched
// Assert selectedSearchResult point to next result on next page
// in forward direction
- assertEquals(10, selectedSearchResult.value?.pageNum)
- assertEquals(0, selectedSearchResult.value?.currentIndex)
+ assertEquals(10, results.queryResultsIndex.pageNum)
+ assertEquals(0, results.queryResultsIndex.resultBoundsIndex)
// fetch next result
- next()
+ produceNextResult()
+ results = queryResults.value as QueryResults.Matched
// Assert selectedSearchResult point to next result cyclically
- assertEquals(1, selectedSearchResult.value?.pageNum)
- assertEquals(0, selectedSearchResult.value?.currentIndex)
+ assertEquals(1, results.queryResultsIndex.pageNum)
+ assertEquals(0, results.queryResultsIndex.resultBoundsIndex)
// fetch previous result
- prev()
+ producePreviousResult()
+ results = queryResults.value as QueryResults.Matched
// Assert selectedSearchResult point to previous result cyclically
- assertEquals(10, selectedSearchResult.value?.pageNum)
- assertEquals(0, selectedSearchResult.value?.currentIndex)
+ assertEquals(10, results.queryResultsIndex.pageNum)
+ assertEquals(0, results.queryResultsIndex.resultBoundsIndex)
}
}
@@ -101,19 +103,20 @@
with(SearchRepository(fakePdfDocument)) {
// search document
- searchDocument(query = "test", currentVisiblePage = 7)
+ produceSearchResults(query = "test", currentVisiblePage = 7)
- val results = searchResults.value
+ var results = queryResults.value as QueryResults.Matched
// Assert results exists on 3 pages
- assertEquals(3, results.results.size())
- assertEquals(10, selectedSearchResult.value?.pageNum)
- assertEquals(0, selectedSearchResult.value?.currentIndex)
+ assertEquals(3, results.resultBounds.size())
+ assertEquals(10, results.queryResultsIndex.pageNum)
+ assertEquals(0, results.queryResultsIndex.resultBoundsIndex)
// fetch next result
- next()
+ produceNextResult()
+ results = queryResults.value as QueryResults.Matched
// Assert selectedSearchResult point to next result cyclically
- assertEquals(1, selectedSearchResult.value?.pageNum)
- assertEquals(0, selectedSearchResult.value?.currentIndex)
+ assertEquals(1, results.queryResultsIndex.pageNum)
+ assertEquals(0, results.queryResultsIndex.resultBoundsIndex)
}
}
@@ -124,20 +127,21 @@
with(SearchRepository(fakePdfDocument)) {
// search document
- searchDocument(query = "test", currentVisiblePage = 11)
+ produceSearchResults(query = "test", currentVisiblePage = 11)
- val results = searchResults.value
+ var results = queryResults.value as QueryResults.Matched
// Assert results exists on 3 pages
- assertEquals(3, results.results.size())
+ assertEquals(3, results.resultBounds.size())
// Assert selectedSearchResult point to next result cyclically
- assertEquals(1, selectedSearchResult.value?.pageNum)
- assertEquals(0, selectedSearchResult.value?.currentIndex)
+ assertEquals(1, results.queryResultsIndex.pageNum)
+ assertEquals(0, results.queryResultsIndex.resultBoundsIndex)
// fetch next result
- next()
+ produceNextResult()
+ results = queryResults.value as QueryResults.Matched
// Assert selectedSearchResult point to next result on next page
- assertEquals(5, selectedSearchResult.value?.pageNum)
- assertEquals(0, selectedSearchResult.value?.currentIndex)
+ assertEquals(5, results.queryResultsIndex.pageNum)
+ assertEquals(0, results.queryResultsIndex.resultBoundsIndex)
}
}
@@ -148,46 +152,47 @@
with(SearchRepository(fakePdfDocument)) {
// search document
- searchDocument(query = "test", currentVisiblePage = 11)
+ produceSearchResults(query = "test", currentVisiblePage = 11)
- val results = searchResults.value
+ val results = queryResults.value
// Assert no results returned
- assertEquals(0, results.results.size())
+ assertTrue(results is QueryResults.NoMatch)
+ assertEquals("test", (results as QueryResults.NoMatch).query)
}
}
@Test(expected = NoSuchElementException::class)
- fun testPrevOperation_noMatchingResults() = runTest {
+ fun testFindPrevOperation_noMatchingResults() = runTest {
val fakeResults = createFakeSearchResults()
val fakePdfDocument = FakePdfDocument(searchResults = fakeResults)
with(SearchRepository(fakePdfDocument)) {
// search document
- searchDocument(query = "test", currentVisiblePage = 11)
+ produceSearchResults(query = "test", currentVisiblePage = 11)
- val results = searchResults.value
- assertEquals(0, results.results.size())
+ val results = queryResults.value
+ assertTrue(results is QueryResults.NoMatch)
// fetch previous result, should throw [NoSuchElementException]
- prev()
+ producePreviousResult()
}
}
@Test(expected = NoSuchElementException::class)
- fun testNextOperation_noMatchingResults() = runTest {
+ fun testFindNextOperation_noMatchingResults() = runTest {
val fakeResults = createFakeSearchResults()
val fakePdfDocument = FakePdfDocument(searchResults = fakeResults)
with(SearchRepository(fakePdfDocument)) {
// search document
- searchDocument(query = "test", currentVisiblePage = 10)
+ produceSearchResults(query = "test", currentVisiblePage = 10)
- val results = searchResults.value
- assertEquals(0, results.results.size())
+ val results = queryResults.value
+ assertTrue(results is QueryResults.NoMatch)
// fetch next result, should throw [NoSuchElementException]
- next()
+ produceNextResult()
}
}
@@ -198,67 +203,16 @@
with(SearchRepository(fakePdfDocument)) {
// search document
- searchDocument(query = "test", currentVisiblePage = 11)
+ produceSearchResults(query = "test", currentVisiblePage = 11)
- assertEquals(3, searchResults.value.results.size())
+ val results = queryResults.value as QueryResults.Matched
+ assertEquals(3, results.resultBounds.size())
// clear results
clearSearchResults()
// assert results are cleared
- assertTrue(searchResults.value.results.isEmpty())
- }
- }
-
- @Test
- fun testSettingStateToRepository() = runTest {
- val fakeResults = createFakeSearchResults()
- val fakePdfDocument = FakePdfDocument(searchResults = fakeResults)
- val currentVisiblePage = 11
-
- with(SearchRepository(fakePdfDocument)) {
- // search document
- searchDocument(query = "test", currentVisiblePage = currentVisiblePage)
-
- // assert there are no results
- assertEquals(0, searchResults.value.results.size())
-
- // set results
- setState(
- searchResults = SearchResults("test", createFakeSearchResults(1, 5, 5, 10)),
- selectedSearchResult = SelectedSearchResult(5, 1),
- currentVisiblePage = currentVisiblePage
- )
-
- // assert results are set
- assertTrue(searchResults.value.results.isNotEmpty())
- assertEquals(5, selectedSearchResult.value?.pageNum)
- assertEquals(1, selectedSearchResult.value?.currentIndex)
- }
- }
-
- @Test(expected = NoSuchElementException::class)
- fun testSettingEmptyResultsToRepository() = runTest {
- val fakeResults = createFakeSearchResults()
- val fakePdfDocument = FakePdfDocument(searchResults = fakeResults)
- val currentVisiblePage = 11
-
- with(SearchRepository(fakePdfDocument)) {
- // search document
- searchDocument(query = "test", currentVisiblePage = currentVisiblePage)
-
- // assert there are no results
- assertEquals(0, searchResults.value.results.size())
-
- // set results
- setState(
- searchResults = SearchResults("test", SparseArray()),
- selectedSearchResult = null,
- currentVisiblePage = currentVisiblePage
- )
-
- // fetch next result, should throw [NoSuchElementException]
- next()
+ assertTrue(queryResults.value is NoQuery)
}
}
}
diff --git a/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/util/IntentUtils.kt b/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/util/IntentUtils.kt
new file mode 100644
index 0000000..3160074
--- /dev/null
+++ b/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/util/IntentUtils.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.pdf.util
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeout
+
+/**
+ * Waits for the intent to be fired by polling repeatedly. This is useful when checking for an
+ * intent that may take some time to be triggered. The function retries every 100 ms until the
+ * intent is captured or the [timeoutMillis] duration has passed.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+internal suspend fun waitForIntent(timeoutMillis: Long = 1000, checkIntent: () -> Unit) {
+ withContext(Dispatchers.Default.limitedParallelism(1)) {
+ withTimeout(timeoutMillis) {
+ var intentCaptured = false
+ while (!intentCaptured) {
+ try {
+ checkIntent()
+ intentCaptured = true
+ } catch (e: AssertionError) {
+ delay(100)
+ }
+ }
+ }
+ }
+}
diff --git a/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/FakePdfDocument.kt b/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/FakePdfDocument.kt
index 2c5993f..3436827 100644
--- a/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/FakePdfDocument.kt
+++ b/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/FakePdfDocument.kt
@@ -65,6 +65,7 @@
override val isLinearized: Boolean = false,
private val searchResults: SparseArray<List<PageMatchBounds>> = SparseArray(),
override val uri: Uri = Uri.parse("content://test.app/document.pdf"),
+ private val pageLinks: List<PdfDocument.PdfPageLinks> = emptyList()
) : PdfDocument {
override val pageCount: Int = pages.size
@@ -84,8 +85,11 @@
}
override suspend fun getPageLinks(pageNumber: Int): PdfDocument.PdfPageLinks {
- // TODO(b/376136907) provide a useful implementation when it's needed for testing
- return PdfDocument.PdfPageLinks(listOf(), listOf())
+ return if (pageNumber < pageLinks.size) {
+ pageLinks[pageNumber]
+ } else {
+ PdfDocument.PdfPageLinks(emptyList(), emptyList())
+ }
}
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 13)
@@ -162,12 +166,12 @@
if (tileRegion == null) {
_bitmapRequests[pageNumber] = FullBitmap(scaledPageSizePx)
// Tiling, and this is a new rect for a tile board we're already tracking
- } else if (requestedSize != null && requestedSize is TileBoard) {
+ } else if (requestedSize != null && requestedSize is Tiles) {
requestedSize.withTile(tileRegion)
// Tiling, and this is the first rect requested
} else {
_bitmapRequests[pageNumber] =
- TileBoard(scaledPageSizePx).apply { withTile(tileRegion) }
+ Tiles(scaledPageSizePx).apply { withTile(tileRegion) }
}
}
}
@@ -185,7 +189,7 @@
internal class FullBitmap(scaledPageSizePx: Size) : SizeParams(scaledPageSizePx)
/** Represents a set of tile region [Bitmap] requested from [PdfDocument.BitmapSource] */
-internal class TileBoard(scaledPageSizePx: Size) : SizeParams(scaledPageSizePx) {
+internal class Tiles(scaledPageSizePx: Size) : SizeParams(scaledPageSizePx) {
private val _tiles = mutableListOf<Rect>()
val tiles: List<Rect>
get() = _tiles
diff --git a/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/PdfViewActions.kt b/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/PdfViewActions.kt
index 0a09c2f..6ee67c8 100644
--- a/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/PdfViewActions.kt
+++ b/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/PdfViewActions.kt
@@ -17,11 +17,14 @@
package androidx.pdf.view
import android.graphics.PointF
+import android.os.SystemClock
+import android.view.MotionEvent
import android.view.View
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import org.hamcrest.Matcher
import org.hamcrest.Matchers
@@ -119,3 +122,55 @@
uiController.loopMainThreadUntilIdle()
}
}
+
+/**
+ * Performs a [ViewAction] that results in single tap on a specific location (x, y) relative to a
+ * given view. This action calculates the screen coordinates of the view and offsets them by the
+ * provided (x, y) values to simulate a tap at the desired position on the screen.
+ *
+ * @param x The horizontal offset (in pixels) from the top-left corner of the view.
+ * @param y The vertical offset (in pixels) from the top-left corner of the view.
+ * @return A ViewAction that can be used with Espresso to perform the tap.
+ */
+internal fun performSingleTapOnCoords(x: Float, y: Float): ViewAction {
+ return object : ViewAction {
+ override fun getConstraints() = isDisplayed()
+
+ override fun getDescription() = "Single tap at coordinates ($x, $y)"
+
+ override fun perform(uiController: UiController, view: View) {
+ check(view is PdfView)
+ val adjustedX = (x * view.zoom) + view.scrollX
+ val adjustedY = (y * view.zoom) + view.scrollY
+
+ val screenCoords = IntArray(2)
+ view.getLocationOnScreen(screenCoords)
+
+ val screenX = screenCoords[0] + adjustedX
+ val screenY = screenCoords[1] + adjustedY
+
+ val downEvent =
+ MotionEvent.obtain(
+ SystemClock.uptimeMillis(),
+ SystemClock.uptimeMillis(),
+ MotionEvent.ACTION_DOWN,
+ screenX,
+ screenY,
+ 0
+ )
+
+ val upEvent =
+ MotionEvent.obtain(
+ SystemClock.uptimeMillis(),
+ SystemClock.uptimeMillis() + 100,
+ MotionEvent.ACTION_UP,
+ screenX,
+ screenY,
+ 0
+ )
+
+ view.dispatchTouchEvent(downEvent)
+ view.dispatchTouchEvent(upEvent)
+ }
+ }
+}
diff --git a/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/PdfViewNavigationTest.kt b/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/PdfViewNavigationTest.kt
new file mode 100644
index 0000000..fad5666
--- /dev/null
+++ b/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/PdfViewNavigationTest.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.pdf.view
+
+import android.content.Intent
+import android.graphics.Point
+import android.graphics.RectF
+import android.net.Uri
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.pdf.PdfDocument
+import androidx.pdf.content.PdfPageGotoLinkContent
+import androidx.pdf.content.PdfPageLinkContent
+import androidx.pdf.util.waitForIntent
+import androidx.test.core.app.ActivityScenario
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasData
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class PdfViewNavigationTest {
+ @After
+ fun tearDown() {
+ PdfViewTestActivity.onCreateCallback = {}
+ }
+
+ @Ignore("Ignored due to flakiness. Pending fix.")
+ @Test
+ fun testGotoLinkNavigation() {
+ // TODO(b/384644788): Fix flakiness in goto link navigation
+ val fakePdfDocument =
+ FakePdfDocument(
+ pages = List(2) { Point(1000, 1000) },
+ pageLinks =
+ listOf(
+ PdfDocument.PdfPageLinks(
+ gotoLinks =
+ listOf(
+ PdfPageGotoLinkContent(
+ bounds = listOf(RectF(0f, 0f, 1000f, 1000f)),
+ destination =
+ PdfPageGotoLinkContent.Destination(
+ pageNumber = 1,
+ xCoordinate = 100f,
+ yCoordinate = 1400f,
+ zoom = 1f
+ )
+ )
+ ),
+ externalLinks = emptyList()
+ )
+ )
+ )
+ PdfViewTestActivity.onCreateCallback = { activity ->
+ val container = FrameLayout(activity)
+ container.addView(
+ PdfView(activity).apply {
+ pdfDocument = fakePdfDocument
+ id = PDF_VIEW_ID
+ },
+ ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ )
+ activity.setContentView(container)
+ }
+
+ with(ActivityScenario.launch(PdfViewTestActivity::class.java)) {
+ Espresso.onView(withId(PDF_VIEW_ID))
+ .perform(performSingleTapOnCoords(100f, 100f))
+ .check { view, noViewFoundException ->
+ view ?: throw noViewFoundException
+ val pdfView = view as PdfView
+ val firstVisiblePage = pdfView.firstVisiblePage
+ val visiblePagesCount = pdfView.visiblePagesCount
+ val targetPage = 1
+ assertThat(targetPage).isAtLeast(firstVisiblePage)
+ assertThat(targetPage).isAtMost(firstVisiblePage + visiblePagesCount - 1)
+ }
+ close()
+ }
+ }
+
+ @Ignore("Test is skipped due to challenges in testing external intent matching. Pending fix.")
+ @Test
+ fun testExternalLinkNavigation() = runTest {
+ // TODO(b/384644788): Fix intent resolution in external links handling
+ val fakePdfDocument =
+ FakePdfDocument(
+ pages = List(5) { Point(1000, 1000) },
+ pageLinks =
+ listOf(
+ PdfDocument.PdfPageLinks(
+ gotoLinks = emptyList(),
+ externalLinks =
+ listOf(
+ PdfPageLinkContent(
+ bounds = listOf(RectF(0f, 0f, 200f, 200f)),
+ uri = Uri.parse("https://www.example.com")
+ )
+ )
+ )
+ )
+ )
+
+ Intents.init()
+ PdfViewTestActivity.onCreateCallback = { activity ->
+ val container = FrameLayout(activity)
+ container.addView(
+ PdfView(activity).apply {
+ pdfDocument = fakePdfDocument
+ id = PDF_VIEW_ID
+ },
+ ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ )
+ activity.setContentView(container)
+ }
+
+ with(ActivityScenario.launch(PdfViewTestActivity::class.java)) {
+ Espresso.onView(withId(PDF_VIEW_ID)).perform(performSingleTapOnCoords(50f, 50f))
+ waitForIntent {
+ Intents.intended(hasAction(Intent.ACTION_VIEW))
+ Intents.intended(hasData(Uri.parse("https://www.example.com")))
+ }
+ close()
+ }
+ Intents.release()
+ }
+}
+
+/** Arbitrary fixed ID for PdfView */
+private const val PDF_VIEW_ID = 123456789
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/CyclicSparseArrayIterator.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/CyclicSparseArrayIterator.kt
index 8b7895a..8a16283 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/CyclicSparseArrayIterator.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/CyclicSparseArrayIterator.kt
@@ -19,7 +19,7 @@
import android.util.SparseArray
import androidx.annotation.RestrictTo
import androidx.pdf.content.PageMatchBounds
-import androidx.pdf.search.model.SelectedSearchResult
+import androidx.pdf.search.model.QueryResultsIndex
/**
* A cyclic iterator implementation over SparseArray.
@@ -54,13 +54,13 @@
}
/** Get the current state of selected search result. */
- fun current(): SelectedSearchResult {
+ fun current(): QueryResultsIndex {
val currentPageNum = pageNumList[pageNumIndex]
- return SelectedSearchResult(currentPageNum, searchIndexOnPage)
+ return QueryResultsIndex(pageNum = currentPageNum, resultBoundsIndex = searchIndexOnPage)
}
/** Move to the nex element in the current page, or to the next page cyclically. */
- fun next(): SelectedSearchResult {
+ fun next(): QueryResultsIndex {
if (totalPages == 0) {
throw NoSuchElementException("No elements to iterate.")
}
@@ -80,7 +80,7 @@
}
/** Move to the previous element in the page list, or to the previous page cyclically. */
- fun prev(): SelectedSearchResult {
+ fun prev(): QueryResultsIndex {
if (totalPages == 0) {
throw NoSuchElementException("No elements to iterate.")
}
@@ -94,6 +94,8 @@
// If we're at the beginning of the current page, move to the previous page
if (searchIndexOnPage == resultsOnPage.size - 1) {
pageNumIndex = (pageNumIndex - 1 + totalPages) % totalPages
+ // update the search index of page to last result on updated page
+ searchIndexOnPage = searchData.valueAt(pageNumIndex).lastIndex
}
return current()
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/SearchRepository.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/SearchRepository.kt
index 80cce7e..3de7dbb 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/SearchRepository.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/SearchRepository.kt
@@ -17,11 +17,11 @@
package androidx.pdf.search
import androidx.annotation.RestrictTo
-import androidx.core.util.isEmpty
import androidx.core.util.isNotEmpty
import androidx.pdf.PdfDocument
-import androidx.pdf.search.model.SearchResults
-import androidx.pdf.search.model.SelectedSearchResult
+import androidx.pdf.search.model.NoQuery
+import androidx.pdf.search.model.QueryResults
+import androidx.pdf.search.model.SearchResultState
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -46,23 +46,17 @@
* to Dispatcher.IO.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
-internal class SearchRepository(
+public class SearchRepository(
private val pdfDocument: PdfDocument,
+ // TODO(b/384001800) Remove dispatcher
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) {
- private val _searchResults: MutableStateFlow<SearchResults> = MutableStateFlow(SearchResults())
+ private val _queryResults: MutableStateFlow<SearchResultState> = MutableStateFlow(NoQuery)
/** Stream of search results for a given query. */
- val searchResults: StateFlow<SearchResults>
- get() = _searchResults.asStateFlow()
-
- private val _selectedSearchResult: MutableStateFlow<SelectedSearchResult?> =
- MutableStateFlow(null)
-
- /** Stream of selected search results. */
- val selectedSearchResult: StateFlow<SelectedSearchResult?>
- get() = _selectedSearchResult.asStateFlow()
+ public val queryResults: StateFlow<SearchResultState>
+ get() = _queryResults.asStateFlow()
private lateinit var cyclicIterator: CyclicSparseArrayIterator
@@ -71,93 +65,108 @@
*
* @param query: The search query string.
* @param currentVisiblePage: Provides current visible document page, which is required to
- * search from specific page and to calculate initial [selectedSearchResult]
+ * search from specific page and to calculate initial QueryResultsIndex.
*
- * Results would be updated to [searchResults] in the coroutine collecting the flow.
+ * Results would be updated to [queryResults] in the coroutine collecting the flow.
*/
- suspend fun searchDocument(query: String, currentVisiblePage: Int) {
- if (query.isEmpty()) return
+ public suspend fun produceSearchResults(query: String, currentVisiblePage: Int) {
+ if (query.isBlank()) {
+ clearSearchResults()
+ return
+ }
- // Clear the existing results
- clearSearchResults()
+ val searchPageRange = IntRange(start = 0, endInclusive = pdfDocument.pageCount - 1)
// search should be a background work, move execution on to provided [dispatcher]
// to make [searchDocument] main-safe
- val currentResult =
+ val searchResults =
withContext(dispatcher) {
- SearchResults(
- searchQuery = query,
- results =
- pdfDocument.searchDocument(
- query = query,
- pageRange = IntRange(start = 0, endInclusive = pdfDocument.pageCount)
- )
- )
+ pdfDocument.searchDocument(query = query, pageRange = searchPageRange)
}
- // update results
- _searchResults.update { currentResult }
+ val queryResults =
+ if (searchResults.isNotEmpty()) {
+ /*
+ When search results are available for a query, we initialize a cyclic iterator.
+ This iterator is used to traverse the results when `findPrev()` and `findNext()` are called.
+ */
+ cyclicIterator = CyclicSparseArrayIterator(searchResults, currentVisiblePage)
- if (currentResult.results.isNotEmpty()) {
- // Init cyclic iterator
- cyclicIterator = CyclicSparseArrayIterator(currentResult.results, currentVisiblePage)
+ QueryResults.Matched(
+ query = query,
+ pageRange = searchPageRange,
+ resultBounds = searchResults,
+ /* Set [queryResultsIndex] to cyclicIterator.current() which points to first result
+ on or nearest page to currentVisiblePage in forward direction. */
+ queryResultsIndex = cyclicIterator.current()
+ )
+ } else {
+ QueryResults.NoMatch(query = query, pageRange = searchPageRange)
+ }
- // update initial selection
- _selectedSearchResult.update { cyclicIterator.current() }
- }
+ _queryResults.update { queryResults }
}
/**
* Iterate through searchResults in backward direction.
*
- * Results would be updated to [selectedSearchResult] in the coroutine collecting the flow.
+ * Results would be updated to [queryResults] in the coroutine collecting the flow.
*
* Throws [NoSuchElementException] is search results are empty.
*/
- suspend fun prev() {
- if (searchResults.value.results.isEmpty())
+ public suspend fun producePreviousResult() {
+ val currentResult = queryResults.value
+
+ if (currentResult !is QueryResults.Matched)
throw NoSuchElementException("Iteration not possible over empty results")
- _selectedSearchResult.update { cyclicIterator.prev() }
+ /*
+ Create a shallow copy of the query result, updating only the `queryResultIndex`
+ to point to the previous element in the `resultsBounds` of the current query result.
+ */
+ val prevResult =
+ QueryResults.Matched(
+ query = currentResult.query,
+ resultBounds = currentResult.resultBounds,
+ pageRange = currentResult.pageRange,
+ queryResultsIndex = cyclicIterator.prev()
+ )
+
+ _queryResults.update { prevResult }
}
/**
* Iterate through searchResults in forward direction.
*
- * Results would be updated to [selectedSearchResult] in the coroutine collecting the flow.
+ * Results would be updated to [queryResults] in the coroutine collecting the flow.
*
* Throws [NoSuchElementException] is search results are empty.
*/
- suspend fun next() {
- if (searchResults.value.results.isEmpty())
+ public suspend fun produceNextResult() {
+ val currentResult = queryResults.value
+
+ if (currentResult !is QueryResults.Matched)
throw NoSuchElementException("Iteration not possible over empty results")
- _selectedSearchResult.update { cyclicIterator.next() }
+ /*
+ Create a shallow copy of the query result, updating only the `queryResultIndex`
+ to point to the next element in the `resultsBounds` of the current query result.
+ */
+ val nextResult =
+ QueryResults.Matched(
+ query = currentResult.query,
+ resultBounds = currentResult.resultBounds,
+ pageRange = currentResult.pageRange,
+ queryResultsIndex = cyclicIterator.next()
+ )
+
+ _queryResults.update { nextResult }
}
/**
- * Resets [searchResults] and [selectedSearchResult] to initial state. This would be required to
- * handle close/cancel action.
+ * Resets [queryResults] to initial state. This would be required to handle close/cancel action.
*/
- fun clearSearchResults() {
- _searchResults.update { SearchResults() }
- _selectedSearchResult.update { null }
- }
-
- /**
- * Set [searchResults] and [selectedSearchResult] flows to provided value.
- *
- * This should be utilized when result state is already available(muck like in restore scenario)
- */
- fun setState(
- searchResults: SearchResults,
- selectedSearchResult: SelectedSearchResult?,
- currentVisiblePage: Int
- ) {
- _searchResults.update { searchResults }
- _selectedSearchResult.update { selectedSearchResult }
- // initiate iterator is results are not empty
- if (searchResults.results.isNotEmpty())
- cyclicIterator = CyclicSparseArrayIterator(searchResults.results, currentVisiblePage)
+ public fun clearSearchResults() {
+ _queryResults.update { NoQuery }
}
}
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/QueryResults.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/QueryResults.kt
new file mode 100644
index 0000000..52c2f36
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/QueryResults.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.pdf.search.model
+
+import android.util.SparseArray
+import androidx.annotation.RestrictTo
+import androidx.pdf.content.PageMatchBounds
+
+/** A sealed interface that encapsulates the various states of a search operation's result. */
+@RestrictTo(RestrictTo.Scope.LIBRARY) public sealed interface SearchResultState
+
+/**
+ * Represents the initial state when no query has been submitted to trigger a search operation. This
+ * state occurs before any search is initiated.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY) public object NoQuery : SearchResultState
+
+/**
+ * A sealed class representing the outcome of a search operation.
+ *
+ * @param query The search query that initiated the search.
+ * @param pageRange The range of PDF pages involved in the search.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public sealed class QueryResults(public val query: String, public val pageRange: IntRange) :
+ SearchResultState {
+
+ /**
+ * Represents the state when no results are found after a search operation. This indicates that
+ * the search yielded no matching results.
+ *
+ * @param query The search query that was executed.
+ * @param pageRange The range of PDF pages included in the search.
+ */
+ public class NoMatch(query: String, pageRange: IntRange) : QueryResults(query, pageRange)
+
+ /**
+ * Represents the state when a search operation returns results.
+ *
+ * @param query The search query that was executed.
+ * @param pageRange The range of PDF pages included in the search.
+ * @param resultBounds A mapping of match bounds for the results, indexed by their position.
+ * @param queryResultsIndex Represents an index pointer to an element in [resultBounds].
+ */
+ public class Matched(
+ query: String,
+ pageRange: IntRange,
+ public val resultBounds: SparseArray<List<PageMatchBounds>>,
+ public val queryResultsIndex: QueryResultsIndex,
+ ) : QueryResults(query, pageRange)
+}
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/QueryResultsIndex.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/QueryResultsIndex.kt
new file mode 100644
index 0000000..36aa484
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/QueryResultsIndex.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.pdf.search.model
+
+import androidx.annotation.RestrictTo
+
+/** A model class that holds the index of a data element within [QueryResults]'s resultBounds. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class QueryResultsIndex(
+
+ /** The page number of the document where the current search result is located. */
+ public val pageNum: Int,
+
+ /** The index of the search result on the page specified by [pageNum]. */
+ public val resultBoundsIndex: Int
+)
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/SearchResults.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/SearchResults.kt
deleted file mode 100644
index 407cd13..0000000
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/SearchResults.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright 2024 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
- *
- * http://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 androidx.pdf.search.model
-
-import android.util.SparseArray
-import androidx.annotation.RestrictTo
-import androidx.pdf.content.PageMatchBounds
-
-/** Model class to hold search results over pdf document for a search query. */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-internal class SearchResults(
- /**
- * search query provided to initiate search
- *
- * By default it will be empty string.
- */
- val searchQuery: String = "",
- /**
- * search results in pdf document for [searchQuery]
- *
- * By default it will be initialized to empty [SparseArray].
- */
- val results: SparseArray<List<PageMatchBounds>> = SparseArray()
-)
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/SelectedSearchResult.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/SelectedSearchResult.kt
deleted file mode 100644
index cb39cb7..0000000
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/search/model/SelectedSearchResult.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright 2024 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
- *
- * http://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 androidx.pdf.search.model
-
-import androidx.annotation.RestrictTo
-
-/** Model class to hold current selected search result. */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-internal class SelectedSearchResult(
-
- /** Represents document page number where current search result is selected */
- val pageNum: Int,
-
- /** index of result on the page specified by [pageNum] */
- val currentIndex: Int
-)
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/AccessibilityPageHelper.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/AccessibilityPageHelper.kt
new file mode 100644
index 0000000..f3e20c0
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/AccessibilityPageHelper.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.pdf.view
+
+import android.content.Context
+import android.graphics.Rect
+import android.os.Bundle
+import androidx.annotation.NonNull
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import androidx.customview.widget.ExploreByTouchHelper
+import androidx.pdf.R
+
+/**
+ * Accessibility delegate for PdfView that provides a virtual view hierarchy for pages.
+ *
+ * This helper class allows accessibility services to interact with individual pages as virtual
+ * views, enabling navigation and content exploration.
+ */
+internal class AccessibilityPageHelper(
+ private val pdfView: PdfView,
+ private val pageLayoutManager: PageLayoutManager,
+ private val pageManager: PageManager
+) : ExploreByTouchHelper(pdfView) {
+
+ override fun getVirtualViewAt(x: Float, y: Float): Int {
+ val visiblePages = pageLayoutManager.visiblePages.value
+
+ val contentX = pdfView.toContentX(x).toInt()
+ val contentY = pdfView.toContentY(y).toInt()
+
+ return (visiblePages.lower..visiblePages.upper).firstOrNull { page ->
+ pageLayoutManager
+ .getPageLocation(page, pdfView.getVisibleAreaInContentCoords())
+ .contains(contentX, contentY)
+ } ?: HOST_ID
+ }
+
+ override fun getVisibleVirtualViews(virtualViewIds: MutableList<Int>) {
+ val visiblePages = pageLayoutManager.visiblePages.value
+ virtualViewIds.addAll(visiblePages.lower..visiblePages.upper)
+ }
+
+ override fun onPopulateNodeForVirtualView(
+ virtualViewId: Int,
+ @NonNull node: AccessibilityNodeInfoCompat
+ ) {
+ node.apply {
+ // Set content description (use extracted text if available, otherwise a placeholder)
+ val pageText = pageManager.pages.get(virtualViewId)?.pageText
+ contentDescription =
+ pageText?.let { getContentDescriptionForPage(pdfView.context, virtualViewId, it) }
+ ?: getDefaultDesc(pdfView.context, virtualViewId)
+
+ val pageBounds =
+ pageLayoutManager.getPageLocation(
+ virtualViewId,
+ pdfView.getVisibleAreaInContentCoords()
+ )
+ setBoundsInScreenFromBoundsInParent(node, scalePageBounds(pageBounds, pdfView.zoom))
+
+ isFocusable = true
+ }
+ }
+
+ override fun onPerformActionForVirtualView(
+ virtualViewId: Int,
+ action: Int,
+ arguments: Bundle?
+ ): Boolean {
+ // This view does not handle any actions.
+ return false
+ }
+
+ private fun scalePageBounds(bounds: Rect, zoom: Float): Rect {
+ return Rect(
+ (bounds.left * zoom).toInt(),
+ (bounds.top * zoom).toInt(),
+ (bounds.right * zoom).toInt(),
+ (bounds.bottom * zoom).toInt()
+ )
+ }
+
+ /**
+ * Updates accessibility node with extracted page text when ready.
+ *
+ * @param pageNum 0-indexed page number.
+ */
+ fun onPageTextReady(pageNum: Int) {
+ val pageText = pageManager.pages.get(pageNum)?.pageText
+
+ if (pageText != null) {
+ // Update accessibility node with new text.
+ invalidateVirtualView(pageNum)
+ }
+ }
+
+ companion object {
+
+ /**
+ * Builds the content description for a page.
+ *
+ * @param context The context for accessing resources.
+ * @param pageText The extracted text content of the page, or null if not loaded.
+ * @param pageNum The 0-indexed page number.
+ * @return The content description string.
+ */
+ private fun getContentDescriptionForPage(
+ context: Context,
+ pageNum: Int,
+ pageText: String?
+ ): String {
+ return when {
+ pageText == null -> getDefaultDesc(context, pageNum)
+ pageText.trim().isEmpty() -> context.getString(R.string.desc_empty_page)
+ else -> context.getString(R.string.desc_page_with_text, pageNum + 1, pageText)
+ }
+ }
+
+ /**
+ * Gets the default content description for a page. This is used as a placeholder while the
+ * actual content description is loading.
+ *
+ * @param context The context for accessing resources.
+ * @param pageNum The 0-indexed page number.
+ * @return The default content description string.
+ */
+ private fun getDefaultDesc(context: Context, pageNum: Int): String {
+ return context.getString(R.string.desc_page, pageNum + 1)
+ }
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/BitmapFetcher.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/BitmapFetcher.kt
new file mode 100644
index 0000000..8b223f0
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/BitmapFetcher.kt
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.pdf.view
+
+import android.graphics.Bitmap
+import android.graphics.Point
+import android.graphics.PointF
+import android.graphics.Rect
+import android.util.Size
+import androidx.annotation.MainThread
+import androidx.annotation.VisibleForTesting
+import androidx.pdf.PdfDocument
+import kotlin.math.roundToInt
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.job
+import kotlinx.coroutines.launch
+
+/**
+ * Manages the loading of [Bitmap]s for a single PDF page. Keeps track of the current zoom level and
+ * will switch between full page rendering to tiled rendering as dictated by [maxBitmapSizePx]
+ */
+internal class BitmapFetcher(
+ private val pageNum: Int,
+ private val pageSize: Point,
+ private val pdfDocument: PdfDocument,
+ private val backgroundScope: CoroutineScope,
+ /**
+ * The maximum size of a single bitmap in pixels. If the pageSize * current zoom exceeds this
+ * threshold, we will start to use tiled rendering.
+ */
+ private val maxBitmapSizePx: Point,
+ private val onPageUpdate: () -> Unit,
+) {
+
+ /**
+ * The maximum size of a full page bitmap that is used as the background for a tiled rendering.
+ * We draw a low-res bitmap behind tiles to avoid blank spaces in the page as high res tiles are
+ * being loaded.
+ */
+ private val maxTileBackgroundSizePx = Point(maxBitmapSizePx.x / 2, maxBitmapSizePx.y / 2)
+
+ var isActive: Boolean = false
+ set(value) {
+ // Debounce setting the field to the same value
+ if (field == value) return
+ field = value
+ if (field) onActive() else onInactive()
+ }
+
+ @get:MainThread var pageContents: PageContents? = null
+
+ private var bitmapSource: PdfDocument.BitmapSource? = null
+ @VisibleForTesting var currentRenderingScale: Float? = null
+ @VisibleForTesting var renderingJob: Job? = null
+
+ /**
+ * Notify this fetcher that the zoom level / scale factor of the UI has changed, and that it
+ * ought to consider fetching new bitmaps
+ */
+ fun onScaleChanged(scale: Float) {
+ if (!shouldRenderNewBitmaps(scale)) return
+
+ currentRenderingScale = scale
+ renderingJob?.cancel()
+ renderingJob =
+ if (needsTiling(scale)) {
+ fetchTiles(scale)
+ } else {
+ fetchNewBitmap(scale)
+ }
+ renderingJob?.invokeOnCompletion { cause ->
+ // We only want to reset these states when we completed naturally
+ if (cause is CancellationException) return@invokeOnCompletion
+ renderingJob = null
+ currentRenderingScale = null
+ }
+ }
+
+ private fun shouldRenderNewBitmaps(scale: Float): Boolean {
+ val renderingAtCurrentScale =
+ currentRenderingScale == scale && renderingJob?.isActive == true
+ val renderedAtCurrentScale = pageContents?.let { it.renderedScale == scale } ?: false
+
+ return !renderedAtCurrentScale && !renderingAtCurrentScale
+ }
+
+ /** Prepare to start fetching bitmaps */
+ private fun onActive() {
+ bitmapSource = pdfDocument.getPageBitmapSource(pageNum)
+ }
+
+ /**
+ * Cancel ongoing work and release resources, including [Bitmap]s and [AutoCloseable]s held by
+ * this fetcher
+ */
+ private fun onInactive() {
+ currentRenderingScale = null
+ pageContents = null
+ renderingJob?.cancel()
+ renderingJob = null
+ bitmapSource?.close()
+ bitmapSource = null
+ }
+
+ /** Fetch a [FullPageBitmap] */
+ private fun fetchNewBitmap(scale: Float): Job {
+ return backgroundScope.launch {
+ val size = limitBitmapSize(scale, maxBitmapSizePx)
+ // If our BitmapSource is null that means this fetcher is inactive and we should
+ // stop what we're doing
+ val bitmap = bitmapSource?.getBitmap(size) ?: return@launch
+ ensureActive()
+ pageContents = FullPageBitmap(bitmap, scale)
+ onPageUpdate()
+ }
+ }
+
+ /** Fetch a [TileBoard] */
+ private fun fetchTiles(scale: Float): Job {
+ val pageSizePx = Point((pageSize.x * scale).roundToInt(), (pageSize.y * scale).roundToInt())
+ val tileBoard = TileBoard(tileSizePx, pageSizePx, scale)
+ // Re-use an existing background bitmap if we have one to avoid unnecessary re-rendering
+ // and jank
+ val prevBackground = (tileBoard as? TileBoard)?.backgroundBitmap
+ if (prevBackground != null) {
+ tileBoard.backgroundBitmap = prevBackground
+ pageContents = tileBoard
+ onPageUpdate()
+ }
+ return backgroundScope.launch {
+ // Render a new background bitmap if we must
+ if (prevBackground == null) {
+ // If our BitmapSource is null that means this fetcher is inactive and we should
+ // stop what we're doing
+ val backgroundSize = limitBitmapSize(scale, maxTileBackgroundSizePx)
+ val bitmap = bitmapSource?.getBitmap(backgroundSize) ?: return@launch
+ pageContents = tileBoard
+ ensureActive()
+ tileBoard.backgroundBitmap = bitmap
+ onPageUpdate()
+ }
+ for (tile in tileBoard.tiles) {
+ renderBitmap(tile, coroutineContext.job, scale)
+ }
+ }
+ }
+
+ /** Render a [Bitmap] for this [TileBoard.Tile] */
+ private suspend fun renderBitmap(tile: TileBoard.Tile, thisJob: Job, scale: Float) {
+ thisJob.ensureActive()
+ val left = tile.offsetPx.x
+ val top = tile.offsetPx.y
+ val tileRect = Rect(left, top, left + tile.exactSizePx.x, top + tile.exactSizePx.y)
+ // If our BitmapSource is null that means this fetcher is inactive and we should
+ // stop what we're doing
+ val bitmap =
+ bitmapSource?.getBitmap(
+ Size((pageSize.x * scale).roundToInt(), (pageSize.y * scale).roundToInt()),
+ tileRect
+ ) ?: return
+ thisJob.ensureActive()
+ tile.bitmap = bitmap
+ onPageUpdate()
+ }
+
+ /** True if the [pageSize] * [scale] exceeds [maxBitmapSizePx] */
+ private fun needsTiling(scale: Float): Boolean {
+ return ((pageSize.x * scale) >= maxBitmapSizePx.x) ||
+ ((pageSize.y * scale) >= maxBitmapSizePx.y)
+ }
+
+ /**
+ * Returns a size that is as near as possible to [pageSize] * [requestedScale] while being
+ * smaller than [maxSize] in both dimensions
+ */
+ private fun limitBitmapSize(requestedScale: Float, maxSize: Point): Size {
+ val finalSize = PointF(pageSize.x * requestedScale, pageSize.y * requestedScale)
+ // Reduce final size by 10% in each dimension until the constraints are satisfied
+ while (finalSize.x > maxSize.x || finalSize.y > maxSize.y) {
+ finalSize.x *= 0.9f
+ finalSize.y *= 0.9f
+ }
+ return Size(finalSize.x.roundToInt(), finalSize.y.roundToInt())
+ }
+
+ companion object {
+ /** The size of a single tile in pixels, when tiling is used */
+ @VisibleForTesting internal val tileSizePx = Point(800, 800)
+ }
+}
+
+/** Represents the [Bitmap] or [Bitmap]s used to render this page */
+internal sealed interface PageContents {
+ val renderedScale: Float
+}
+
+/** A singular [Bitmap] depicting the full page, when full page rendering is used */
+internal class FullPageBitmap(val bitmap: Bitmap, override val renderedScale: Float) : PageContents
+
+/**
+ * A set of [Bitmap]s that depict the full page as a rectangular grid of individual bitmap tiles.
+ * This [PageContents] is mutable; it's updated with new Bitmaps as tiles are loaded incrementally
+ */
+internal class TileBoard(
+ val tileSizePx: Point,
+ val pageSizePx: Point,
+ override val renderedScale: Float
+) : PageContents {
+
+ /** The low res background [Bitmap] for this [TileBoard] */
+ var backgroundBitmap: Bitmap? = null
+
+ /** The number of rows in the current tiling */
+ private val numRows
+ get() = (1 + (pageSizePx.y - 1) / tileSizePx.y)
+
+ /** The number of columns in the current tiling */
+ private val numCols
+ get() = (1 + (pageSizePx.x - 1) / tileSizePx.x)
+
+ /** The [Tile]s in this board */
+ val tiles = Array(numRows * numCols) { index -> Tile(index) }
+
+ /** An individual [Tile] in this [TileBoard] */
+ inner class Tile(index: Int) {
+ /** The x position of this tile in the tile board */
+ private val rowIdx = index / numCols
+
+ /** The y position of this tile in the tile board */
+ private val colIdx = index % numCols
+
+ /**
+ * The offset of this [Tile] from the origin of the page in pixels, used in computations
+ * where an exact pixel size is expected, e.g. rendering bitmaps
+ */
+ val offsetPx = Point(colIdx * tileSizePx.x, rowIdx * tileSizePx.y)
+
+ /** The size of this [Tile] in pixels */
+ val exactSizePx =
+ Point(
+ minOf(tileSizePx.x, pageSizePx.x - offsetPx.x),
+ minOf(tileSizePx.y, pageSizePx.y - offsetPx.y),
+ )
+
+ /** The high res [Bitmap] for this [Tile] */
+ var bitmap: Bitmap? = null
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/Page.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/Page.kt
index dbc764e..cdf2a80 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/Page.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/Page.kt
@@ -16,20 +16,18 @@
package androidx.pdf.view
-import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
+import android.graphics.Paint.Style
import android.graphics.Point
import android.graphics.Rect
import android.graphics.RectF
-import android.util.Size
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.pdf.PdfDocument
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
-import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
/** A single PDF page that knows how to render and draw itself */
@@ -38,54 +36,86 @@
/** The 0-based index of this page in the PDF */
private val pageNum: Int,
/** The size of this PDF page, in content coordinates */
- private val size: Point,
+ pageSizePx: Point,
/** The [PdfDocument] this [Page] belongs to */
private val pdfDocument: PdfDocument,
/** The [CoroutineScope] to use for background work */
private val backgroundScope: CoroutineScope,
+ /**
+ * The maximum size of any single [android.graphics.Bitmap] we render for a page, i.e. the
+ * threshold for tiled rendering
+ */
+ maxBitmapSizePx: Point,
/** A function to call when the [PdfView] hosting this [Page] ought to invalidate itself */
- private val onPageUpdate: () -> Unit,
+ onPageUpdate: () -> Unit,
+ /** A function to call when page text is ready (invoked with page number). */
+ private val onPageTextReady: ((Int) -> Unit)
) {
init {
require(pageNum >= 0) { "Invalid negative page" }
}
- /**
- * Pre-allocated [Paint] to draw [Highlight]s, color is changed at drawing time to the value
- * defined by the [Highlight]
- */
- private val highlightPaint = Paint().apply { style = Paint.Style.FILL }
+ /** Handles rendering bitmaps for this page using [PdfDocument] */
+ private val bitmapFetcher =
+ BitmapFetcher(
+ pageNum,
+ pageSizePx,
+ pdfDocument,
+ backgroundScope,
+ maxBitmapSizePx,
+ onPageUpdate,
+ )
- /**
- * Pre-allocated [RectF] used to represent the View-coordinate location of a [Highlight] during
- * drawing
- */
+ // Pre-allocated values to avoid allocations at drawing time
+ private val highlightPaint = Paint().apply { style = Style.FILL }
private val highlightRect = RectF()
-
- private var isVisible: Boolean = false
- private var renderedZoom: Float? = null
- @VisibleForTesting internal var renderBitmapJob: Job? = null
- @VisibleForTesting internal var bitmap: Bitmap? = null
+ private val tileLocationRect = RectF()
+ internal var fetchPageTextJob: Job? = null
+ internal var pageText: String? = null
+ internal var links: PdfDocument.PdfPageLinks? = null
+ private set
fun setVisible(zoom: Float) {
- isVisible = true
- maybeUpdateBitmaps(zoom)
+ bitmapFetcher.isActive = true
+ bitmapFetcher.onScaleChanged(zoom)
+ if (links == null) {
+ fetchLinks()
+ }
+ fetchPageText()
}
fun setInvisible() {
- isVisible = false
- renderBitmapJob?.cancel()
- renderBitmapJob = null
- bitmap = null
- renderedZoom = null
+ bitmapFetcher.isActive = false
+
+ pageText = null
+ fetchPageTextJob?.cancel()
+ fetchPageTextJob = null
+ }
+
+ fun fetchPageText() {
+ if (fetchPageTextJob?.isActive == true || pageText != null) {
+ return
+ }
+
+ fetchPageTextJob =
+ backgroundScope.launch {
+ pageText =
+ pdfDocument.getPageContent(pageNum)?.textContents?.joinToString { it.text }
+ onPageTextReady.invoke(pageNum)
+ }
}
fun draw(canvas: Canvas, locationInView: Rect, highlights: List<Highlight>) {
- if (bitmap == null) {
+ val pageBitmaps = bitmapFetcher.pageContents
+ if (pageBitmaps == null) {
canvas.drawRect(locationInView, BLANK_PAINT)
return
}
- bitmap?.let { canvas.drawBitmap(it, /* src= */ null, locationInView, BMP_PAINT) }
+ if (pageBitmaps is FullPageBitmap) {
+ draw(pageBitmaps, canvas, locationInView)
+ } else if (pageBitmaps is TileBoard) {
+ draw(pageBitmaps, canvas, locationInView)
+ }
for (highlight in highlights) {
// Highlight locations are defined in content coordinates, compute their location
// in View coordinates using locationInView
@@ -96,39 +126,58 @@
}
}
- private fun maybeUpdateBitmaps(zoom: Float) {
- // If we're actively rendering or have rendered a bitmap for the current zoom level, there's
- // no need to refresh bitmaps
- if (renderedZoom?.equals(zoom) == true && (bitmap != null || renderBitmapJob != null)) {
- return
- }
- renderBitmapJob?.cancel()
- // If we're not visible, don't bother fetching new bitmaps
- if (!isVisible) return
- fetchNewBitmap(zoom)
+ private fun fetchLinks() {
+ backgroundScope.launch { links = pdfDocument.getPageLinks(pageNum) }
}
- private fun fetchNewBitmap(zoom: Float) {
- val bitmapSource = pdfDocument.getPageBitmapSource(pageNum)
- renderBitmapJob =
- backgroundScope.launch {
- ensureActive()
- val width = (size.x * zoom).toInt()
- val height = (size.y * zoom).toInt()
- renderedZoom = zoom
- bitmap = bitmapSource.getBitmap(Size(width, height))
- ensureActive()
- onPageUpdate.invoke()
+ private fun draw(fullPageBitmap: FullPageBitmap, canvas: Canvas, locationInView: Rect) {
+ canvas.drawBitmap(fullPageBitmap.bitmap, /* src= */ null, locationInView, BMP_PAINT)
+ }
+
+ private fun draw(tileBoard: TileBoard, canvas: Canvas, locationInView: Rect) {
+ tileBoard.backgroundBitmap?.let {
+ canvas.drawBitmap(it, /* src= */ null, locationInView, BMP_PAINT)
+ }
+ for (tile in tileBoard.tiles) {
+ tile.bitmap?.let { bitmap ->
+ canvas.drawBitmap(
+ bitmap, /* src */
+ null,
+ locationForTile(tile, tileBoard.renderedScale, locationInView),
+ BMP_PAINT
+ )
}
- renderBitmapJob?.invokeOnCompletion { renderBitmapJob = null }
+ }
+ }
+
+ private fun locationForTile(
+ tile: TileBoard.Tile,
+ renderedScale: Float,
+ locationInView: Rect
+ ): RectF {
+ val tileOffsetPx = tile.offsetPx
+ // The tile describes its own location in pixels, i.e. scaled coordinates, however
+ // our Canvas is already scaled by the zoom factor, so we need to describe the tile's
+ // location to the Canvas in unscaled coordinates
+ val left = locationInView.left + tileOffsetPx.x / renderedScale
+ val top = locationInView.top + tileOffsetPx.y / renderedScale
+ val exactSize = tile.exactSizePx
+ tileLocationRect.set(
+ left,
+ top,
+ left + exactSize.x / renderedScale,
+ top + exactSize.y / renderedScale
+ )
+ return tileLocationRect
}
}
/** Constant [Paint]s used in drawing */
@VisibleForTesting internal val BMP_PAINT = Paint(Paint.FILTER_BITMAP_FLAG)
+
@VisibleForTesting
internal val BLANK_PAINT =
Paint().apply {
color = Color.WHITE
- style = Paint.Style.FILL
+ style = Style.FILL
}
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PageLayoutManager.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PageLayoutManager.kt
index 59fe5c2..4af7810 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PageLayoutManager.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PageLayoutManager.kt
@@ -17,7 +17,9 @@
package androidx.pdf.view
import android.graphics.Point
+import android.graphics.PointF
import android.graphics.Rect
+import android.graphics.RectF
import android.util.Range
import androidx.pdf.PdfDocument
import kotlin.math.ceil
@@ -102,6 +104,47 @@
}
/**
+ * Returns the page number containing the specified PDF coordinates within the given viewport.
+ * If no page contains the coordinates, returns [INVALID_ID].
+ *
+ * @param pdfCoordinates The PDF coordinates to check.
+ * @param viewport The visible area of the PDF.
+ * @return The page number or [INVALID_ID].
+ */
+ fun getPageNumberAt(pdfCoordinates: PointF, viewport: Rect): Int {
+ val visiblePages = visiblePages.value
+ for (pageIndex in visiblePages.lower..visiblePages.upper) {
+ val pageBounds = paginationModel.getPageLocation(pageIndex, viewport)
+ if (RectF(pageBounds).contains(pdfCoordinates.x, pdfCoordinates.y)) {
+ return pageIndex
+ }
+ }
+ return INVALID_ID
+ }
+
+ /**
+ * Converts tap coordinates (relative to the viewport) to content coordinates relative to the
+ * specified page.
+ *
+ * @param pageNum The 0-indexed page number.
+ * @param viewport The current viewport's dimensions.
+ * @param tapCoordinates The tap coordinates relative to the visible pages.
+ * @return The coordinates relative to the clicked page.
+ */
+ fun getPageCoordinatesRelativeToTappedPage(
+ pageNum: Int,
+ viewport: Rect,
+ tapCoordinates: PointF
+ ): PointF {
+ val pageLocation = getPageLocation(pageNum, viewport)
+
+ val contentX = tapCoordinates.x - pageLocation.left
+ val contentY = tapCoordinates.y - pageLocation.top
+
+ return PointF(contentX, contentY)
+ }
+
+ /**
* Emits a new [Range] to [visiblePages] based on the current [scrollY], [height], and [zoom] of
* a [PdfView]
*/
@@ -148,5 +191,6 @@
companion object {
private const val DEFAULT_PAGE_SPACING_PX: Int = 20
+ private const val INVALID_ID = -1
}
}
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PageManager.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PageManager.kt
index d1d0e00..8c4c23b 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PageManager.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PageManager.kt
@@ -21,7 +21,6 @@
import android.graphics.Rect
import android.util.Range
import android.util.SparseArray
-import androidx.annotation.VisibleForTesting
import androidx.core.util.keyIterator
import androidx.core.util.valueIterator
import androidx.pdf.PdfDocument
@@ -41,6 +40,11 @@
private val pdfDocument: PdfDocument,
private val backgroundScope: CoroutineScope,
private val pagePrefetchRadius: Int,
+ /**
+ * The maximum size of any single [android.graphics.Bitmap] we render for a page, i.e. the
+ * threshold for tiled rendering
+ */
+ private val maxBitmapSizePx: Point,
) {
/**
* Replay at least 1 value in case of an invalidation signal issued while [PdfView] is not
@@ -58,7 +62,11 @@
val invalidationSignalFlow: SharedFlow<Unit>
get() = _invalidationSignalFlow
- @VisibleForTesting val pages = SparseArray<Page>()
+ internal val pages = SparseArray<Page>()
+
+ private val _pageTextReadyFlow = MutableSharedFlow<Int>(replay = 1)
+ val pageTextReadyFlow: SharedFlow<Int>
+ get() = _pageTextReadyFlow
/**
* [Highlight]s supplied by the developer to be drawn along with the pages they belong to
@@ -99,9 +107,15 @@
fun onPageSizeReceived(pageNum: Int, size: Point, isVisible: Boolean, currentZoomLevel: Float) {
if (pages.contains(pageNum)) return
val page =
- Page(pageNum, size, pdfDocument, backgroundScope) {
- _invalidationSignalFlow.tryEmit(Unit)
- }
+ Page(
+ pageNum,
+ size,
+ pdfDocument,
+ backgroundScope,
+ maxBitmapSizePx,
+ onPageUpdate = { _invalidationSignalFlow.tryEmit(Unit) },
+ onPageTextReady = { pageNumber -> _pageTextReadyFlow.tryEmit(pageNumber) }
+ )
.apply { if (isVisible) setVisible(currentZoomLevel) }
pages.put(pageNum, page)
}
@@ -130,6 +144,10 @@
page.setInvisible()
}
}
+
+ fun getLinkAtTapPoint(pdfPoint: PdfPoint): PdfDocument.PdfPageLinks? {
+ return pages[pdfPoint.pageNum]?.links
+ }
}
/** Constant empty list to avoid allocations during drawing */
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt
index 3c9b951..c104fd2 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt
@@ -17,22 +17,31 @@
package androidx.pdf.view
import android.content.Context
+import android.content.Intent
import android.graphics.Canvas
import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
+import android.net.Uri
import android.os.Looper
import android.os.Parcelable
import android.util.AttributeSet
import android.util.Range
+import android.view.KeyEvent
import android.view.MotionEvent
+import android.view.ScaleGestureDetector
import android.view.View
import android.widget.OverScroller
import androidx.annotation.RestrictTo
import androidx.core.os.HandlerCompat
+import androidx.core.view.ViewCompat
import androidx.pdf.PdfDocument
import androidx.pdf.util.ZoomUtils
+import java.util.LinkedList
+import java.util.Queue
import java.util.concurrent.Executors
+import kotlin.math.abs
+import kotlin.math.max
import kotlin.math.round
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
@@ -87,6 +96,7 @@
checkMainThread()
field = value
onZoomChanged()
+ invalidate()
}
/**
@@ -114,6 +124,19 @@
public val visiblePagesCount: Int
get() = if (pdfDocument != null) visiblePages.upper - visiblePages.lower + 1 else 0
+ /** Listener interface for handling clicks on links in a PDF document. */
+ public interface LinkClickListener {
+ /**
+ * Called when a link in the PDF is clicked.
+ *
+ * @param uri The URI associated with the link.
+ */
+ public fun onLinkClicked(uri: Uri)
+ }
+
+ /** The listener that is notified when a link in the PDF is clicked. */
+ public var linkClickListener: LinkClickListener? = null
+
/**
* The [CoroutineScope] used to make suspending calls to [PdfDocument]. The size of the fixed
* thread pool is arbitrary and subject to tuning.
@@ -136,13 +159,15 @@
private var scrollPositionToRestore: PointF? = null
private var zoomToRestore: Float? = null
- private val gestureHandler = ZoomScrollGestureHandler(this@PdfView)
- private val gestureTracker = GestureTracker(context).apply { delegate = gestureHandler }
+ private val gestureTracker =
+ GestureTracker(context).apply { delegate = ZoomScrollGestureHandler() }
private val scroller = OverScroller(context)
// To avoid allocations during drawing
private val visibleAreaRect = Rect()
+ private var accessibilityPageHelper: AccessibilityPageHelper? = null
+
/**
* Scrolls to the 0-indexed [pageNum], optionally animating the scroll
*
@@ -251,6 +276,21 @@
scrollTo(x.toInt(), y.toInt())
}
+ override fun dispatchHoverEvent(event: MotionEvent): Boolean {
+ return accessibilityPageHelper?.dispatchHoverEvent(event) == true ||
+ super.dispatchHoverEvent(event)
+ }
+
+ override fun dispatchKeyEvent(event: KeyEvent): Boolean {
+ return accessibilityPageHelper?.dispatchKeyEvent(event) == true ||
+ super.dispatchKeyEvent(event)
+ }
+
+ override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
+ accessibilityPageHelper?.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
+ }
+
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val localPaginationManager = pageLayoutManager ?: return
@@ -274,11 +314,13 @@
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
pageLayoutManager?.onViewportChanged(scrollY, height, zoom)
+ accessibilityPageHelper?.invalidateRoot()
}
override fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int) {
super.onScrollChanged(l, t, oldl, oldt)
pageLayoutManager?.onViewportChanged(scrollY, height, zoom)
+ accessibilityPageHelper?.invalidateRoot()
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
@@ -407,6 +449,7 @@
} else {
scrollToRestoredPosition(positionToRestore, localStateToRestore.zoom)
}
+ setAccessibility()
stateToRestore = null
return true
@@ -450,7 +493,12 @@
mainScope.launch(start = CoroutineStart.UNDISPATCHED) {
// Prevent 2 copies from running concurrently
invalidationToJoin?.join()
- manager.invalidationSignalFlow.collect { invalidate() }
+ launch { manager.invalidationSignalFlow.collect { invalidate() } }
+ launch {
+ manager.pageTextReadyFlow.collect { pageNum ->
+ accessibilityPageHelper?.onPageTextReady(pageNum)
+ }
+ }
}
}
}
@@ -462,14 +510,29 @@
}
/** Start using the [PdfDocument] to present PDF content */
+ // Display.width and height are deprecated in favor of WindowMetrics, but in this case we
+ // actually want to use the size of the display and not the size of the window.
+ @Suppress("deprecation")
private fun onDocumentSet() {
val localPdfDocument = pdfDocument ?: return
- pageManager = PageManager(localPdfDocument, backgroundScope, DEFAULT_PAGE_PREFETCH_RADIUS)
+ /* We use the maximum pixel dimension of the display as the maximum pixel dimension for any
+ single Bitmap we render, i.e. the threshold for tiled rendering. This is an arbitrary,
+ but reasonable threshold to use that does not depend on volatile state like the current
+ screen orientation or the current size of our application's Window. */
+ val maxBitmapDimensionPx = max(context.display.width, context.display.height)
+ pageManager =
+ PageManager(
+ localPdfDocument,
+ backgroundScope,
+ DEFAULT_PAGE_PREFETCH_RADIUS,
+ Point(maxBitmapDimensionPx, maxBitmapDimensionPx)
+ )
// We'll either create our layout manager from restored state, or instantiate a new one
if (!maybeRestoreState()) {
pageLayoutManager =
PageLayoutManager(localPdfDocument, backgroundScope, DEFAULT_PAGE_PREFETCH_RADIUS)
.apply { onViewportChanged(scrollY, height, zoom) }
+ setAccessibility()
}
// If not, we'll start doing this when we _are_ attached to a visible window
if (isAttachedToVisibleWindow) {
@@ -484,11 +547,22 @@
* Compute what content is visible from the current position of this View. Generally invoked on
* position or size changes.
*/
- internal fun onZoomChanged() {
+ private fun onZoomChanged() {
pageLayoutManager?.onViewportChanged(scrollY, height, zoom)
- if (!gestureHandler.scaleInProgress && !gestureHandler.scrollInProgress) {
+ // Don't fetch new Bitmaps while the user is actively zooming, to avoid jank and rendering
+ // churn
+ if (!gestureTracker.matches(GestureTracker.Gesture.ZOOM)) {
pageManager?.maybeUpdateBitmaps(visiblePages, zoom)
}
+ accessibilityPageHelper?.invalidateRoot()
+ }
+
+ /**
+ * Invoked by gesture handlers to let this view know that its position has stabilized, i.e. it's
+ * not actively changing due to user input
+ */
+ internal fun onStableZoom() {
+ pageManager?.maybeUpdateBitmaps(visiblePages, zoom)
}
private fun reset() {
@@ -559,7 +633,7 @@
* Computes the part of the content visible within the outer part of this view (including this
* view's padding) in co-ordinates of the content.
*/
- private fun getVisibleAreaInContentCoords(): Rect {
+ internal fun getVisibleAreaInContentCoords(): Rect {
visibleAreaRect.set(
toContentX(-paddingLeft.toFloat()).toInt(),
toContentY(-paddingTop.toFloat()).toInt(),
@@ -569,6 +643,21 @@
return visibleAreaRect
}
+ /**
+ * Initializes and sets the accessibility delegate for the PdfView.
+ *
+ * This method creates an instance of [AccessibilityPageHelper] if both [.pageLayoutManager] and
+ * [.pageManager] are initialized, and sets it as the accessibility delegate for the view using
+ * [ViewCompat.setAccessibilityDelegate].
+ */
+ private fun setAccessibility() {
+ if (pageLayoutManager != null && pageManager != null) {
+ accessibilityPageHelper =
+ AccessibilityPageHelper(this, pageLayoutManager!!, pageManager!!)
+ ViewCompat.setAccessibilityDelegate(this, accessibilityPageHelper)
+ }
+ }
+
/** The height of the viewport, minus padding */
private val viewportHeight: Int
get() = bottom - top - paddingBottom - paddingTop
@@ -578,12 +667,12 @@
get() = right - left - paddingRight - paddingLeft
/** Converts an X coordinate in View space to an X coordinate in content space */
- private fun toContentX(viewX: Float): Float {
+ internal fun toContentX(viewX: Float): Float {
return toContentCoord(viewX, zoom, scrollX)
}
/** Converts a Y coordinate in View space to a Y coordinate in content space */
- private fun toContentY(viewY: Float): Float {
+ internal fun toContentY(viewY: Float): Float {
return toContentCoord(viewY, zoom, scrollY)
}
@@ -605,12 +694,218 @@
private val contentHeight: Int
get() = pageLayoutManager?.paginationModel?.totalEstimatedHeight ?: 0
+ internal inner class ZoomScrollGestureHandler : GestureTracker.GestureHandler() {
+ internal var scrollInProgress = false
+ internal var scaleInProgress = false
+
+ /**
+ * The multiplier to convert from a scale gesture's delta span, in pixels, to scale factor.
+ *
+ * [ScaleGestureDetector] returns scale factors proportional to the ratio of `currentSpan /
+ * prevSpan`. This is problematic because it results in scale factors that are very large
+ * for small pixel spans, which is particularly problematic for quickScale gestures, where
+ * the span pixel values can be small, but the ratio can yield very large scale factors.
+ *
+ * Instead, we use this to ensure that pinching or quick scale dragging a certain number of
+ * pixels always corresponds to a certain change in zoom. The equation that we've found to
+ * work well is a delta span of the larger screen dimension should result in a zoom change
+ * of 2x.
+ */
+ private val linearScaleSpanMultiplier: Float =
+ 2f / maxOf(resources.displayMetrics.heightPixels, resources.displayMetrics.widthPixels)
+
+ /** The maximum scroll distance used to determine if the direction is vertical. */
+ private val maxScrollWindow =
+ (resources.displayMetrics.density * MAX_SCROLL_WINDOW_DP).toInt()
+
+ /** The smallest scroll distance that can switch mode to "free scrolling". */
+ private val minScrollToSwitch =
+ (resources.displayMetrics.density * MIN_SCROLL_TO_SWITCH_DP).toInt()
+
+ /** Remember recent scroll events so we can examine the general direction. */
+ private val scrollQueue: Queue<PointF> = LinkedList()
+
+ /** Are we correcting vertical scroll for the current gesture? */
+ private var straightenCurrentVerticalScroll = true
+
+ private var totalX = 0f
+ private var totalY = 0f
+
+ private val totalScrollLength
+ // No need for accuracy of correct hypotenuse calculation
+ get() = abs(totalX) + abs(totalY)
+
+ override fun onScroll(
+ e1: MotionEvent?,
+ e2: MotionEvent,
+ distanceX: Float,
+ distanceY: Float,
+ ): Boolean {
+ scrollInProgress = true
+ var dx = Math.round(distanceX)
+ val dy = Math.round(distanceY)
+
+ if (straightenCurrentVerticalScroll) {
+ // Remember a window of recent scroll events.
+ scrollQueue.offer(PointF(distanceX, distanceY))
+ totalX += distanceX
+ totalY += distanceY
+
+ // Only consider scroll direction for a certain window of scroll events.
+ while (totalScrollLength > maxScrollWindow && scrollQueue.size > 1) {
+ // Remove the oldest scroll event - it is too far away to determine scroll
+ // direction.
+ val oldest = scrollQueue.poll()
+ oldest?.let {
+ totalY -= oldest.y
+ totalX -= oldest.x
+ }
+ }
+
+ if (
+ totalScrollLength > minScrollToSwitch &&
+ abs((totalY / totalX).toDouble()) < SCROLL_CORRECTION_RATIO
+ ) {
+ straightenCurrentVerticalScroll = false
+ } else {
+ // Ignore the horizontal component of the scroll.
+ dx = 0
+ }
+ }
+
+ scrollBy(dx, dy)
+ return true
+ }
+
+ override fun onFling(
+ e1: MotionEvent?,
+ e2: MotionEvent,
+ velocityX: Float,
+ velocityY: Float
+ ): Boolean {
+ return super.onFling(e1, e2, velocityX, velocityY)
+ // TODO(b/376136621) Animate scroll position during a fling
+ }
+
+ override fun onDoubleTap(e: MotionEvent): Boolean {
+ return super.onDoubleTap(e)
+ // TODO(b/376136331) Toggle between fit-to-page and zoomed-in on double tap gestures
+ }
+
+ override fun onScale(detector: ScaleGestureDetector): Boolean {
+ scaleInProgress = true
+ val rawScaleFactor = detector.scaleFactor
+ val deltaSpan = abs(detector.currentSpan - detector.previousSpan)
+ val scaleDelta = deltaSpan * linearScaleSpanMultiplier
+ val linearScaleFactor = if (rawScaleFactor >= 1f) 1f + scaleDelta else 1f - scaleDelta
+ val newZoom = (zoom * linearScaleFactor).coerceIn(minZoom, maxZoom)
+
+ zoomTo(newZoom, detector.focusX, detector.focusY)
+ return true
+ }
+
+ override fun onGestureEnd(gesture: GestureTracker.Gesture?) {
+ when (gesture) {
+ GestureTracker.Gesture.ZOOM -> {
+ scaleInProgress = false
+ onZoomChanged()
+ }
+ GestureTracker.Gesture.DRAG,
+ GestureTracker.Gesture.DRAG_Y,
+ GestureTracker.Gesture.DRAG_X -> {
+ scrollInProgress = false
+ onZoomChanged()
+ }
+ else -> {
+ /* no-op */
+ }
+ }
+ totalX = 0f
+ totalY = 0f
+ straightenCurrentVerticalScroll = true
+ scrollQueue.clear()
+ }
+
+ override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
+ val contentX = toContentX(e.x)
+ val contentY = toContentY(e.y)
+
+ val pageLayoutManager = pageLayoutManager ?: return super.onSingleTapConfirmed(e)
+ val pageNum =
+ pageLayoutManager.getPageNumberAt(
+ PointF(contentX, contentY),
+ getVisibleAreaInContentCoords()
+ )
+ if (pageNum == INVALID_ID) return super.onSingleTapConfirmed(e)
+
+ val pdfCoordinates =
+ pageLayoutManager.getPageCoordinatesRelativeToTappedPage(
+ pageNum,
+ getVisibleAreaInContentCoords(),
+ PointF(contentX, contentY)
+ )
+
+ pageManager?.getLinkAtTapPoint(PdfPoint(pageNum, pdfCoordinates))?.let { links ->
+ if (handleGotoLinks(links, pdfCoordinates)) return true
+ if (handleExternalLinks(links, pdfCoordinates)) return true
+ }
+ return super.onSingleTapConfirmed(e)
+ }
+
+ private fun handleGotoLinks(
+ links: PdfDocument.PdfPageLinks,
+ pdfCoordinates: PointF
+ ): Boolean {
+ links.gotoLinks.forEach { gotoLink ->
+ if (gotoLink.bounds.any { it.contains(pdfCoordinates.x, pdfCoordinates.y) }) {
+ val destination =
+ PdfPoint(
+ pageNum = gotoLink.destination.pageNumber,
+ pagePoint =
+ PointF(
+ gotoLink.destination.xCoordinate,
+ gotoLink.destination.yCoordinate
+ )
+ )
+ gotoPoint(destination)
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun handleExternalLinks(
+ links: PdfDocument.PdfPageLinks,
+ pdfCoordinates: PointF
+ ): Boolean {
+ links.externalLinks.forEach { externalLink ->
+ if (externalLink.bounds.any { it.contains(pdfCoordinates.x, pdfCoordinates.y) }) {
+ linkClickListener?.onLinkClicked(externalLink.uri)
+ ?: run {
+ val intent = Intent(Intent.ACTION_VIEW, externalLink.uri)
+ context.startActivity(intent)
+ }
+ return true
+ }
+ }
+ return false
+ }
+ }
+
public companion object {
public const val DEFAULT_INIT_ZOOM: Float = 1.0f
public const val DEFAULT_MAX_ZOOM: Float = 25.0f
public const val DEFAULT_MIN_ZOOM: Float = 0.1f
+ /** The ratio of vertical to horizontal scroll that is assumed to be vertical only */
+ private const val SCROLL_CORRECTION_RATIO = 1.5f
+ /** The maximum scroll distance used to determine if the direction is vertical */
+ private const val MAX_SCROLL_WINDOW_DP = 70
+ /** The smallest scroll distance that can switch mode to "free scrolling" */
+ private const val MIN_SCROLL_TO_SWITCH_DP = 30
+
private const val DEFAULT_PAGE_PREFETCH_RADIUS: Int = 2
+ private const val INVALID_ID = -1
private fun checkMainThread() {
check(Looper.myLooper() == Looper.getMainLooper()) {
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/ZoomScrollGestureHandler.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/ZoomScrollGestureHandler.kt
index e5b1cc7..8b13789 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/ZoomScrollGestureHandler.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/ZoomScrollGestureHandler.kt
@@ -1,170 +1 @@
-/*
- * Copyright 2024 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
- *
- * http://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 androidx.pdf.view
-
-import android.graphics.PointF
-import android.view.MotionEvent
-import android.view.ScaleGestureDetector
-import java.util.LinkedList
-import java.util.Queue
-import kotlin.math.abs
-
-/** Adjusts the position of [PdfView] in response to gestures detected by [GestureTracker] */
-internal class ZoomScrollGestureHandler(private val pdfView: PdfView) :
- GestureTracker.GestureHandler() {
- internal var scrollInProgress = false
- internal var scaleInProgress = false
-
- /**
- * The multiplier to convert from a scale gesture's delta span, in pixels, to scale factor.
- *
- * [ScaleGestureDetector] returns scale factors proportional to the ratio of `currentSpan /
- * prevSpan`. This is problematic because it results in scale factors that are very large for
- * small pixel spans, which is particularly problematic for quickScale gestures, where the span
- * pixel values can be small, but the ratio can yield very large scale factors.
- *
- * Instead, we use this to ensure that pinching or quick scale dragging a certain number of
- * pixels always corresponds to a certain change in zoom. The equation that we've found to work
- * well is a delta span of the larger screen dimension should result in a zoom change of 2x.
- */
- private val linearScaleSpanMultiplier: Float =
- 2f /
- maxOf(
- pdfView.resources.displayMetrics.heightPixels,
- pdfView.resources.displayMetrics.widthPixels
- )
- /** The maximum scroll distance used to determine if the direction is vertical. */
- private val maxScrollWindow =
- (pdfView.resources.displayMetrics.density * MAX_SCROLL_WINDOW_DP).toInt()
-
- /** The smallest scroll distance that can switch mode to "free scrolling". */
- private val minScrollToSwitch =
- (pdfView.resources.displayMetrics.density * MIN_SCROLL_TO_SWITCH_DP).toInt()
-
- /** Remember recent scroll events so we can examine the general direction. */
- private val scrollQueue: Queue<PointF> = LinkedList()
-
- /** Are we correcting vertical scroll for the current gesture? */
- private var straightenCurrentVerticalScroll = true
-
- private var totalX = 0f
- private var totalY = 0f
-
- private val totalScrollLength
- // No need for accuracy of correct hypotenuse calculation
- get() = abs(totalX) + abs(totalY)
-
- override fun onScroll(
- e1: MotionEvent?,
- e2: MotionEvent,
- distanceX: Float,
- distanceY: Float,
- ): Boolean {
- scrollInProgress = true
- var dx = Math.round(distanceX)
- val dy = Math.round(distanceY)
-
- if (straightenCurrentVerticalScroll) {
- // Remember a window of recent scroll events.
- scrollQueue.offer(PointF(distanceX, distanceY))
- totalX += distanceX
- totalY += distanceY
-
- // Only consider scroll direction for a certain window of scroll events.
- while (totalScrollLength > maxScrollWindow && scrollQueue.size > 1) {
- // Remove the oldest scroll event - it is too far away to determine scroll
- // direction.
- val oldest = scrollQueue.poll()
- oldest?.let {
- totalY -= oldest.y
- totalX -= oldest.x
- }
- }
-
- if (
- totalScrollLength > minScrollToSwitch &&
- abs((totalY / totalX).toDouble()) < SCROLL_CORRECTION_RATIO
- ) {
- straightenCurrentVerticalScroll = false
- } else {
- // Ignore the horizontal component of the scroll.
- dx = 0
- }
- }
-
- pdfView.scrollBy(dx, dy)
- return true
- }
-
- override fun onFling(
- e1: MotionEvent?,
- e2: MotionEvent,
- velocityX: Float,
- velocityY: Float
- ): Boolean {
- return super.onFling(e1, e2, velocityX, velocityY)
- // TODO(b/376136621) Animate scroll position during a fling
- }
-
- override fun onDoubleTap(e: MotionEvent): Boolean {
- return super.onDoubleTap(e)
- // TODO(b/376136331) Toggle between fit-to-page and zoomed-in on double tap gestures
- }
-
- override fun onScale(detector: ScaleGestureDetector): Boolean {
- scaleInProgress = true
- val rawScaleFactor = detector.scaleFactor
- val deltaSpan = abs(detector.currentSpan - detector.previousSpan)
- val scaleDelta = deltaSpan * linearScaleSpanMultiplier
- val linearScaleFactor = if (rawScaleFactor >= 1f) 1f + scaleDelta else 1f - scaleDelta
- val newZoom = (pdfView.zoom * linearScaleFactor).coerceIn(pdfView.minZoom, pdfView.maxZoom)
-
- pdfView.zoomTo(newZoom, detector.focusX, detector.focusY)
- return true
- }
-
- override fun onGestureEnd(gesture: GestureTracker.Gesture?) {
- when (gesture) {
- GestureTracker.Gesture.ZOOM -> {
- scaleInProgress = false
- pdfView.onZoomChanged()
- }
- GestureTracker.Gesture.DRAG,
- GestureTracker.Gesture.DRAG_Y,
- GestureTracker.Gesture.DRAG_X -> {
- scrollInProgress = false
- pdfView.onZoomChanged()
- }
- else -> {
- /* no-op */
- }
- }
- totalX = 0f
- totalY = 0f
- straightenCurrentVerticalScroll = true
- scrollQueue.clear()
- }
-
- companion object {
- /** The ratio of vertical to horizontal scroll that is assumed to be vertical only */
- private const val SCROLL_CORRECTION_RATIO = 1.5f
- /** The maximum scroll distance used to determine if the direction is vertical */
- private const val MAX_SCROLL_WINDOW_DP = 70
- /** The smallest scroll distance that can switch mode to "free scrolling" */
- private const val MIN_SCROLL_TO_SWITCH_DP = 30
- }
-}
diff --git a/pdf/pdf-viewer/src/main/res/values/strings.xml b/pdf/pdf-viewer/src/main/res/values/strings.xml
index f4d7e82..9c0abaf 100644
--- a/pdf/pdf-viewer/src/main/res/values/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values/strings.xml
@@ -60,6 +60,9 @@
<!-- Content description for an empty page of a paginated document -->
<string name="desc_empty_page">Empty page</string>
+ <!-- Content description for a page with its page number and corresponding text content -->
+ <string name="desc_page_with_text">Page <xliff:g example="3" id="page_number">%1$d</xliff:g>: <xliff:g example="This is the text" id="page_text">%2$s</xliff:g></string>
+
<!-- Error message when file format isn't valid PDF. [CHAR LIMIT=60] -->
<string name="error_file_format_pdf">Cannot display PDF ("<xliff:g example="Treasure Island" id="title">%1$s</xliff:g>" is of invalid format)</string>
diff --git a/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/view/BitmapFetcherTest.kt b/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/view/BitmapFetcherTest.kt
new file mode 100644
index 0000000..fc60503
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/view/BitmapFetcherTest.kt
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.pdf.view
+
+import android.graphics.Point
+import androidx.pdf.PdfDocument
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.mock
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class BitmapFetcherTest {
+ private val testDispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+ private val pdfDocument =
+ mock<PdfDocument> {
+ on { getPageBitmapSource(any()) } doAnswer
+ { invocation ->
+ FakeBitmapSource(invocation.getArgument(0))
+ }
+ }
+
+ private var invalidationCounter = 0
+ private val invalidationTracker: () -> Unit = { invalidationCounter++ }
+
+ private val maxBitmapSizePx = Point(2048, 2048)
+ private val pageSize = Point(512, 512)
+
+ private lateinit var bitmapFetcher: BitmapFetcher
+ private lateinit var tileSizePx: Point
+
+ @Before
+ fun setup() {
+ testDispatcher.cancelChildren()
+ invalidationCounter = 0
+
+ bitmapFetcher =
+ BitmapFetcher(
+ pageNum = 0,
+ pageSize,
+ pdfDocument,
+ testScope,
+ maxBitmapSizePx,
+ invalidationTracker,
+ )
+ bitmapFetcher.isActive = true
+ tileSizePx = BitmapFetcher.tileSizePx
+ }
+
+ @Test
+ fun setInactive_cancelsWorkAndFreesBitmaps() {
+ bitmapFetcher.onScaleChanged(1.5f)
+ assertThat(bitmapFetcher.renderingJob?.isActive).isTrue()
+
+ bitmapFetcher.isActive = false
+ assertThat(bitmapFetcher.renderingJob).isNull()
+ assertThat(bitmapFetcher.pageContents).isNull()
+ }
+
+ @Test
+ fun setScale_rendersFullPageBitmap() {
+ bitmapFetcher.onScaleChanged(1.5f)
+
+ testDispatcher.scheduler.runCurrent()
+
+ val pageBitmaps = bitmapFetcher.pageContents
+ assertThat(pageBitmaps).isInstanceOf(FullPageBitmap::class.java)
+ assertThat(pageBitmaps?.renderedScale).isEqualTo(1.5f)
+ pageBitmaps as FullPageBitmap // Make smartcast work nicely below
+ assertThat(pageBitmaps.bitmap.width).isEqualTo((pageSize.x * 1.5f).roundToInt())
+ assertThat(pageBitmaps.bitmap.height).isEqualTo((pageSize.y * 1.5f).roundToInt())
+ assertThat(invalidationCounter).isEqualTo(1)
+ }
+
+ @Test
+ fun setScale_rendersTileBoard() {
+ bitmapFetcher.onScaleChanged(5.0f)
+
+ testDispatcher.scheduler.runCurrent()
+
+ val pageBitmaps = bitmapFetcher.pageContents
+ assertThat(pageBitmaps).isInstanceOf(TileBoard::class.java)
+ assertThat(pageBitmaps?.renderedScale).isEqualTo(5.0f)
+ pageBitmaps as TileBoard // Make smartcast work nicely below
+
+ // Check the properties of an arbitrary full-size tile
+ val row0Col2 = pageBitmaps.tiles[2]
+ assertThat(row0Col2.bitmap?.width).isEqualTo(tileSizePx.x)
+ assertThat(row0Col2.bitmap?.height).isEqualTo(tileSizePx.y)
+ assertThat(row0Col2.offsetPx).isEqualTo(Point(tileSizePx.x * 2, 0))
+ assertThat(row0Col2.exactSizePx).isEqualTo(Point(tileSizePx.x, tileSizePx.y))
+
+ // Check the properties of the last tile in the bottom right corner
+ val row3Col3 = pageBitmaps.tiles[15]
+ val pageSizePx = Point(pageSize.x * 5, pageSize.y * 5)
+ // This tile is not the same size as the others b/c it's cut off on both edges
+ val row3Col3Size = Point(pageSizePx.x - tileSizePx.x * 3, pageSizePx.y - tileSizePx.y * 3)
+ assertThat(row3Col3.bitmap?.width).isEqualTo(row3Col3Size.x)
+ assertThat(row3Col3.bitmap?.height).isEqualTo(row3Col3Size.y)
+ assertThat(row3Col3.offsetPx).isEqualTo(Point(tileSizePx.x * 3, tileSizePx.y * 3))
+ assertThat(row3Col3.exactSizePx).isEqualTo(row3Col3Size)
+
+ // 1 invalidation for the low-res background, 1 for each tile * 16 tiles
+ assertThat(invalidationCounter).isEqualTo(17)
+ }
+
+ @Test
+ fun setScale_toRenderedValue_noNewWork() {
+ bitmapFetcher.isActive = true
+
+ bitmapFetcher.onScaleChanged(1.5f)
+ testDispatcher.scheduler.runCurrent()
+ val firstBitmaps = bitmapFetcher.pageContents
+ bitmapFetcher.onScaleChanged(1.5f)
+
+ // We shouldn't have started a new Job the second time onScaleChanged to the same value
+ assertThat(bitmapFetcher.renderingJob).isNull()
+ // And we should still have the same bitmaps
+ assertThat(bitmapFetcher.pageContents).isEqualTo(firstBitmaps)
+ // 1 total invalidation
+ assertThat(invalidationCounter).isEqualTo(1)
+ }
+
+ @Test
+ fun setScale_toRenderingValue_noNewWork() {
+ bitmapFetcher.onScaleChanged(1.5f)
+ val firstJob = bitmapFetcher.renderingJob
+ bitmapFetcher.onScaleChanged(1.5f)
+
+ // This should be the same Job we started the first time onScaleChanged
+ assertThat(bitmapFetcher.renderingJob).isEqualTo(firstJob)
+ // 0 invalidations because we're still rendering
+ assertThat(invalidationCounter).isEqualTo(0)
+ }
+
+ @Test
+ fun setScale_afterInactive_rendersNewBitmaps() {
+ bitmapFetcher.onScaleChanged(1.5f)
+ testDispatcher.scheduler.runCurrent()
+ assertThat(bitmapFetcher.pageContents).isNotNull()
+ assertThat(invalidationCounter).isEqualTo(1)
+
+ bitmapFetcher.isActive = false
+ assertThat(bitmapFetcher.pageContents).isNull()
+
+ bitmapFetcher.isActive = true
+ bitmapFetcher.onScaleChanged(1.5f)
+ testDispatcher.scheduler.runCurrent()
+ assertThat(bitmapFetcher.pageContents).isNotNull()
+ assertThat(invalidationCounter).isEqualTo(2)
+ }
+
+ @Test
+ fun setScale_fromFullPage_toTiled() {
+ bitmapFetcher.onScaleChanged(1.5f)
+ testDispatcher.scheduler.runCurrent()
+ val fullPageBitmap = bitmapFetcher.pageContents
+ assertThat(fullPageBitmap).isInstanceOf(FullPageBitmap::class.java)
+ assertThat(fullPageBitmap?.renderedScale).isEqualTo(1.5f)
+ assertThat(invalidationCounter).isEqualTo(1)
+
+ bitmapFetcher.onScaleChanged(5.0f)
+ testDispatcher.scheduler.runCurrent()
+ val tileBoard = bitmapFetcher.pageContents
+ assertThat(tileBoard).isInstanceOf(TileBoard::class.java)
+ assertThat(tileBoard?.renderedScale).isEqualTo(5.0f)
+ // 1 invalidation for the previous full page bitmap + 1 for the low res background
+ // + (1 for each tile * 16 tiles)
+ assertThat(invalidationCounter).isEqualTo(18)
+ }
+
+ @Test
+ fun setScale_fromTiled_toFullPage() {
+ bitmapFetcher.onScaleChanged(5.0f)
+ testDispatcher.scheduler.runCurrent()
+ val tileBoard = bitmapFetcher.pageContents
+ assertThat(tileBoard).isInstanceOf(TileBoard::class.java)
+ assertThat(tileBoard?.renderedScale).isEqualTo(5.0f)
+ // 1 invalidation for the low res background + (1 for each tile * 16 tiles)
+ assertThat(invalidationCounter).isEqualTo(17)
+
+ bitmapFetcher.onScaleChanged(1.5f)
+ testDispatcher.scheduler.runCurrent()
+ val fullPageBitmap = bitmapFetcher.pageContents
+ assertThat(fullPageBitmap).isInstanceOf(FullPageBitmap::class.java)
+ assertThat(fullPageBitmap?.renderedScale).isEqualTo(1.5f)
+ // 1 additional invalidation for the new full page bitmap
+ assertThat(invalidationCounter).isEqualTo(18)
+ }
+}
diff --git a/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/view/FakeBitmapSource.kt b/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/view/FakeBitmapSource.kt
new file mode 100644
index 0000000..5433bcb
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/view/FakeBitmapSource.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.pdf.view
+
+import android.graphics.Bitmap
+import android.graphics.Rect
+import android.util.Size
+import androidx.pdf.PdfDocument
+
+/**
+ * Fake implementation of [PdfDocument.BitmapSource] that always produces a blank bitmap of the
+ * requested size.
+ */
+internal class FakeBitmapSource(override val pageNumber: Int) : PdfDocument.BitmapSource {
+
+ override suspend fun getBitmap(scaledPageSizePx: Size, tileRegion: Rect?): Bitmap {
+ return if (tileRegion != null) {
+ Bitmap.createBitmap(tileRegion.width(), tileRegion.height(), Bitmap.Config.ARGB_8888)
+ } else {
+ Bitmap.createBitmap(
+ scaledPageSizePx.width,
+ scaledPageSizePx.height,
+ Bitmap.Config.ARGB_8888
+ )
+ }
+ }
+
+ override fun close() {
+ /* no-op, fake */
+ }
+}
diff --git a/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/view/PageTest.kt b/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/view/PageTest.kt
index 3abba3b..3a73055 100644
--- a/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/view/PageTest.kt
+++ b/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/view/PageTest.kt
@@ -16,14 +16,13 @@
package androidx.pdf.view
-import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Point
import android.graphics.Rect
import android.graphics.RectF
-import android.util.Size
import androidx.pdf.PdfDocument
+import androidx.pdf.content.PdfPageTextContent
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.test.StandardTestDispatcher
@@ -34,6 +33,7 @@
import org.mockito.kotlin.any
import org.mockito.kotlin.argThat
import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.isNull
import org.mockito.kotlin.mock
@@ -45,18 +45,29 @@
class PageTest {
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
+ private val pageContent =
+ PdfDocument.PdfPageContent(
+ listOf(PdfPageTextContent(listOf(RectF(10f, 10f, 50f, 20f)), "SampleText")),
+ emptyList() // No images in this test case
+ )
+
private val pdfDocument =
mock<PdfDocument> {
on { getPageBitmapSource(any()) } doAnswer
{ invocation ->
FakeBitmapSource(invocation.getArgument(0))
}
+ onBlocking { getPageContent(pageNumber = 0) } doReturn pageContent
}
private val canvasSpy = spy(Canvas())
private var invalidationCounter = 0
private val invalidationTracker: () -> Unit = { invalidationCounter++ }
+
+ private var pageTextReadyCounter = 0
+ private val onPageTextReady: ((Int) -> Unit) = { _ -> pageTextReadyCounter++ }
+
private lateinit var page: Page
@Before
@@ -64,89 +75,18 @@
// Cancel any work from previous tests, and reset tracking variables
testDispatcher.cancelChildren()
invalidationCounter = 0
+ pageTextReadyCounter = 0
- page = Page(0, size = PAGE_SIZE, pdfDocument, testScope, invalidationTracker)
- }
-
- @Test
- fun setVisible_fromInvisible() {
- // Set the page to visible
- page.setVisible(zoom = 1.0F)
-
- // Make sure we start and finish fetching a Bitmap
- assertThat(page.renderBitmapJob).isNotNull()
- testDispatcher.scheduler.runCurrent()
- assertThat(page.renderBitmapJob).isNull()
-
- // Make we fetched the right bitmap
- assertThat(page.bitmap).isNotNull()
- assertThat(page.bitmap?.width).isEqualTo(PAGE_SIZE.x)
- assertThat(page.bitmap?.height).isEqualTo(PAGE_SIZE.y)
- }
-
- @Test
- fun setVisible_fromVisible_noNewBitmaps() {
- // Set the page to visible once, and make sure we fetched the correct Bitmap
- page.setVisible(zoom = 1.0F)
-
- testDispatcher.scheduler.runCurrent()
- assertThat(page.bitmap).isNotNull()
- assertThat(page.bitmap?.width).isEqualTo(PAGE_SIZE.x)
- assertThat(page.bitmap?.height).isEqualTo(PAGE_SIZE.y)
-
- // Set the page to visible again, at the same zoom level, and make sure we don't fetch or
- // start fetching a new Bitmap
- page.setVisible(zoom = 1.0F)
-
- assertThat(page.renderBitmapJob).isNull()
- assertThat(page.bitmap).isNotNull()
- assertThat(page.bitmap?.width).isEqualTo(PAGE_SIZE.x)
- assertThat(page.bitmap?.height).isEqualTo(PAGE_SIZE.y)
-
- // 1 total invalidation from 1 Bitmap prepared
- assertThat(invalidationCounter).isEqualTo(1)
- }
-
- @Test
- fun setVisible_fromVisible_fetchNewBitmaps() {
- // Set the page to visible once, at 1.0 zoom, and make sure we fetched the correct Bitmap
- page.setVisible(zoom = 1.0F)
-
- testDispatcher.scheduler.runCurrent()
- assertThat(page.bitmap).isNotNull()
- assertThat(page.bitmap?.width).isEqualTo(PAGE_SIZE.x)
- assertThat(page.bitmap?.height).isEqualTo(PAGE_SIZE.y)
- assertThat(invalidationCounter).isEqualTo(1)
-
- // Set the page to visible again, but this time at 2.0 zoom, and make sure we fetch a
- // _new_ Bitmap
- page.setVisible(zoom = 2.0F)
-
- assertThat(page.renderBitmapJob).isNotNull()
- testDispatcher.scheduler.runCurrent()
- assertThat(page.bitmap).isNotNull()
- assertThat(page.bitmap?.width).isEqualTo(PAGE_SIZE.x * 2)
- assertThat(page.bitmap?.height).isEqualTo(PAGE_SIZE.y * 2)
-
- // 2 total invalidations from 2 Bitmaps prepared
- assertThat(invalidationCounter).isEqualTo(2)
- }
-
- @Test
- fun setInvisible() {
- // Set the page to visible, and make sure we start fetching a bitmap
- page.setVisible(zoom = 1.0F)
- assertThat(page.renderBitmapJob).isNotNull()
-
- // Set the page to invisible, make sure we stop fetching the bitmap, and make sure internal
- // state is updated appropriately
- page.setInvisible()
- testDispatcher.scheduler.runCurrent()
- assertThat(page.renderBitmapJob).isNull()
- assertThat(page.bitmap).isNull()
-
- // 0 invalidations, as the initial job to fetch a Bitmap should have been cancelled
- assertThat(invalidationCounter).isEqualTo(0)
+ page =
+ Page(
+ 0,
+ pageSizePx = PAGE_SIZE,
+ pdfDocument,
+ testScope,
+ MAX_BITMAP_SIZE,
+ invalidationTracker,
+ onPageTextReady
+ )
}
@Test
@@ -211,29 +151,37 @@
// Mockito's Spy functionality, as it captures arguments by reference, and we re-use
// Rect and Paint arguments to canvas.drawRect() to avoid allocations on the drawing path
}
+
+ @Test
+ fun setVisible_fetchesPageText() {
+ page.setVisible(zoom = 1.0f)
+ testDispatcher.scheduler.runCurrent()
+ assertThat(page.pageText).isEqualTo("SampleText")
+ assertThat(pageTextReadyCounter).isEqualTo(1)
+ }
+
+ @Test
+ fun setVisible_doesNotFetchPageTextIfAlreadyFetched() {
+ page.setVisible(zoom = 1.0f)
+ testDispatcher.scheduler.runCurrent()
+ assertThat(page.pageText).isEqualTo("SampleText")
+ assertThat(pageTextReadyCounter).isEqualTo(1)
+
+ page.setVisible(zoom = 1.0f)
+ testDispatcher.scheduler.runCurrent()
+ assertThat(page.pageText).isEqualTo("SampleText")
+ assertThat(pageTextReadyCounter).isEqualTo(1)
+ }
+
+ @Test
+ fun setInvisible_cancelsPageTextFetch() {
+ page.setVisible(zoom = 1.0f)
+ page.setInvisible()
+ testDispatcher.scheduler.runCurrent()
+ assertThat(page.pageText).isNull()
+ assertThat(pageTextReadyCounter).isEqualTo(0)
+ }
}
val PAGE_SIZE = Point(100, 150)
-
-/**
- * Fake implementation of [PdfDocument.BitmapSource] that always produces a blank bitmap of the
- * requested size.
- */
-private class FakeBitmapSource(override val pageNumber: Int) : PdfDocument.BitmapSource {
-
- override suspend fun getBitmap(scaledPageSizePx: Size, tileRegion: Rect?): Bitmap {
- return if (tileRegion != null) {
- Bitmap.createBitmap(tileRegion.width(), tileRegion.height(), Bitmap.Config.ARGB_8888)
- } else {
- Bitmap.createBitmap(
- scaledPageSizePx.width,
- scaledPageSizePx.height,
- Bitmap.Config.ARGB_8888
- )
- }
- }
-
- override fun close() {
- /* no-op, fake */
- }
-}
+val MAX_BITMAP_SIZE = Point(500, 500)
diff --git a/playground-common/androidx-shared.properties b/playground-common/androidx-shared.properties
index 6a94086..7b2e789 100644
--- a/playground-common/androidx-shared.properties
+++ b/playground-common/androidx-shared.properties
@@ -64,7 +64,7 @@
android.experimental.dependency.excludeLibraryComponentsFromConstraints=true
# Disallow resolving dependencies at configuration time, which is a slight performance problem
android.dependencyResolutionAtConfigurationTime.disallow=true
-android.suppressUnsupportedOptionWarnings=android.suppressUnsupportedOptionWarnings,android.dependencyResolutionAtConfigurationTime.disallow,android.experimental.lint.missingBaselineIsEmptyBaseline,android.lint.printStackTrace,android.lint.baselineOmitLineNumbers,android.experimental.disableCompileSdkChecks,android.overrideVersionCheck,android.r8.maxWorkers,android.experimental.lint.reservedMemoryPerTask,android.experimental.dependency.excludeLibraryComponentsFromConstraints,android.prefabVersion,android.experimental.privacysandboxsdk.plugin.enable,android.experimental.privacysandboxsdk.requireServices,android.lint.useK2Uast
+android.suppressUnsupportedOptionWarnings=android.suppressUnsupportedOptionWarnings,android.dependencyResolutionAtConfigurationTime.disallow,android.experimental.lint.missingBaselineIsEmptyBaseline,android.lint.printStackTrace,android.lint.baselineOmitLineNumbers,android.experimental.disableCompileSdkChecks,android.overrideVersionCheck,android.r8.maxWorkers,android.experimental.lint.reservedMemoryPerTask,android.experimental.dependency.excludeLibraryComponentsFromConstraints,android.prefabVersion,android.experimental.privacysandboxsdk.plugin.enable,android.experimental.privacysandboxsdk.requireServices,android.lint.useK2Uast,android.experimental.skipApksViaBundleIfPossible
# Workaround for b/162074215
android.includeDependencyInfoInApks=false
@@ -88,3 +88,5 @@
android.experimental.privacysandboxsdk.plugin.enable=true
# Allow non-shim usage
android.experimental.privacysandboxsdk.requireServices=false
+# Use fast-path APKs from AGP, ensuring that single APK will be used (for FTL configs)
+android.experimental.skipApksViaBundleIfPossible=true
diff --git a/preference/preference/build.gradle b/preference/preference/build.gradle
index b4bd3f9..dda828a 100644
--- a/preference/preference/build.gradle
+++ b/preference/preference/build.gradle
@@ -46,13 +46,6 @@
}
android {
- sourceSets {
- main.res.srcDirs = [
- "res",
- "res-public"
- ]
- }
-
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
diff --git a/preference/preference/res/layout/preference_list_fragment.xml b/preference/preference/res/layout/preference_list_fragment.xml
deleted file mode 100644
index c2ae115..0000000
--- a/preference/preference/res/layout/preference_list_fragment.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2015 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
- ~
- ~ http://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.
- -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- tools:ignore="NewApi"
- android:orientation="vertical"
- android:layout_height="match_parent"
- android:layout_width="match_parent" >
-
- <FrameLayout
- android:id="@android:id/list_container"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1" />
-
- <TextView android:id="@android:id/empty"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:padding="8dp"
- android:gravity="center"
- android:visibility="gone" />
-
-</LinearLayout>
diff --git a/preference/preference/res/drawable-v21/ic_arrow_down_24dp.xml b/preference/preference/src/main/res/drawable-v21/ic_arrow_down_24dp.xml
similarity index 100%
rename from preference/preference/res/drawable-v21/ic_arrow_down_24dp.xml
rename to preference/preference/src/main/res/drawable-v21/ic_arrow_down_24dp.xml
diff --git a/preference/preference/res/drawable-v21/preference_list_divider_material.xml b/preference/preference/src/main/res/drawable-v21/preference_list_divider_material.xml
similarity index 100%
rename from preference/preference/res/drawable-v21/preference_list_divider_material.xml
rename to preference/preference/src/main/res/drawable-v21/preference_list_divider_material.xml
diff --git a/preference/preference/res/drawable/ic_arrow_down_24dp.xml b/preference/preference/src/main/res/drawable/ic_arrow_down_24dp.xml
similarity index 100%
rename from preference/preference/res/drawable/ic_arrow_down_24dp.xml
rename to preference/preference/src/main/res/drawable/ic_arrow_down_24dp.xml
diff --git a/preference/preference/res/drawable/preference_list_divider_material.xml b/preference/preference/src/main/res/drawable/preference_list_divider_material.xml
similarity index 100%
rename from preference/preference/res/drawable/preference_list_divider_material.xml
rename to preference/preference/src/main/res/drawable/preference_list_divider_material.xml
diff --git a/preference/preference/res/layout/expand_button.xml b/preference/preference/src/main/res/layout/expand_button.xml
similarity index 100%
rename from preference/preference/res/layout/expand_button.xml
rename to preference/preference/src/main/res/layout/expand_button.xml
diff --git a/preference/preference/res/layout/image_frame.xml b/preference/preference/src/main/res/layout/image_frame.xml
similarity index 100%
rename from preference/preference/res/layout/image_frame.xml
rename to preference/preference/src/main/res/layout/image_frame.xml
diff --git a/preference/preference/res/layout/preference.xml b/preference/preference/src/main/res/layout/preference.xml
similarity index 100%
rename from preference/preference/res/layout/preference.xml
rename to preference/preference/src/main/res/layout/preference.xml
diff --git a/preference/preference/res/layout/preference_category.xml b/preference/preference/src/main/res/layout/preference_category.xml
similarity index 100%
rename from preference/preference/res/layout/preference_category.xml
rename to preference/preference/src/main/res/layout/preference_category.xml
diff --git a/preference/preference/res/layout/preference_category_material.xml b/preference/preference/src/main/res/layout/preference_category_material.xml
similarity index 100%
rename from preference/preference/res/layout/preference_category_material.xml
rename to preference/preference/src/main/res/layout/preference_category_material.xml
diff --git a/preference/preference/res/layout/preference_dialog_edittext.xml b/preference/preference/src/main/res/layout/preference_dialog_edittext.xml
similarity index 100%
rename from preference/preference/res/layout/preference_dialog_edittext.xml
rename to preference/preference/src/main/res/layout/preference_dialog_edittext.xml
diff --git a/preference/preference/res/layout/preference_dropdown.xml b/preference/preference/src/main/res/layout/preference_dropdown.xml
similarity index 100%
rename from preference/preference/res/layout/preference_dropdown.xml
rename to preference/preference/src/main/res/layout/preference_dropdown.xml
diff --git a/preference/preference/res/layout/preference_dropdown_material.xml b/preference/preference/src/main/res/layout/preference_dropdown_material.xml
similarity index 100%
rename from preference/preference/res/layout/preference_dropdown_material.xml
rename to preference/preference/src/main/res/layout/preference_dropdown_material.xml
diff --git a/preference/preference/res/layout/preference_information.xml b/preference/preference/src/main/res/layout/preference_information.xml
similarity index 100%
rename from preference/preference/res/layout/preference_information.xml
rename to preference/preference/src/main/res/layout/preference_information.xml
diff --git a/preference/preference/res/layout/preference_information_material.xml b/preference/preference/src/main/res/layout/preference_information_material.xml
similarity index 100%
rename from preference/preference/res/layout/preference_information_material.xml
rename to preference/preference/src/main/res/layout/preference_information_material.xml
diff --git a/preference/preference/src/main/res/layout/preference_list_fragment.xml b/preference/preference/src/main/res/layout/preference_list_fragment.xml
new file mode 100644
index 0000000..ad03481
--- /dev/null
+++ b/preference/preference/src/main/res/layout/preference_list_fragment.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 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
+ ~
+ ~ http://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.
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:ignore="NewApi"
+ android:orientation="vertical"
+ android:fitsSystemWindows="true"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent" >
+
+ <FrameLayout
+ android:id="@android:id/list_container"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+ <TextView android:id="@android:id/empty"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="8dp"
+ android:gravity="center"
+ android:visibility="gone" />
+
+</LinearLayout>
diff --git a/preference/preference/res/layout/preference_material.xml b/preference/preference/src/main/res/layout/preference_material.xml
similarity index 100%
rename from preference/preference/res/layout/preference_material.xml
rename to preference/preference/src/main/res/layout/preference_material.xml
diff --git a/preference/preference/res/layout/preference_recyclerview.xml b/preference/preference/src/main/res/layout/preference_recyclerview.xml
similarity index 100%
rename from preference/preference/res/layout/preference_recyclerview.xml
rename to preference/preference/src/main/res/layout/preference_recyclerview.xml
diff --git a/preference/preference/res/layout/preference_widget_checkbox.xml b/preference/preference/src/main/res/layout/preference_widget_checkbox.xml
similarity index 100%
rename from preference/preference/res/layout/preference_widget_checkbox.xml
rename to preference/preference/src/main/res/layout/preference_widget_checkbox.xml
diff --git a/preference/preference/res/layout/preference_widget_seekbar.xml b/preference/preference/src/main/res/layout/preference_widget_seekbar.xml
similarity index 100%
rename from preference/preference/res/layout/preference_widget_seekbar.xml
rename to preference/preference/src/main/res/layout/preference_widget_seekbar.xml
diff --git a/preference/preference/res/layout/preference_widget_seekbar_material.xml b/preference/preference/src/main/res/layout/preference_widget_seekbar_material.xml
similarity index 100%
rename from preference/preference/res/layout/preference_widget_seekbar_material.xml
rename to preference/preference/src/main/res/layout/preference_widget_seekbar_material.xml
diff --git a/preference/preference/res/layout/preference_widget_switch.xml b/preference/preference/src/main/res/layout/preference_widget_switch.xml
similarity index 100%
rename from preference/preference/res/layout/preference_widget_switch.xml
rename to preference/preference/src/main/res/layout/preference_widget_switch.xml
diff --git a/preference/preference/res/layout/preference_widget_switch_compat.xml b/preference/preference/src/main/res/layout/preference_widget_switch_compat.xml
similarity index 100%
rename from preference/preference/res/layout/preference_widget_switch_compat.xml
rename to preference/preference/src/main/res/layout/preference_widget_switch_compat.xml
diff --git a/preference/preference/res/values-af/strings.xml b/preference/preference/src/main/res/values-af/strings.xml
similarity index 100%
rename from preference/preference/res/values-af/strings.xml
rename to preference/preference/src/main/res/values-af/strings.xml
diff --git a/preference/preference/res/values-am/strings.xml b/preference/preference/src/main/res/values-am/strings.xml
similarity index 100%
rename from preference/preference/res/values-am/strings.xml
rename to preference/preference/src/main/res/values-am/strings.xml
diff --git a/preference/preference/res/values-ar/strings.xml b/preference/preference/src/main/res/values-ar/strings.xml
similarity index 100%
rename from preference/preference/res/values-ar/strings.xml
rename to preference/preference/src/main/res/values-ar/strings.xml
diff --git a/preference/preference/res/values-as/strings.xml b/preference/preference/src/main/res/values-as/strings.xml
similarity index 100%
rename from preference/preference/res/values-as/strings.xml
rename to preference/preference/src/main/res/values-as/strings.xml
diff --git a/preference/preference/res/values-az/strings.xml b/preference/preference/src/main/res/values-az/strings.xml
similarity index 100%
rename from preference/preference/res/values-az/strings.xml
rename to preference/preference/src/main/res/values-az/strings.xml
diff --git a/preference/preference/res/values-b+sr+Latn/strings.xml b/preference/preference/src/main/res/values-b+sr+Latn/strings.xml
similarity index 100%
rename from preference/preference/res/values-b+sr+Latn/strings.xml
rename to preference/preference/src/main/res/values-b+sr+Latn/strings.xml
diff --git a/preference/preference/res/values-be/strings.xml b/preference/preference/src/main/res/values-be/strings.xml
similarity index 100%
rename from preference/preference/res/values-be/strings.xml
rename to preference/preference/src/main/res/values-be/strings.xml
diff --git a/preference/preference/res/values-bg/strings.xml b/preference/preference/src/main/res/values-bg/strings.xml
similarity index 100%
rename from preference/preference/res/values-bg/strings.xml
rename to preference/preference/src/main/res/values-bg/strings.xml
diff --git a/preference/preference/res/values-bn/strings.xml b/preference/preference/src/main/res/values-bn/strings.xml
similarity index 100%
rename from preference/preference/res/values-bn/strings.xml
rename to preference/preference/src/main/res/values-bn/strings.xml
diff --git a/preference/preference/res/values-bs/strings.xml b/preference/preference/src/main/res/values-bs/strings.xml
similarity index 100%
rename from preference/preference/res/values-bs/strings.xml
rename to preference/preference/src/main/res/values-bs/strings.xml
diff --git a/preference/preference/res/values-ca/strings.xml b/preference/preference/src/main/res/values-ca/strings.xml
similarity index 100%
rename from preference/preference/res/values-ca/strings.xml
rename to preference/preference/src/main/res/values-ca/strings.xml
diff --git a/preference/preference/res/values-cs/strings.xml b/preference/preference/src/main/res/values-cs/strings.xml
similarity index 100%
rename from preference/preference/res/values-cs/strings.xml
rename to preference/preference/src/main/res/values-cs/strings.xml
diff --git a/preference/preference/res/values-da/strings.xml b/preference/preference/src/main/res/values-da/strings.xml
similarity index 100%
rename from preference/preference/res/values-da/strings.xml
rename to preference/preference/src/main/res/values-da/strings.xml
diff --git a/preference/preference/res/values-de/strings.xml b/preference/preference/src/main/res/values-de/strings.xml
similarity index 100%
rename from preference/preference/res/values-de/strings.xml
rename to preference/preference/src/main/res/values-de/strings.xml
diff --git a/preference/preference/res/values-el/strings.xml b/preference/preference/src/main/res/values-el/strings.xml
similarity index 100%
rename from preference/preference/res/values-el/strings.xml
rename to preference/preference/src/main/res/values-el/strings.xml
diff --git a/preference/preference/res/values-en-rAU/strings.xml b/preference/preference/src/main/res/values-en-rAU/strings.xml
similarity index 100%
rename from preference/preference/res/values-en-rAU/strings.xml
rename to preference/preference/src/main/res/values-en-rAU/strings.xml
diff --git a/preference/preference/res/values-en-rCA/strings.xml b/preference/preference/src/main/res/values-en-rCA/strings.xml
similarity index 100%
rename from preference/preference/res/values-en-rCA/strings.xml
rename to preference/preference/src/main/res/values-en-rCA/strings.xml
diff --git a/preference/preference/res/values-en-rGB/strings.xml b/preference/preference/src/main/res/values-en-rGB/strings.xml
similarity index 100%
rename from preference/preference/res/values-en-rGB/strings.xml
rename to preference/preference/src/main/res/values-en-rGB/strings.xml
diff --git a/preference/preference/res/values-en-rIN/strings.xml b/preference/preference/src/main/res/values-en-rIN/strings.xml
similarity index 100%
rename from preference/preference/res/values-en-rIN/strings.xml
rename to preference/preference/src/main/res/values-en-rIN/strings.xml
diff --git a/preference/preference/res/values-en-rXC/strings.xml b/preference/preference/src/main/res/values-en-rXC/strings.xml
similarity index 100%
rename from preference/preference/res/values-en-rXC/strings.xml
rename to preference/preference/src/main/res/values-en-rXC/strings.xml
diff --git a/preference/preference/res/values-es-rUS/strings.xml b/preference/preference/src/main/res/values-es-rUS/strings.xml
similarity index 100%
rename from preference/preference/res/values-es-rUS/strings.xml
rename to preference/preference/src/main/res/values-es-rUS/strings.xml
diff --git a/preference/preference/res/values-es/strings.xml b/preference/preference/src/main/res/values-es/strings.xml
similarity index 100%
rename from preference/preference/res/values-es/strings.xml
rename to preference/preference/src/main/res/values-es/strings.xml
diff --git a/preference/preference/res/values-et/strings.xml b/preference/preference/src/main/res/values-et/strings.xml
similarity index 100%
rename from preference/preference/res/values-et/strings.xml
rename to preference/preference/src/main/res/values-et/strings.xml
diff --git a/preference/preference/res/values-eu/strings.xml b/preference/preference/src/main/res/values-eu/strings.xml
similarity index 100%
rename from preference/preference/res/values-eu/strings.xml
rename to preference/preference/src/main/res/values-eu/strings.xml
diff --git a/preference/preference/res/values-fa/strings.xml b/preference/preference/src/main/res/values-fa/strings.xml
similarity index 100%
rename from preference/preference/res/values-fa/strings.xml
rename to preference/preference/src/main/res/values-fa/strings.xml
diff --git a/preference/preference/res/values-fi/strings.xml b/preference/preference/src/main/res/values-fi/strings.xml
similarity index 100%
rename from preference/preference/res/values-fi/strings.xml
rename to preference/preference/src/main/res/values-fi/strings.xml
diff --git a/preference/preference/res/values-fr-rCA/strings.xml b/preference/preference/src/main/res/values-fr-rCA/strings.xml
similarity index 100%
rename from preference/preference/res/values-fr-rCA/strings.xml
rename to preference/preference/src/main/res/values-fr-rCA/strings.xml
diff --git a/preference/preference/res/values-fr/strings.xml b/preference/preference/src/main/res/values-fr/strings.xml
similarity index 100%
rename from preference/preference/res/values-fr/strings.xml
rename to preference/preference/src/main/res/values-fr/strings.xml
diff --git a/preference/preference/res/values-gl/strings.xml b/preference/preference/src/main/res/values-gl/strings.xml
similarity index 100%
rename from preference/preference/res/values-gl/strings.xml
rename to preference/preference/src/main/res/values-gl/strings.xml
diff --git a/preference/preference/res/values-gu/strings.xml b/preference/preference/src/main/res/values-gu/strings.xml
similarity index 100%
rename from preference/preference/res/values-gu/strings.xml
rename to preference/preference/src/main/res/values-gu/strings.xml
diff --git a/preference/preference/res/values-hi/strings.xml b/preference/preference/src/main/res/values-hi/strings.xml
similarity index 100%
rename from preference/preference/res/values-hi/strings.xml
rename to preference/preference/src/main/res/values-hi/strings.xml
diff --git a/preference/preference/res/values-hr/strings.xml b/preference/preference/src/main/res/values-hr/strings.xml
similarity index 100%
rename from preference/preference/res/values-hr/strings.xml
rename to preference/preference/src/main/res/values-hr/strings.xml
diff --git a/preference/preference/res/values-hu/strings.xml b/preference/preference/src/main/res/values-hu/strings.xml
similarity index 100%
rename from preference/preference/res/values-hu/strings.xml
rename to preference/preference/src/main/res/values-hu/strings.xml
diff --git a/preference/preference/res/values-hy/strings.xml b/preference/preference/src/main/res/values-hy/strings.xml
similarity index 100%
rename from preference/preference/res/values-hy/strings.xml
rename to preference/preference/src/main/res/values-hy/strings.xml
diff --git a/preference/preference/res/values-in/strings.xml b/preference/preference/src/main/res/values-in/strings.xml
similarity index 100%
rename from preference/preference/res/values-in/strings.xml
rename to preference/preference/src/main/res/values-in/strings.xml
diff --git a/preference/preference/res/values-is/strings.xml b/preference/preference/src/main/res/values-is/strings.xml
similarity index 100%
rename from preference/preference/res/values-is/strings.xml
rename to preference/preference/src/main/res/values-is/strings.xml
diff --git a/preference/preference/res/values-it/strings.xml b/preference/preference/src/main/res/values-it/strings.xml
similarity index 100%
rename from preference/preference/res/values-it/strings.xml
rename to preference/preference/src/main/res/values-it/strings.xml
diff --git a/preference/preference/res/values-iw/strings.xml b/preference/preference/src/main/res/values-iw/strings.xml
similarity index 100%
rename from preference/preference/res/values-iw/strings.xml
rename to preference/preference/src/main/res/values-iw/strings.xml
diff --git a/preference/preference/res/values-ja/strings.xml b/preference/preference/src/main/res/values-ja/strings.xml
similarity index 100%
rename from preference/preference/res/values-ja/strings.xml
rename to preference/preference/src/main/res/values-ja/strings.xml
diff --git a/preference/preference/res/values-ka/strings.xml b/preference/preference/src/main/res/values-ka/strings.xml
similarity index 100%
rename from preference/preference/res/values-ka/strings.xml
rename to preference/preference/src/main/res/values-ka/strings.xml
diff --git a/preference/preference/res/values-kk/strings.xml b/preference/preference/src/main/res/values-kk/strings.xml
similarity index 100%
rename from preference/preference/res/values-kk/strings.xml
rename to preference/preference/src/main/res/values-kk/strings.xml
diff --git a/preference/preference/res/values-km/strings.xml b/preference/preference/src/main/res/values-km/strings.xml
similarity index 100%
rename from preference/preference/res/values-km/strings.xml
rename to preference/preference/src/main/res/values-km/strings.xml
diff --git a/preference/preference/res/values-kn/strings.xml b/preference/preference/src/main/res/values-kn/strings.xml
similarity index 100%
rename from preference/preference/res/values-kn/strings.xml
rename to preference/preference/src/main/res/values-kn/strings.xml
diff --git a/preference/preference/res/values-ko/strings.xml b/preference/preference/src/main/res/values-ko/strings.xml
similarity index 100%
rename from preference/preference/res/values-ko/strings.xml
rename to preference/preference/src/main/res/values-ko/strings.xml
diff --git a/preference/preference/res/values-ky/strings.xml b/preference/preference/src/main/res/values-ky/strings.xml
similarity index 100%
rename from preference/preference/res/values-ky/strings.xml
rename to preference/preference/src/main/res/values-ky/strings.xml
diff --git a/preference/preference/res/values-lo/strings.xml b/preference/preference/src/main/res/values-lo/strings.xml
similarity index 100%
rename from preference/preference/res/values-lo/strings.xml
rename to preference/preference/src/main/res/values-lo/strings.xml
diff --git a/preference/preference/res/values-lt/strings.xml b/preference/preference/src/main/res/values-lt/strings.xml
similarity index 100%
rename from preference/preference/res/values-lt/strings.xml
rename to preference/preference/src/main/res/values-lt/strings.xml
diff --git a/preference/preference/res/values-lv/strings.xml b/preference/preference/src/main/res/values-lv/strings.xml
similarity index 100%
rename from preference/preference/res/values-lv/strings.xml
rename to preference/preference/src/main/res/values-lv/strings.xml
diff --git a/preference/preference/res/values-mk/strings.xml b/preference/preference/src/main/res/values-mk/strings.xml
similarity index 100%
rename from preference/preference/res/values-mk/strings.xml
rename to preference/preference/src/main/res/values-mk/strings.xml
diff --git a/preference/preference/res/values-ml/strings.xml b/preference/preference/src/main/res/values-ml/strings.xml
similarity index 100%
rename from preference/preference/res/values-ml/strings.xml
rename to preference/preference/src/main/res/values-ml/strings.xml
diff --git a/preference/preference/res/values-mn/strings.xml b/preference/preference/src/main/res/values-mn/strings.xml
similarity index 100%
rename from preference/preference/res/values-mn/strings.xml
rename to preference/preference/src/main/res/values-mn/strings.xml
diff --git a/preference/preference/res/values-mr/strings.xml b/preference/preference/src/main/res/values-mr/strings.xml
similarity index 100%
rename from preference/preference/res/values-mr/strings.xml
rename to preference/preference/src/main/res/values-mr/strings.xml
diff --git a/preference/preference/res/values-ms/strings.xml b/preference/preference/src/main/res/values-ms/strings.xml
similarity index 100%
rename from preference/preference/res/values-ms/strings.xml
rename to preference/preference/src/main/res/values-ms/strings.xml
diff --git a/preference/preference/res/values-my/strings.xml b/preference/preference/src/main/res/values-my/strings.xml
similarity index 100%
rename from preference/preference/res/values-my/strings.xml
rename to preference/preference/src/main/res/values-my/strings.xml
diff --git a/preference/preference/res/values-nb/strings.xml b/preference/preference/src/main/res/values-nb/strings.xml
similarity index 100%
rename from preference/preference/res/values-nb/strings.xml
rename to preference/preference/src/main/res/values-nb/strings.xml
diff --git a/preference/preference/res/values-ne/strings.xml b/preference/preference/src/main/res/values-ne/strings.xml
similarity index 100%
rename from preference/preference/res/values-ne/strings.xml
rename to preference/preference/src/main/res/values-ne/strings.xml
diff --git a/preference/preference/res/values-nl/strings.xml b/preference/preference/src/main/res/values-nl/strings.xml
similarity index 100%
rename from preference/preference/res/values-nl/strings.xml
rename to preference/preference/src/main/res/values-nl/strings.xml
diff --git a/preference/preference/res/values-or/strings.xml b/preference/preference/src/main/res/values-or/strings.xml
similarity index 100%
rename from preference/preference/res/values-or/strings.xml
rename to preference/preference/src/main/res/values-or/strings.xml
diff --git a/preference/preference/res/values-pa/strings.xml b/preference/preference/src/main/res/values-pa/strings.xml
similarity index 100%
rename from preference/preference/res/values-pa/strings.xml
rename to preference/preference/src/main/res/values-pa/strings.xml
diff --git a/preference/preference/res/values-pl/strings.xml b/preference/preference/src/main/res/values-pl/strings.xml
similarity index 100%
rename from preference/preference/res/values-pl/strings.xml
rename to preference/preference/src/main/res/values-pl/strings.xml
diff --git a/preference/preference/res/values-pt-rBR/strings.xml b/preference/preference/src/main/res/values-pt-rBR/strings.xml
similarity index 100%
rename from preference/preference/res/values-pt-rBR/strings.xml
rename to preference/preference/src/main/res/values-pt-rBR/strings.xml
diff --git a/preference/preference/res/values-pt-rPT/strings.xml b/preference/preference/src/main/res/values-pt-rPT/strings.xml
similarity index 100%
rename from preference/preference/res/values-pt-rPT/strings.xml
rename to preference/preference/src/main/res/values-pt-rPT/strings.xml
diff --git a/preference/preference/res/values-pt/strings.xml b/preference/preference/src/main/res/values-pt/strings.xml
similarity index 100%
rename from preference/preference/res/values-pt/strings.xml
rename to preference/preference/src/main/res/values-pt/strings.xml
diff --git a/preference/preference/res/values-ro/strings.xml b/preference/preference/src/main/res/values-ro/strings.xml
similarity index 100%
rename from preference/preference/res/values-ro/strings.xml
rename to preference/preference/src/main/res/values-ro/strings.xml
diff --git a/preference/preference/res/values-ru/strings.xml b/preference/preference/src/main/res/values-ru/strings.xml
similarity index 100%
rename from preference/preference/res/values-ru/strings.xml
rename to preference/preference/src/main/res/values-ru/strings.xml
diff --git a/preference/preference/res/values-si/strings.xml b/preference/preference/src/main/res/values-si/strings.xml
similarity index 100%
rename from preference/preference/res/values-si/strings.xml
rename to preference/preference/src/main/res/values-si/strings.xml
diff --git a/preference/preference/res/values-sk/strings.xml b/preference/preference/src/main/res/values-sk/strings.xml
similarity index 100%
rename from preference/preference/res/values-sk/strings.xml
rename to preference/preference/src/main/res/values-sk/strings.xml
diff --git a/preference/preference/res/values-sl/strings.xml b/preference/preference/src/main/res/values-sl/strings.xml
similarity index 100%
rename from preference/preference/res/values-sl/strings.xml
rename to preference/preference/src/main/res/values-sl/strings.xml
diff --git a/preference/preference/res/values-sq/strings.xml b/preference/preference/src/main/res/values-sq/strings.xml
similarity index 100%
rename from preference/preference/res/values-sq/strings.xml
rename to preference/preference/src/main/res/values-sq/strings.xml
diff --git a/preference/preference/res/values-sr/strings.xml b/preference/preference/src/main/res/values-sr/strings.xml
similarity index 100%
rename from preference/preference/res/values-sr/strings.xml
rename to preference/preference/src/main/res/values-sr/strings.xml
diff --git a/preference/preference/res/values-sv/strings.xml b/preference/preference/src/main/res/values-sv/strings.xml
similarity index 100%
rename from preference/preference/res/values-sv/strings.xml
rename to preference/preference/src/main/res/values-sv/strings.xml
diff --git a/preference/preference/res/values-sw/strings.xml b/preference/preference/src/main/res/values-sw/strings.xml
similarity index 100%
rename from preference/preference/res/values-sw/strings.xml
rename to preference/preference/src/main/res/values-sw/strings.xml
diff --git a/preference/preference/res/values-sw360dp/config.xml b/preference/preference/src/main/res/values-sw360dp/config.xml
similarity index 100%
rename from preference/preference/res/values-sw360dp/config.xml
rename to preference/preference/src/main/res/values-sw360dp/config.xml
diff --git a/preference/preference/res/values-ta/strings.xml b/preference/preference/src/main/res/values-ta/strings.xml
similarity index 100%
rename from preference/preference/res/values-ta/strings.xml
rename to preference/preference/src/main/res/values-ta/strings.xml
diff --git a/preference/preference/res/values-te/strings.xml b/preference/preference/src/main/res/values-te/strings.xml
similarity index 100%
rename from preference/preference/res/values-te/strings.xml
rename to preference/preference/src/main/res/values-te/strings.xml
diff --git a/preference/preference/res/values-th/strings.xml b/preference/preference/src/main/res/values-th/strings.xml
similarity index 100%
rename from preference/preference/res/values-th/strings.xml
rename to preference/preference/src/main/res/values-th/strings.xml
diff --git a/preference/preference/res/values-tl/strings.xml b/preference/preference/src/main/res/values-tl/strings.xml
similarity index 100%
rename from preference/preference/res/values-tl/strings.xml
rename to preference/preference/src/main/res/values-tl/strings.xml
diff --git a/preference/preference/res/values-tr/strings.xml b/preference/preference/src/main/res/values-tr/strings.xml
similarity index 100%
rename from preference/preference/res/values-tr/strings.xml
rename to preference/preference/src/main/res/values-tr/strings.xml
diff --git a/preference/preference/res/values-uk/strings.xml b/preference/preference/src/main/res/values-uk/strings.xml
similarity index 100%
rename from preference/preference/res/values-uk/strings.xml
rename to preference/preference/src/main/res/values-uk/strings.xml
diff --git a/preference/preference/res/values-ur/strings.xml b/preference/preference/src/main/res/values-ur/strings.xml
similarity index 100%
rename from preference/preference/res/values-ur/strings.xml
rename to preference/preference/src/main/res/values-ur/strings.xml
diff --git a/preference/preference/res/values-uz/strings.xml b/preference/preference/src/main/res/values-uz/strings.xml
similarity index 100%
rename from preference/preference/res/values-uz/strings.xml
rename to preference/preference/src/main/res/values-uz/strings.xml
diff --git a/preference/preference/res/values-v21/dimens.xml b/preference/preference/src/main/res/values-v21/dimens.xml
similarity index 100%
rename from preference/preference/res/values-v21/dimens.xml
rename to preference/preference/src/main/res/values-v21/dimens.xml
diff --git a/preference/preference/res/values-v21/styles.xml b/preference/preference/src/main/res/values-v21/styles.xml
similarity index 100%
rename from preference/preference/res/values-v21/styles.xml
rename to preference/preference/src/main/res/values-v21/styles.xml
diff --git a/preference/preference/res/values-v21/themes.xml b/preference/preference/src/main/res/values-v21/themes.xml
similarity index 100%
rename from preference/preference/res/values-v21/themes.xml
rename to preference/preference/src/main/res/values-v21/themes.xml
diff --git a/preference/preference/res/values-vi/strings.xml b/preference/preference/src/main/res/values-vi/strings.xml
similarity index 100%
rename from preference/preference/res/values-vi/strings.xml
rename to preference/preference/src/main/res/values-vi/strings.xml
diff --git a/preference/preference/res/values-zh-rCN/strings.xml b/preference/preference/src/main/res/values-zh-rCN/strings.xml
similarity index 100%
rename from preference/preference/res/values-zh-rCN/strings.xml
rename to preference/preference/src/main/res/values-zh-rCN/strings.xml
diff --git a/preference/preference/res/values-zh-rHK/strings.xml b/preference/preference/src/main/res/values-zh-rHK/strings.xml
similarity index 100%
rename from preference/preference/res/values-zh-rHK/strings.xml
rename to preference/preference/src/main/res/values-zh-rHK/strings.xml
diff --git a/preference/preference/res/values-zh-rTW/strings.xml b/preference/preference/src/main/res/values-zh-rTW/strings.xml
similarity index 100%
rename from preference/preference/res/values-zh-rTW/strings.xml
rename to preference/preference/src/main/res/values-zh-rTW/strings.xml
diff --git a/preference/preference/res/values-zu/strings.xml b/preference/preference/src/main/res/values-zu/strings.xml
similarity index 100%
rename from preference/preference/res/values-zu/strings.xml
rename to preference/preference/src/main/res/values-zu/strings.xml
diff --git a/preference/preference/res/values/attrs.xml b/preference/preference/src/main/res/values/attrs.xml
similarity index 100%
rename from preference/preference/res/values/attrs.xml
rename to preference/preference/src/main/res/values/attrs.xml
diff --git a/preference/preference/res/values/colors.xml b/preference/preference/src/main/res/values/colors.xml
similarity index 100%
rename from preference/preference/res/values/colors.xml
rename to preference/preference/src/main/res/values/colors.xml
diff --git a/preference/preference/res/values/config.xml b/preference/preference/src/main/res/values/config.xml
similarity index 100%
rename from preference/preference/res/values/config.xml
rename to preference/preference/src/main/res/values/config.xml
diff --git a/preference/preference/res/values/dimens.xml b/preference/preference/src/main/res/values/dimens.xml
similarity index 100%
rename from preference/preference/res/values/dimens.xml
rename to preference/preference/src/main/res/values/dimens.xml
diff --git a/preference/preference/res/values/ids.xml b/preference/preference/src/main/res/values/ids.xml
similarity index 100%
rename from preference/preference/res/values/ids.xml
rename to preference/preference/src/main/res/values/ids.xml
diff --git a/preference/preference/res/values/integers.xml b/preference/preference/src/main/res/values/integers.xml
similarity index 100%
rename from preference/preference/res/values/integers.xml
rename to preference/preference/src/main/res/values/integers.xml
diff --git a/preference/preference/res-public/values/public_attrs.xml b/preference/preference/src/main/res/values/public_attrs.xml
similarity index 100%
rename from preference/preference/res-public/values/public_attrs.xml
rename to preference/preference/src/main/res/values/public_attrs.xml
diff --git a/preference/preference/res-public/values/public_styles.xml b/preference/preference/src/main/res/values/public_styles.xml
similarity index 100%
rename from preference/preference/res-public/values/public_styles.xml
rename to preference/preference/src/main/res/values/public_styles.xml
diff --git a/preference/preference/res-public/values/public_themes.xml b/preference/preference/src/main/res/values/public_themes.xml
similarity index 100%
rename from preference/preference/res-public/values/public_themes.xml
rename to preference/preference/src/main/res/values/public_themes.xml
diff --git a/preference/preference/res/values/strings.xml b/preference/preference/src/main/res/values/strings.xml
similarity index 100%
rename from preference/preference/res/values/strings.xml
rename to preference/preference/src/main/res/values/strings.xml
diff --git a/preference/preference/res/values/styles.xml b/preference/preference/src/main/res/values/styles.xml
similarity index 100%
rename from preference/preference/res/values/styles.xml
rename to preference/preference/src/main/res/values/styles.xml
diff --git a/preference/preference/res/values/themes.xml b/preference/preference/src/main/res/values/themes.xml
similarity index 100%
rename from preference/preference/res/values/themes.xml
rename to preference/preference/src/main/res/values/themes.xml
diff --git a/privacysandbox/ads/ads-adservices-java/build.gradle b/privacysandbox/ads/ads-adservices-java/build.gradle
index 07ed384..56debed 100644
--- a/privacysandbox/ads/ads-adservices-java/build.gradle
+++ b/privacysandbox/ads/ads-adservices-java/build.gradle
@@ -38,11 +38,11 @@
// To use CallbackToFutureAdapter
implementation "androidx.concurrent:concurrent-futures:1.1.0"
api(libs.guavaListenableFuture)
- implementation project(":privacysandbox:ads:ads-adservices")
+ implementation(project(":privacysandbox:ads:ads-adservices"))
androidTestImplementation(libs.kotlinCoroutinesAndroid)
- androidTestImplementation project(":privacysandbox:ads:ads-adservices")
- androidTestImplementation project(":javascriptengine:javascriptengine")
+ androidTestImplementation(project(":privacysandbox:ads:ads-adservices"))
+ androidTestImplementation(project(":javascriptengine:javascriptengine"))
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinTestJunit)
androidTestImplementation(libs.mockitoKotlin4)
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt
index df5782a..a6d6b6c 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt
@@ -49,7 +49,7 @@
* returned is null.
*/
@JvmStatic
- @SuppressLint("NewApi", "ClassVerificationFailure")
+ @SuppressLint("NewApi")
fun obtain(context: Context): AdIdManager? {
return if (AdServicesInfo.adServicesVersion() >= 4) {
AdIdManagerApi33Ext4Impl(context)
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi31Ext9Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi31Ext9Impl.kt
index 1e42cf8..27eaf51 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi31Ext9Impl.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi31Ext9Impl.kt
@@ -23,7 +23,7 @@
import androidx.annotation.RestrictTo
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("NewApi", "ClassVerificationFailure")
+@SuppressLint("NewApi")
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
class AdIdManagerApi31Ext9Impl(context: Context) :
AdIdManagerImplCommon(android.adservices.adid.AdIdManager.get(context))
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi33Ext4Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi33Ext4Impl.kt
index 77d2421..1466843 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi33Ext4Impl.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi33Ext4Impl.kt
@@ -23,7 +23,7 @@
import androidx.annotation.RestrictTo
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("NewApi", "ClassVerificationFailure")
+@SuppressLint("NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
class AdIdManagerApi33Ext4Impl(context: Context) :
AdIdManagerImplCommon(
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerImplCommon.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerImplCommon.kt
index 94c2ad7..5e294d2 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerImplCommon.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerImplCommon.kt
@@ -28,7 +28,7 @@
import kotlinx.coroutines.suspendCancellableCoroutine
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("ClassVerificationFailure", "NewApi")
+@SuppressLint("NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
open class AdIdManagerImplCommon(private val mAdIdManager: android.adservices.adid.AdIdManager) :
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionConfig.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionConfig.kt
index bec5c9a..4991c75 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionConfig.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionConfig.kt
@@ -16,7 +16,6 @@
package androidx.privacysandbox.ads.adservices.adselection
-import android.annotation.SuppressLint
import android.net.Uri
import android.os.Build
import android.os.ext.SdkExtensions
@@ -47,7 +46,6 @@
* @param trustedScoringSignalsUri URI endpoint of sell-side trusted signal from which creative
* specific realtime information can be fetched from.
*/
-@SuppressLint("ClassVerificationFailure")
class AdSelectionConfig
public constructor(
val seller: AdTechIdentifier,
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManager.kt
index 7a3b053..6b8efc4 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManager.kt
@@ -283,7 +283,7 @@
* value returned is null.
*/
@JvmStatic
- @SuppressLint("NewApi", "ClassVerificationFailure")
+ @SuppressLint("NewApi")
fun obtain(context: Context): AdSelectionManager? {
return if (AdServicesInfo.adServicesVersion() >= 4) {
AdSelectionManagerApi33Ext4Impl(context)
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerApi31Ext9Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerApi31Ext9Impl.kt
index 54070d5..24f8a2e 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerApi31Ext9Impl.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerApi31Ext9Impl.kt
@@ -23,7 +23,7 @@
import androidx.annotation.RestrictTo
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("NewApi", "ClassVerificationFailure")
+@SuppressLint("NewApi")
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
class AdSelectionManagerApi31Ext9Impl(context: Context) :
AdSelectionManagerImplCommon(android.adservices.adselection.AdSelectionManager.get(context))
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerApi33Ext4Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerApi33Ext4Impl.kt
index babf6a3..e27b121 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerApi33Ext4Impl.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerApi33Ext4Impl.kt
@@ -23,7 +23,7 @@
import androidx.annotation.RestrictTo
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("NewApi", "ClassVerificationFailure")
+@SuppressLint("NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
class AdSelectionManagerApi33Ext4Impl(context: Context) :
AdSelectionManagerImplCommon(
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerImplCommon.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerImplCommon.kt
index cd21ad8..2db68c6 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerImplCommon.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerImplCommon.kt
@@ -31,7 +31,7 @@
@OptIn(ExperimentalFeatures.Ext8OptIn::class, ExperimentalFeatures.Ext10OptIn::class)
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("NewApi", "ClassVerificationFailure")
+@SuppressLint("NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
open class AdSelectionManagerImplCommon(
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionOutcome.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionOutcome.kt
index 4b437cb..59b102c 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionOutcome.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionOutcome.kt
@@ -16,7 +16,6 @@
package androidx.privacysandbox.ads.adservices.adselection
-import android.annotation.SuppressLint
import android.net.Uri
import android.os.Build
import android.os.ext.SdkExtensions
@@ -33,7 +32,6 @@
* selection.
* @param renderUri A render URL for the winning ad.
*/
-@SuppressLint("ClassVerificationFailure")
class AdSelectionOutcome public constructor(val adSelectionId: Long, val renderUri: Uri) {
/** Checks whether two [AdSelectionOutcome] objects contain the same information. */
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequest.kt
index ddae9b6..ad627dd 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequest.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequest.kt
@@ -36,7 +36,6 @@
* by [AdSelectionManager#getAdSelectionData} then the impression reporting request should only
* include the ad selection id.
*/
-@SuppressLint("ClassVerificationFailure")
class ReportImpressionRequest
public constructor(val adSelectionId: Long, val adSelectionConfig: AdSelectionConfig) {
@ExperimentalFeatures.Ext8OptIn
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt
index 0b0e371..152cac9 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt
@@ -44,7 +44,7 @@
* returned is null.
*/
@JvmStatic
- @SuppressLint("NewApi", "ClassVerificationFailure")
+ @SuppressLint("NewApi")
fun obtain(context: Context): AppSetIdManager? {
return if (AdServicesInfo.adServicesVersion() >= 4) {
AppSetIdManagerApi33Ext4Impl(context)
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerApi31Ext9Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerApi31Ext9Impl.kt
index eeddd6d..ab76870 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerApi31Ext9Impl.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerApi31Ext9Impl.kt
@@ -23,7 +23,7 @@
import androidx.annotation.RestrictTo
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("NewApi", "ClassVerificationFailure")
+@SuppressLint("NewApi")
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
class AppSetIdManagerApi31Ext9Impl(context: Context) :
AppSetIdManagerImplCommon(android.adservices.appsetid.AppSetIdManager.get(context))
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerApi33Ext4Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerApi33Ext4Impl.kt
index 54983ca..c6ad21a 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerApi33Ext4Impl.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerApi33Ext4Impl.kt
@@ -23,7 +23,7 @@
import androidx.annotation.RestrictTo
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("NewApi", "ClassVerificationFailure")
+@SuppressLint("NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
class AppSetIdManagerApi33Ext4Impl(context: Context) :
AppSetIdManagerImplCommon(
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerImplCommon.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerImplCommon.kt
index 0a422cc..dc38cc1 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerImplCommon.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerImplCommon.kt
@@ -26,7 +26,7 @@
import kotlinx.coroutines.suspendCancellableCoroutine
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("ClassVerificationFailure", "NewApi")
+@SuppressLint("NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
open class AppSetIdManagerImplCommon(
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdData.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdData.kt
index 670608e..47f8c48 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdData.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdData.kt
@@ -35,7 +35,6 @@
* @param adRenderId ad render id for server auctions
*/
@OptIn(ExperimentalFeatures.Ext8OptIn::class, ExperimentalFeatures.Ext10OptIn::class)
-@SuppressLint("ClassVerificationFailure")
class AdData
@ExperimentalFeatures.Ext10OptIn
public constructor(
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdSelectionSignals.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdSelectionSignals.kt
index 5d67943..59503a0 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdSelectionSignals.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdSelectionSignals.kt
@@ -16,7 +16,6 @@
package androidx.privacysandbox.ads.adservices.common
-import android.annotation.SuppressLint
import android.os.Build
import android.os.ext.SdkExtensions
import androidx.annotation.RequiresExtension
@@ -31,7 +30,6 @@
*
* @param signals Any valid JSON string to create the AdSelectionSignals with.
*/
-@SuppressLint("ClassVerificationFailure")
class AdSelectionSignals public constructor(val signals: String) {
/**
* Compares this AdSelectionSignals to the specified object. The result is true if and only if
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdTechIdentifier.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdTechIdentifier.kt
index 0273b1a..683a9bd 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdTechIdentifier.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdTechIdentifier.kt
@@ -16,7 +16,6 @@
package androidx.privacysandbox.ads.adservices.common
-import android.annotation.SuppressLint
import android.os.Build
import android.os.ext.SdkExtensions
import androidx.annotation.RequiresExtension
@@ -27,7 +26,6 @@
*
* @param identifier The identifier.
*/
-@SuppressLint("ClassVerificationFailure")
class AdTechIdentifier public constructor(val identifier: String) {
/**
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt
index db1f91e..9f5a392 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt
@@ -125,7 +125,7 @@
* value returned is null.
*/
@JvmStatic
- @SuppressLint("NewApi", "ClassVerificationFailure")
+ @SuppressLint("NewApi")
fun obtain(context: Context): CustomAudienceManager? {
return if (AdServicesInfo.adServicesVersion() >= 4) {
CustomAudienceManagerApi33Ext4Impl(context)
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerApi31Ext9Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerApi31Ext9Impl.kt
index ee9ddd0..4c3ff46 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerApi31Ext9Impl.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerApi31Ext9Impl.kt
@@ -23,7 +23,7 @@
import androidx.annotation.RestrictTo
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("NewApi", "ClassVerificationFailure")
+@SuppressLint("NewApi")
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
class CustomAudienceManagerApi31Ext9Impl(context: Context) :
CustomAudienceManagerImplCommon(
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerApi33Ext4Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerApi33Ext4Impl.kt
index 8ed1b45..924473c 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerApi33Ext4Impl.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerApi33Ext4Impl.kt
@@ -23,7 +23,7 @@
import androidx.annotation.RestrictTo
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("NewApi", "ClassVerificationFailure")
+@SuppressLint("NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
class CustomAudienceManagerApi33Ext4Impl(context: Context) :
CustomAudienceManagerImplCommon(
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerImplCommon.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerImplCommon.kt
index 9aff642..c333164 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerImplCommon.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerImplCommon.kt
@@ -32,7 +32,7 @@
@OptIn(ExperimentalFeatures.Ext10OptIn::class)
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("NewApi", "ClassVerificationFailure")
+@SuppressLint("NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
open class CustomAudienceManagerImplCommon(
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequest.kt
index 14782a9..7e005ae 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequest.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequest.kt
@@ -97,7 +97,7 @@
"Start=$start, End=$end, DomainUris=$domainUris, OriginUris=$originUris }"
}
- @SuppressLint("ClassVerificationFailure", "NewApi")
+ @SuppressLint("NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
internal fun convertToAdServices(): android.adservices.measurement.DeletionRequest {
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt
index ea8584b..ca65337 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt
@@ -137,7 +137,7 @@
* value returned is null.
*/
@JvmStatic
- @SuppressLint("NewApi", "ClassVerificationFailure")
+ @SuppressLint("NewApi")
fun obtain(context: Context): MeasurementManager? {
Log.d(
"MeasurementManager",
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerApi31Ext9Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerApi31Ext9Impl.kt
index 359681d..1ea2911 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerApi31Ext9Impl.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerApi31Ext9Impl.kt
@@ -23,7 +23,7 @@
import androidx.annotation.RestrictTo
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("NewApi", "ClassVerificationFailure")
+@SuppressLint("NewApi")
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
class MeasurementManagerApi31Ext9Impl(context: Context) :
MeasurementManagerImplCommon(android.adservices.measurement.MeasurementManager.get(context))
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerApi33Ext5Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerApi33Ext5Impl.kt
index f63061e..e7a052e 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerApi33Ext5Impl.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerApi33Ext5Impl.kt
@@ -23,7 +23,7 @@
import androidx.annotation.RestrictTo
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("NewApi", "ClassVerificationFailure")
+@SuppressLint("NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
class MeasurementManagerApi33Ext5Impl(context: Context) :
MeasurementManagerImplCommon(
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerImplCommon.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerImplCommon.kt
index 2932b8e..1eaf91c 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerImplCommon.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerImplCommon.kt
@@ -33,7 +33,7 @@
import kotlinx.coroutines.suspendCancellableCoroutine
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("NewApi", "ClassVerificationFailure")
+@SuppressLint("NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
open class MeasurementManagerImplCommon(
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParams.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParams.kt
index 1d3218f..0908a6a7 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParams.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParams.kt
@@ -51,7 +51,7 @@
}
internal companion object {
- @SuppressLint("ClassVerificationFailure", "NewApi")
+ @SuppressLint("NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
internal fun convertWebSourceParams(
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequest.kt
index 6499e73..8618b7c 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequest.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequest.kt
@@ -92,7 +92,7 @@
return "WebSourceRegistrationRequest { $vals }"
}
- @SuppressLint("ClassVerificationFailure", "NewApi")
+ @SuppressLint("NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
internal fun convertToAdServices():
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParams.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParams.kt
index bc3eefc..3d60473 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParams.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParams.kt
@@ -51,7 +51,7 @@
}
internal companion object {
- @SuppressLint("ClassVerificationFailure", "NewApi")
+ @SuppressLint("NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
internal fun convertWebTriggerParams(
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequest.kt
index 7384929..2993621 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequest.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequest.kt
@@ -49,7 +49,7 @@
"Destination=$destination"
}
- @SuppressLint("ClassVerificationFailure", "NewApi")
+ @SuppressLint("NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
internal fun convertToAdServices():
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/signals/ProtectedSignalsManagerImpl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/signals/ProtectedSignalsManagerImpl.kt
index b69bc12..ec0da07 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/signals/ProtectedSignalsManagerImpl.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/signals/ProtectedSignalsManagerImpl.kt
@@ -29,7 +29,7 @@
@ExperimentalFeatures.Ext12OptIn
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("NewApi", "ClassVerificationFailure")
+@SuppressLint("NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 12)
open class ProtectedSignalsManagerImpl(
private val protectedSignalsManager: android.adservices.signals.ProtectedSignalsManager
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequestHelper.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequestHelper.kt
index a44b7ca..31091bd 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequestHelper.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequestHelper.kt
@@ -16,7 +16,6 @@
package androidx.privacysandbox.ads.adservices.topics
-import android.annotation.SuppressLint
import android.os.Build
import android.os.ext.SdkExtensions
import androidx.annotation.RequiresExtension
@@ -24,7 +23,6 @@
/** Helper class to consolidate conversion logic for GetTopicsRequest. */
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("ClassVerificationFailure")
object GetTopicsRequestHelper {
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsResponseHelper.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsResponseHelper.kt
index 414c766..08bd019 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsResponseHelper.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsResponseHelper.kt
@@ -16,7 +16,6 @@
package androidx.privacysandbox.ads.adservices.topics
-import android.annotation.SuppressLint
import android.os.Build
import android.os.ext.SdkExtensions
import androidx.annotation.RequiresExtension
@@ -25,7 +24,6 @@
/** Helper class to consolidate conversion logic for GetTopicsResponse. */
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("ClassVerificationFailure")
object GetTopicsResponseHelper {
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt
index a1bbde3..91b77c9 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt
@@ -49,7 +49,7 @@
* value returned is null.
*/
@JvmStatic
- @SuppressLint("NewApi", "ClassVerificationFailure")
+ @SuppressLint("NewApi")
fun obtain(context: Context): TopicsManager? {
return if (AdServicesInfo.adServicesVersion() >= 11) {
TopicsManagerApi33Ext11Impl(context)
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerApi31Ext11Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerApi31Ext11Impl.kt
index 6352492..64f5f9f 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerApi31Ext11Impl.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerApi31Ext11Impl.kt
@@ -24,7 +24,7 @@
import androidx.privacysandbox.ads.adservices.common.ExperimentalFeatures
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("NewApi", "ClassVerificationFailure")
+@SuppressLint("NewApi")
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 11)
class TopicsManagerApi31Ext11Impl(context: Context) :
TopicsManagerImplCommon(
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerApi31Ext9Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerApi31Ext9Impl.kt
index cc73c22..70866366 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerApi31Ext9Impl.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerApi31Ext9Impl.kt
@@ -23,7 +23,7 @@
import androidx.annotation.RestrictTo
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("NewApi", "ClassVerificationFailure")
+@SuppressLint("NewApi")
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
class TopicsManagerApi31Ext9Impl(context: Context) :
TopicsManagerImplCommon(
diff --git a/privacysandbox/sdkruntime/integration-tests/macrobenchmark/build.gradle b/privacysandbox/sdkruntime/integration-tests/macrobenchmark/build.gradle
new file mode 100644
index 0000000..4d06e65
--- /dev/null
+++ b/privacysandbox/sdkruntime/integration-tests/macrobenchmark/build.gradle
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.test")
+ id("kotlin-android")
+}
+android {
+ namespace = "androidx.privacysandbox.sdkruntime.integration.macrobenchmark"
+ targetProjectPath = ":privacysandbox:sdkruntime:integration-tests:testapp"
+ experimentalProperties["android.experimental.self-instrumenting"] = true
+ defaultConfig {
+ // :internal-testutils-macrobenchmark
+ minSdk = 23
+ }
+ privacySandbox {
+ enable = true
+ }
+}
+
+// Create a release build type and make sure it's the only one enabled.
+// This is needed because we benchmark the release build type only.
+android.buildTypes { release {} }
+androidComponents { beforeVariants(selector().all()) { enabled = buildType == "release" } }
+
+dependencies {
+ implementation(project(":benchmark:benchmark-junit4"))
+ implementation(project(":benchmark:benchmark-macro-junit4"))
+ implementation(project(":internal-testutils-macrobenchmark"))
+ implementation(libs.testRules)
+ implementation(libs.testExtJunit)
+ implementation(libs.testCore)
+ implementation(libs.testRunner)
+ implementation(libs.testUiautomator)
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/integration-tests/macrobenchmark/src/main/java/androidx/privacysandbox/sdkruntime/integration/macrobenchmark/SdkRuntimeBenchmark.kt b/privacysandbox/sdkruntime/integration-tests/macrobenchmark/src/main/java/androidx/privacysandbox/sdkruntime/integration/macrobenchmark/SdkRuntimeBenchmark.kt
new file mode 100644
index 0000000..b2aa064
--- /dev/null
+++ b/privacysandbox/sdkruntime/integration-tests/macrobenchmark/src/main/java/androidx/privacysandbox/sdkruntime/integration/macrobenchmark/SdkRuntimeBenchmark.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.privacysandbox.sdkruntime.integration.macrobenchmark
+
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.testutils.measureStartup
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * ./gradlew :privacysandbox:sdkruntime:integration-tests:macrobenchmark:connectedReleaseAndroidTest
+ */
+@LargeTest
+@RunWith(Parameterized::class)
+class SdkRuntimeBenchmark(
+ @Suppress("unused") private val ciTestConfigType: String, // Added to test name by Parameterized
+) {
+ @get:Rule val benchmarkRule = MacrobenchmarkRule()
+
+ @Test
+ fun startup() =
+ benchmarkRule.measureStartup(
+ compilationMode = CompilationMode.DEFAULT,
+ startupMode = StartupMode.COLD,
+ packageName = "androidx.privacysandbox.sdkruntime.integration.testapp"
+ ) {
+ action = "androidx.privacysandbox.sdkruntime.integration.testapp.BenchmarkActivity"
+ }
+
+ companion object {
+ /** Add test config type (main or compat) to test name */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun params(): List<String> =
+ listOf(
+ InstrumentationRegistry.getArguments()
+ .getString("androidx.testConfigType", "LOCAL_RUN")
+ )
+ }
+}
diff --git a/privacysandbox/sdkruntime/integration-tests/testapp/build.gradle b/privacysandbox/sdkruntime/integration-tests/testapp/build.gradle
index 7334fd7..f7286b5 100644
--- a/privacysandbox/sdkruntime/integration-tests/testapp/build.gradle
+++ b/privacysandbox/sdkruntime/integration-tests/testapp/build.gradle
@@ -37,6 +37,9 @@
implementation("androidx.appcompat:appcompat:1.6.0")
implementation(project(":privacysandbox:sdkruntime:sdkruntime-client"))
+ // For macrobenchmarks on API 34+
+ implementation("androidx.profileinstaller:profileinstaller:1.4.1")
+
androidTestImplementation(project(":internal-testutils-runtime"))
androidTestImplementation(libs.truth)
androidTestImplementation(libs.testExtJunit)
diff --git a/privacysandbox/sdkruntime/integration-tests/testapp/src/main/AndroidManifest.xml b/privacysandbox/sdkruntime/integration-tests/testapp/src/main/AndroidManifest.xml
index 6072134..de84cc8 100644
--- a/privacysandbox/sdkruntime/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/privacysandbox/sdkruntime/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -31,5 +31,17 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
+ <activity
+ android:name=".BenchmarkActivity"
+ android:exported="true"
+ android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+ android:theme="@style/Base.Theme.AppCompat">
+ <intent-filter>
+ <action
+ android:name=
+ "androidx.privacysandbox.sdkruntime.integration.testapp.BenchmarkActivity" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
</application>
</manifest>
diff --git a/privacysandbox/sdkruntime/integration-tests/testapp/src/main/java/androidx/privacysandbox/sdkruntime/integration/testapp/BenchmarkActivity.kt b/privacysandbox/sdkruntime/integration-tests/testapp/src/main/java/androidx/privacysandbox/sdkruntime/integration/testapp/BenchmarkActivity.kt
new file mode 100644
index 0000000..3664c46
--- /dev/null
+++ b/privacysandbox/sdkruntime/integration-tests/testapp/src/main/java/androidx/privacysandbox/sdkruntime/integration/testapp/BenchmarkActivity.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.privacysandbox.sdkruntime.integration.testapp
+
+import android.app.Activity
+import android.os.Bundle
+import kotlinx.coroutines.runBlocking
+
+class BenchmarkActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val api = TestAppApi(applicationContext)
+ val sdkApi = runBlocking { api.loadTestSdk() }
+ val invertResult = sdkApi.invert(false)
+ if (!invertResult) {
+ throw RuntimeException("Something went wrong")
+ }
+ }
+}
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/build.gradle b/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
index 4774389..f4961a2 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
+++ b/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
@@ -101,7 +101,7 @@
api(libs.kotlinCoroutinesCore)
implementation("androidx.core:core-ktx:1.15.0")
- api project(":privacysandbox:sdkruntime:sdkruntime-core")
+ api(project(":privacysandbox:sdkruntime:sdkruntime-core"))
implementation("androidx.core:core:1.15.0")
implementation("androidx.activity:activity:1.9.3")
@@ -109,7 +109,7 @@
testImplementation(libs.junit)
testImplementation(libs.truth)
- testImplementation project(":room:room-compiler-processing-testing")
+ testImplementation(project(":room:room-compiler-processing-testing"))
// TODO(b/249982004): cleanup dependencies
androidTestImplementation(libs.testCore)
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerAppOwnedInterfacesTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerAppOwnedInterfacesTest.kt
index 7e5fdc6..57e7ede 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerAppOwnedInterfacesTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerAppOwnedInterfacesTest.kt
@@ -59,7 +59,7 @@
sandboxManagerCompat = SdkSandboxManagerCompat.from(context)
}
- @SuppressLint("NewApi", "ClassVerificationFailure") // For supporting DP Builds
+ @SuppressLint("NewApi") // For supporting DP Builds
@After
fun tearDown() {
SdkSandboxManagerCompat.reset()
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompat.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompat.kt
index 187c0fe..fe3b785 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompat.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompat.kt
@@ -408,7 +408,7 @@
}
private object AppOwnedSdkRegistryFactory {
- @SuppressLint("NewApi", "ClassVerificationFailure") // For supporting DP Builds
+ @SuppressLint("NewApi") // For supporting DP Builds
fun create(context: Context): AppOwnedSdkRegistry {
return if (
BuildCompat.AD_SERVICES_EXTENSION_INT >= 8 || AdServicesInfo.isDeveloperPreview()
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/AppOwnedSdkProvider.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/AppOwnedSdkProvider.kt
index dcd34f8..1051288 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/AppOwnedSdkProvider.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/AppOwnedSdkProvider.kt
@@ -46,7 +46,6 @@
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 8)
private class ApiAdServicesV8Impl(private val controller: SdkSandboxController) : ProviderImpl {
@DoNotInline
- @SuppressLint("ClassVerificationFailure") // flaky lint
override fun getAppOwnedSdkSandboxInterfaces(): List<AppOwnedSdkSandboxInterfaceCompat> {
val apiResult = controller.getAppOwnedSdkSandboxInterfaces()
return apiResult.map { AppOwnedSdkSandboxInterfaceCompat(it) }
@@ -54,7 +53,7 @@
}
companion object {
- @SuppressLint("NewApi", "ClassVerificationFailure") // For supporting DP Builds
+ @SuppressLint("NewApi") // For supporting DP Builds
fun create(controller: SdkSandboxController): AppOwnedSdkProvider {
return if (
BuildCompat.AD_SERVICES_EXTENSION_INT >= 8 || AdServicesInfo.isDeveloperPreview()
diff --git a/privacysandbox/sdkruntime/sdkruntime-provider/build.gradle b/privacysandbox/sdkruntime/sdkruntime-provider/build.gradle
index 69c7a41..b4c525b 100644
--- a/privacysandbox/sdkruntime/sdkruntime-provider/build.gradle
+++ b/privacysandbox/sdkruntime/sdkruntime-provider/build.gradle
@@ -30,7 +30,7 @@
}
dependencies {
- api project(":privacysandbox:sdkruntime:sdkruntime-core")
+ api(project(":privacysandbox:sdkruntime:sdkruntime-core"))
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testExtJunit)
diff --git a/privacysandbox/tools/integration-tests/testsdk-asb/build.gradle b/privacysandbox/tools/integration-tests/testsdk-asb/build.gradle
index bd5584c..fb19ea7 100644
--- a/privacysandbox/tools/integration-tests/testsdk-asb/build.gradle
+++ b/privacysandbox/tools/integration-tests/testsdk-asb/build.gradle
@@ -43,5 +43,5 @@
}
dependencies {
- include project(":privacysandbox:tools:integration-tests:testsdk")
+ include(project(":privacysandbox:tools:integration-tests:testsdk"))
}
diff --git a/privacysandbox/tools/tools-apicompiler/build.gradle b/privacysandbox/tools/tools-apicompiler/build.gradle
index f32bca4..52fb8f0 100644
--- a/privacysandbox/tools/tools-apicompiler/build.gradle
+++ b/privacysandbox/tools/tools-apicompiler/build.gradle
@@ -41,8 +41,8 @@
api(libs.kotlinStdlib)
implementation(libs.kspApi)
implementation(libs.kotlinPoet)
- implementation project(":privacysandbox:tools:tools")
- implementation project(":privacysandbox:tools:tools-core")
+ implementation(project(":privacysandbox:tools:tools"))
+ implementation(project(":privacysandbox:tools:tools-core"))
testImplementation(project(":privacysandbox:tools:tools-testing"))
testImplementation(project(":room:room-compiler-processing-testing"))
diff --git a/privacysandbox/tools/tools-apigenerator/build.gradle b/privacysandbox/tools/tools-apigenerator/build.gradle
index 40370c9..6ea7548 100644
--- a/privacysandbox/tools/tools-apigenerator/build.gradle
+++ b/privacysandbox/tools/tools-apigenerator/build.gradle
@@ -80,8 +80,8 @@
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib"
}
implementation(libs.kotlinPoet)
- implementation project(":privacysandbox:tools:tools")
- implementation project(":privacysandbox:tools:tools-core")
+ implementation(project(":privacysandbox:tools:tools"))
+ implementation(project(":privacysandbox:tools:tools-core"))
testImplementation(project(":internal-testutils-truth"))
testImplementation(project(":privacysandbox:tools:tools-apipackager"))
diff --git a/privacysandbox/tools/tools-apipackager/build.gradle b/privacysandbox/tools/tools-apipackager/build.gradle
index fa48338..55cbcf2 100644
--- a/privacysandbox/tools/tools-apipackager/build.gradle
+++ b/privacysandbox/tools/tools-apipackager/build.gradle
@@ -38,8 +38,8 @@
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib"
}
- implementation project(":privacysandbox:tools:tools")
- implementation project(":privacysandbox:tools:tools-core")
+ implementation(project(":privacysandbox:tools:tools"))
+ implementation(project(":privacysandbox:tools:tools-core"))
testImplementation(project(":internal-testutils-truth"))
testImplementation(project(":privacysandbox:tools:tools-testing"))
diff --git a/privacysandbox/tools/tools-core/build.gradle b/privacysandbox/tools/tools-core/build.gradle
index 908f915..69f7a93 100644
--- a/privacysandbox/tools/tools-core/build.gradle
+++ b/privacysandbox/tools/tools-core/build.gradle
@@ -41,7 +41,7 @@
api(libs.protobuf)
api(libs.kotlinPoet)
implementation(libs.guava)
- implementation project(":privacysandbox:tools:tools")
+ implementation(project(":privacysandbox:tools:tools"))
testImplementation(libs.junit)
testImplementation(libs.truth)
diff --git a/privacysandbox/tools/tools-testing/build.gradle b/privacysandbox/tools/tools-testing/build.gradle
index 1a20c8a..8281ce3 100644
--- a/privacysandbox/tools/tools-testing/build.gradle
+++ b/privacysandbox/tools/tools-testing/build.gradle
@@ -33,7 +33,7 @@
api(libs.kspApi)
api(libs.junit)
api(libs.truth)
- implementation project(":room:room-compiler-processing-testing")
+ implementation(project(":room:room-compiler-processing-testing"))
}
androidx {
diff --git a/privacysandbox/ui/integration-tests/mediateesdkprovider/build.gradle b/privacysandbox/ui/integration-tests/mediateesdkprovider/build.gradle
index 1294960..26a8672 100644
--- a/privacysandbox/ui/integration-tests/mediateesdkprovider/build.gradle
+++ b/privacysandbox/ui/integration-tests/mediateesdkprovider/build.gradle
@@ -37,7 +37,6 @@
api("androidx.annotation:annotation:1.8.1")
implementation project(":privacysandbox:sdkruntime:sdkruntime-provider")
implementation project(":privacysandbox:ui:integration-tests:testaidl")
- implementation project(":privacysandbox:ui:ui-core")
implementation project(":privacysandbox:ui:ui-provider")
implementation project(":privacysandbox:ui:integration-tests:sdkproviderutils")
}
diff --git a/privacysandbox/ui/integration-tests/sdkproviderutils/build.gradle b/privacysandbox/ui/integration-tests/sdkproviderutils/build.gradle
index 0d6df29..254eef8 100644
--- a/privacysandbox/ui/integration-tests/sdkproviderutils/build.gradle
+++ b/privacysandbox/ui/integration-tests/sdkproviderutils/build.gradle
@@ -28,7 +28,6 @@
dependencies {
implementation "androidx.activity:activity-ktx:1.7.2"
implementation project(":privacysandbox:ui:ui-client")
- implementation project(":privacysandbox:ui:ui-core")
implementation project(":privacysandbox:ui:ui-provider")
implementation project(":privacysandbox:sdkruntime:sdkruntime-core")
implementation project(":privacysandbox:activity:activity-core")
diff --git a/privacysandbox/ui/integration-tests/testaidl/src/main/aidl/androidx/privacysandbox/ui/integration/testaidl/ISdkApi.aidl b/privacysandbox/ui/integration-tests/testaidl/src/main/aidl/androidx/privacysandbox/ui/integration/testaidl/ISdkApi.aidl
deleted file mode 100644
index fe8f8b2..0000000
--- a/privacysandbox/ui/integration-tests/testaidl/src/main/aidl/androidx/privacysandbox/ui/integration/testaidl/ISdkApi.aidl
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx.privacysandbox.ui.integration.testaidl;
-
-import android.os.Bundle;
-
-interface ISdkApi {
- Bundle loadBannerAd(int adType, int sdkType, boolean withSlowDraw, boolean drawViewability);
- void requestResize(int width, int height);
- oneway void triggerProcessDeath();
- oneway void launchFullscreenAd(in Bundle launcherInfo, int screenOrientation, int backButtonNavigation);
-}
diff --git a/privacysandbox/ui/integration-tests/testapp/build.gradle b/privacysandbox/ui/integration-tests/testapp/build.gradle
index f461c92..2ff14c0 100644
--- a/privacysandbox/ui/integration-tests/testapp/build.gradle
+++ b/privacysandbox/ui/integration-tests/testapp/build.gradle
@@ -55,14 +55,13 @@
implementation("androidx.compose.foundation:foundation-layout:1.7.5")
implementation("androidx.compose.material3:material3-android:1.3.1")
implementation("androidx.compose.ui:ui-util:1.7.5")
- implementation project(':privacysandbox:activity:activity-core')
- implementation project(':privacysandbox:activity:activity-client')
+ implementation(project(":privacysandbox:activity:activity-core"))
+ implementation(project(":privacysandbox:activity:activity-client"))
implementation(project(":privacysandbox:sdkruntime:sdkruntime-client"))
implementation(project(":privacysandbox:ui:integration-tests:testaidl"))
implementation(project(":privacysandbox:ui:integration-tests:sdkproviderutils"))
implementation(project(":privacysandbox:ui:integration-tests:testsdkproviderwrapper"))
implementation(project(":privacysandbox:ui:integration-tests:mediateesdkproviderwrapper"))
- implementation(project(":privacysandbox:ui:ui-core"))
implementation(project(":privacysandbox:ui:ui-client"))
implementation(project(":privacysandbox:ui:ui-provider"))
}
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
index 19f1db2..56e84a8 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
@@ -30,7 +30,8 @@
import androidx.privacysandbox.ui.client.view.SandboxedSdkViewEventListener
import androidx.privacysandbox.ui.integration.sdkproviderutils.SdkApiConstants.Companion.AdType
import androidx.privacysandbox.ui.integration.sdkproviderutils.SdkApiConstants.Companion.MediationOption
-import androidx.privacysandbox.ui.integration.testaidl.ISdkApi
+import androidx.privacysandbox.ui.integration.testsdkprovider.ISdkApi
+import androidx.privacysandbox.ui.integration.testsdkprovider.ISdkApiFactory
import kotlinx.coroutines.runBlocking
/**
@@ -57,7 +58,10 @@
loadedSdk = sdkSandboxManager.loadSdk(SDK_NAME, Bundle())
sdkSandboxManager.loadSdk(MEDIATEE_SDK_NAME, Bundle())
}
- sdkApi = ISdkApi.Stub.asInterface(loadedSdk.getInterface())
+ sdkApi =
+ ISdkApiFactory.wrapToISdkApi(
+ checkNotNull(loadedSdk.getInterface()) { "Cannot find Sdk Service!" }
+ )
}
}
@@ -105,9 +109,11 @@
drawViewabilityLayer: Boolean,
waitInsideOnDraw: Boolean = false
) {
- val sdkBundle =
- sdkApi.loadBannerAd(adType, mediationOption, waitInsideOnDraw, drawViewabilityLayer)
- sandboxedSdkView.setAdapter(SandboxedUiAdapterFactory.createFromCoreLibInfo(sdkBundle))
+ runBlocking {
+ val sdkBundle =
+ sdkApi.loadBannerAd(adType, mediationOption, waitInsideOnDraw, drawViewabilityLayer)
+ sandboxedSdkView.setAdapter(SandboxedUiAdapterFactory.createFromCoreLibInfo(sdkBundle))
+ }
}
open fun handleDrawerStateChange(isDrawerOpen: Boolean) {
diff --git a/privacysandbox/ui/integration-tests/testingutils/build.gradle b/privacysandbox/ui/integration-tests/testingutils/build.gradle
index 52c7232..fefa781 100644
--- a/privacysandbox/ui/integration-tests/testingutils/build.gradle
+++ b/privacysandbox/ui/integration-tests/testingutils/build.gradle
@@ -26,5 +26,5 @@
}
dependencies {
- implementation project(":privacysandbox:ui:ui-client")
+ implementation(project(":privacysandbox:ui:ui-client"))
}
diff --git a/privacysandbox/ui/integration-tests/testsdkprovider/build.gradle b/privacysandbox/ui/integration-tests/testsdkprovider/build.gradle
index b366e9a..b2f5d51 100644
--- a/privacysandbox/ui/integration-tests/testsdkprovider/build.gradle
+++ b/privacysandbox/ui/integration-tests/testsdkprovider/build.gradle
@@ -16,7 +16,7 @@
plugins {
id("AndroidXPlugin")
- id("com.android.library")
+ id("androidx.privacysandboxplugin")
id("org.jetbrains.kotlin.android")
}
@@ -35,10 +35,17 @@
dependencies {
api(libs.kotlinStdlib)
api("androidx.annotation:annotation:1.8.1")
- implementation project(":privacysandbox:sdkruntime:sdkruntime-provider")
- implementation project(":privacysandbox:ui:integration-tests:testaidl")
- implementation project(":privacysandbox:ui:integration-tests:sdkproviderutils")
- implementation project(":privacysandbox:ui:ui-core")
- implementation project(":privacysandbox:ui:ui-provider")
- implementation project(":webkit:webkit")
+ implementation(project(":privacysandbox:sdkruntime:sdkruntime-provider"))
+ implementation(project(":privacysandbox:ui:integration-tests:testaidl"))
+ implementation(project(":privacysandbox:ui:integration-tests:sdkproviderutils"))
+ implementation(project(":privacysandbox:ui:ui-provider"))
+ implementation(project(":webkit:webkit"))
+
+ // Add library dependencies at head - to override the dependencies that will be added by
+ // privacysandboxplugin.
+ implementation(project(':privacysandbox:tools:tools'))
+ ksp(project(':privacysandbox:tools:tools-apicompiler'))
+ implementation(project(":privacysandbox:ui:ui-core"))
+ implementation(project(':privacysandbox:activity:activity-core'))
+ implementation(project(':privacysandbox:activity:activity-provider'))
}
diff --git a/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/ISdkApi.kt b/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/ISdkApi.kt
new file mode 100644
index 0000000..02419f2
--- /dev/null
+++ b/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/ISdkApi.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.privacysandbox.ui.integration.testsdkprovider
+
+import android.os.Bundle
+import androidx.privacysandbox.tools.PrivacySandboxService
+
+@PrivacySandboxService
+interface ISdkApi {
+ suspend fun loadBannerAd(
+ adType: Int,
+ mediationOption: Int,
+ waitInsideOnDraw: Boolean,
+ drawViewability: Boolean
+ ): Bundle
+
+ fun requestResize(width: Int, height: Int)
+
+ fun triggerProcessDeath()
+
+ fun launchFullscreenAd(launcherInfo: Bundle, screenOrientation: Int, backButtonNavigation: Int)
+}
diff --git a/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt b/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
index 7c103f7..7cc4c81 100644
--- a/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
+++ b/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
@@ -35,16 +35,15 @@
import androidx.privacysandbox.ui.integration.sdkproviderutils.ViewabilityHandler
import androidx.privacysandbox.ui.integration.sdkproviderutils.fullscreen.FullscreenAd
import androidx.privacysandbox.ui.integration.testaidl.IMediateeSdkApi
-import androidx.privacysandbox.ui.integration.testaidl.ISdkApi
import androidx.privacysandbox.ui.provider.toCoreLibInfo
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
-class SdkApi(private val sdkContext: Context) : ISdkApi.Stub() {
+class SdkApi(private val sdkContext: Context) : ISdkApi {
private val testAdapters = TestAdapters(sdkContext)
private val handler = Handler(Looper.getMainLooper())
- override fun loadBannerAd(
+ override suspend fun loadBannerAd(
@AdType adType: Int,
@MediationOption mediationOption: Int,
waitInsideOnDraw: Boolean,
diff --git a/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkProviderImpl.kt b/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkProviderImpl.kt
index 62b9086..caac113a 100644
--- a/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkProviderImpl.kt
+++ b/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkProviderImpl.kt
@@ -17,17 +17,7 @@
package androidx.privacysandbox.ui.integration.testsdkprovider
import android.content.Context
-import android.os.Bundle
-import android.view.View
-import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
-import androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
-class SdkProviderImpl : SandboxedSdkProviderCompat() {
- override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
- return SandboxedSdkCompat(SdkApi(context!!))
- }
-
- override fun getView(windowContext: Context, params: Bundle, width: Int, height: Int): View {
- TODO("Not yet implemented")
- }
+class SdkProviderImpl : AbstractSandboxedSdkProviderCompat() {
+ override fun createISdkApi(context: Context): ISdkApi = SdkApi(context)
}
diff --git a/privacysandbox/ui/ui-client/build.gradle b/privacysandbox/ui/ui-client/build.gradle
index 54e3e13..1f05fe9 100644
--- a/privacysandbox/ui/ui-client/build.gradle
+++ b/privacysandbox/ui/ui-client/build.gradle
@@ -32,13 +32,11 @@
dependencies {
api(libs.kotlinStdlib)
api("androidx.annotation:annotation:1.8.1")
-
- implementation("androidx.core:core:1.12.0")
-
- implementation("androidx.lifecycle:lifecycle-common:2.6.2")
- implementation("androidx.privacysandbox.sdkruntime:sdkruntime-client:1.0.0-alpha14")
- implementation("androidx.customview:customview-poolingcontainer:1.0.0")
- implementation(project(":privacysandbox:ui:ui-core"))
+ api("androidx.core:core:1.12.0")
+ api("androidx.lifecycle:lifecycle-common:2.6.2")
+ api("androidx.privacysandbox.sdkruntime:sdkruntime-client:1.0.0-alpha14")
+ api("androidx.customview:customview-poolingcontainer:1.0.0")
+ api(project(":privacysandbox:ui:ui-core"))
androidTestImplementation(project(":internal-testutils-runtime"))
androidTestImplementation(libs.junit)
@@ -52,9 +50,9 @@
androidTestImplementation(libs.espressoCore)
androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.testUiautomator)
- androidTestImplementation project(":appcompat:appcompat")
- androidTestImplementation project(":privacysandbox:ui:ui-provider")
- androidTestImplementation project(":privacysandbox:ui:integration-tests:testingutils")
+ androidTestImplementation(project(":appcompat:appcompat"))
+ androidTestImplementation(project(":privacysandbox:ui:ui-provider"))
+ androidTestImplementation(project(":privacysandbox:ui:integration-tests:testingutils"))
}
android {
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
index 54bc440..2225374 100644
--- a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
@@ -391,7 +391,6 @@
}
}
- @SuppressLint("ClassVerificationFailure")
override fun notifyResized(width: Int, height: Int) {
val parentView = surfaceView.parent as View
diff --git a/privacysandbox/ui/ui-core/build.gradle b/privacysandbox/ui/ui-core/build.gradle
index bbb6d03..f451589 100644
--- a/privacysandbox/ui/ui-core/build.gradle
+++ b/privacysandbox/ui/ui-core/build.gradle
@@ -31,8 +31,9 @@
dependencies {
api(libs.kotlinStdlib)
- implementation(libs.kotlinCoroutinesCore)
+ api(libs.kotlinCoroutinesCore)
api("androidx.annotation:annotation:1.8.1")
+ api("androidx.core:core:1.13.0")
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.testExtJunit)
@@ -40,7 +41,6 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
- implementation("androidx.core:core:1.13.0")
}
android {
diff --git a/privacysandbox/ui/ui-provider/build.gradle b/privacysandbox/ui/ui-provider/build.gradle
index 67c11c6..0ebe484 100644
--- a/privacysandbox/ui/ui-provider/build.gradle
+++ b/privacysandbox/ui/ui-provider/build.gradle
@@ -31,11 +31,10 @@
dependencies {
api(libs.kotlinStdlib)
+ api(libs.kotlinCoroutinesCore)
api("androidx.annotation:annotation:1.8.1")
-
- implementation project(":privacysandbox:ui:ui-core")
- implementation("androidx.core:core:1.12.0")
- implementation(libs.kotlinCoroutinesCore)
+ api("androidx.core:core:1.12.0")
+ api project(":privacysandbox:ui:ui-core")
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
diff --git a/privacysandbox/ui/ui-tests/build.gradle b/privacysandbox/ui/ui-tests/build.gradle
index a51f5f0..8fa1016 100644
--- a/privacysandbox/ui/ui-tests/build.gradle
+++ b/privacysandbox/ui/ui-tests/build.gradle
@@ -30,10 +30,9 @@
}
dependencies {
- implementation project(":privacysandbox:ui:ui-core")
+ implementation(libs.kotlinStdlib)
implementation project(":privacysandbox:ui:ui-client")
implementation project(":privacysandbox:ui:ui-provider")
- implementation(libs.kotlinStdlib)
implementation("androidx.recyclerview:recyclerview:1.3.2")
androidTestImplementation(project(":internal-testutils-runtime"))
@@ -45,9 +44,9 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
- androidTestImplementation project(":appcompat:appcompat")
- androidTestImplementation project(":recyclerview:recyclerview")
- androidTestImplementation project(":privacysandbox:ui:integration-tests:testingutils")
+ androidTestImplementation(project(":appcompat:appcompat"))
+ androidTestImplementation(project(":recyclerview:recyclerview"))
+ androidTestImplementation(project(":privacysandbox:ui:integration-tests:testingutils"))
}
android {
diff --git a/profileinstaller/profileinstaller-benchmark/build.gradle b/profileinstaller/profileinstaller-benchmark/build.gradle
index 7dfe226..78d904e 100644
--- a/profileinstaller/profileinstaller-benchmark/build.gradle
+++ b/profileinstaller/profileinstaller-benchmark/build.gradle
@@ -40,7 +40,7 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation project(":internal-testutils-runtime")
+ androidTestImplementation(project(":internal-testutils-runtime"))
androidTestImplementation(libs.kotlinStdlib)
}
diff --git a/recyclerview/recyclerview/build.gradle b/recyclerview/recyclerview/build.gradle
index ba5cd36..0b5fd9d 100644
--- a/recyclerview/recyclerview/build.gradle
+++ b/recyclerview/recyclerview/build.gradle
@@ -47,14 +47,9 @@
}
android {
- sourceSets {
- main.res.srcDirs "res", "res-public"
- }
-
buildTypes.configureEach {
consumerProguardFiles("proguard-rules.pro")
}
-
defaultConfig {
compileSdk = 35
testInstrumentationRunner "androidx.recyclerview.test.TestRunner"
diff --git a/recyclerview/recyclerview/res/values/attrs.xml b/recyclerview/recyclerview/src/main/res/values/attrs.xml
similarity index 100%
rename from recyclerview/recyclerview/res/values/attrs.xml
rename to recyclerview/recyclerview/src/main/res/values/attrs.xml
diff --git a/recyclerview/recyclerview/res/values/dimens.xml b/recyclerview/recyclerview/src/main/res/values/dimens.xml
similarity index 100%
rename from recyclerview/recyclerview/res/values/dimens.xml
rename to recyclerview/recyclerview/src/main/res/values/dimens.xml
diff --git a/recyclerview/recyclerview/res/values/ids.xml b/recyclerview/recyclerview/src/main/res/values/ids.xml
similarity index 100%
rename from recyclerview/recyclerview/res/values/ids.xml
rename to recyclerview/recyclerview/src/main/res/values/ids.xml
diff --git a/recyclerview/recyclerview/res-public/values/public_attrs.xml b/recyclerview/recyclerview/src/main/res/values/public_attrs.xml
similarity index 100%
rename from recyclerview/recyclerview/res-public/values/public_attrs.xml
rename to recyclerview/recyclerview/src/main/res/values/public_attrs.xml
diff --git a/room/benchmark/build.gradle b/room/benchmark/build.gradle
index a65a1a2..8613005 100644
--- a/room/benchmark/build.gradle
+++ b/room/benchmark/build.gradle
@@ -34,7 +34,7 @@
dependencies {
androidTestImplementation(project(":room:room-common"))
androidTestImplementation(project(":room:room-runtime"))
- kspAndroidTest project(":room:room-compiler")
+ kspAndroidTest(project(":room:room-compiler"))
androidTestImplementation(project(":room:room-rxjava2"))
androidTestImplementation("androidx.arch.core:core-runtime:2.2.0")
androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
diff --git a/room/integration-tests/autovaluetestapp/build.gradle b/room/integration-tests/autovaluetestapp/build.gradle
index 04f09c0..2e220c6 100644
--- a/room/integration-tests/autovaluetestapp/build.gradle
+++ b/room/integration-tests/autovaluetestapp/build.gradle
@@ -24,7 +24,7 @@
implementation(project(":room:room-runtime"))
implementation("androidx.arch.core:core-runtime:2.2.0")
- androidTestAnnotationProcessor project(":room:room-compiler")
+ androidTestAnnotationProcessor(project(":room:room-compiler"))
androidTestAnnotationProcessor(libs.autoValue)
androidTestAnnotationProcessor(libs.autoValueParcel)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/FlowQueryTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/FlowQueryTest.kt
index d067381..86bd96e 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/FlowQueryTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/FlowQueryTest.kt
@@ -16,29 +16,39 @@
package androidx.room.integration.kotlintestapp.test
+import android.content.Context
import androidx.kruth.assertThat
+import androidx.room.ExperimentalRoomApi
+import androidx.room.Room
+import androidx.room.integration.kotlintestapp.TestDatabase
import androidx.room.integration.kotlintestapp.vo.Book
import androidx.room.integration.kotlintestapp.vo.Playlist
import androidx.room.integration.kotlintestapp.vo.PlaylistSongXRef
import androidx.room.integration.kotlintestapp.vo.PlaylistWithSongs
import androidx.room.integration.kotlintestapp.vo.Song
import androidx.room.withTransaction
+import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
+import kotlin.coroutines.cancellation.CancellationException
import kotlin.test.Ignore
+import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.produceIn
import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.yield
import org.junit.After
import org.junit.Assert.fail
@@ -387,4 +397,46 @@
job.cancelAndJoin()
}
+
+ @Test
+ fun collectBooks_autoClose(): Unit = runBlocking {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ context.deleteDatabase("auto-close-test.db")
+
+ @OptIn(ExperimentalRoomApi::class)
+ val database =
+ Room.databaseBuilder<TestDatabase>(context, "auto-close-test.db")
+ .setAutoCloseTimeout(1, TimeUnit.SECONDS)
+ .build()
+
+ database.booksDao().insertPublisher(TestUtil.PUBLISHER.publisherId, TestUtil.PUBLISHER.name)
+
+ val collectJob = launch {
+ var collections = 0
+ database.booksDao().getBooksFlow().collect {
+ when (collections) {
+ 0 -> {
+ assertThat(it).isEmpty()
+ }
+ 1 -> {
+ assertThat(it).containsExactly(TestUtil.BOOK_1)
+ throw CancellationException()
+ }
+ else -> {
+ fail("Received too many Flow.collect")
+ }
+ }
+ collections++
+ }
+ }
+
+ val insertJob = launch {
+ delay(2.seconds)
+ database.booksDao().insertBookSuspend(TestUtil.BOOK_1)
+ }
+
+ withTimeout(TimeUnit.SECONDS.toMillis(3)) { listOf(collectJob, insertJob).joinAll() }
+
+ database.close()
+ }
}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java
index fcb0a14..83214d1 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java
@@ -271,6 +271,9 @@
@Query("SELECT * FROM user where mAge > :age")
public abstract DataSource.Factory<Integer, User> loadPagedByAge(int age);
+ @Query("SELECT * FROM user where mLastName = :name")
+ public abstract DataSource.Factory<Integer, User> loadPagedByLastname(String name);
+
@RawQuery(observedEntities = User.class)
public abstract DataSource.Factory<Integer, User> loadPagedByAgeWithObserver(
SupportSQLiteQuery query);
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt
index 4fe0f34..513fd43 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt
@@ -21,26 +21,39 @@
import com.google.auto.common.MoreTypes
import com.squareup.javapoet.ParameterizedTypeName
import com.squareup.javapoet.TypeName
+import javax.lang.model.element.AnnotationMirror
import javax.lang.model.element.Element
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier
import javax.lang.model.element.TypeElement
import javax.lang.model.type.ExecutableType
+import javax.lang.model.type.TypeMirror
import javax.lang.model.util.Types
import kotlin.coroutines.Continuation
private val NONNULL_ANNOTATIONS =
- arrayOf("androidx.annotation.NonNull", "org.jetbrains.annotations.NotNull")
+ arrayOf(
+ "androidx.annotation.NonNull",
+ "org.jetbrains.annotations.NotNull",
+ "org.jspecify.annotations.NonNull"
+ )
private val NULLABLE_ANNOTATIONS =
- arrayOf("androidx.annotation.Nullable", "org.jetbrains.annotations.Nullable")
+ arrayOf(
+ "androidx.annotation.Nullable",
+ "org.jetbrains.annotations.Nullable",
+ "org.jspecify.annotations.Nullable"
+ )
+
+/** Checks if any of the [annotations] are present on the [Element] or its [type]. */
+private fun Element.hasAnyOf(annotations: Array<String>, type: TypeMirror) =
+ annotationMirrors.hasAnyOf(annotations) || type.annotationMirrors.hasAnyOf(annotations)
@Suppress("UnstableApiUsage")
-private fun Element.hasAnyOf(annotations: Array<String>) =
- annotationMirrors.any { annotationMirror ->
- val annotationTypeElement = MoreElements.asType(annotationMirror.annotationType.asElement())
- annotations.any { annotationTypeElement.qualifiedName.contentEquals(it) }
- }
+private fun List<AnnotationMirror>.hasAnyOf(annotations: Array<String>) = any { annotationMirror ->
+ val annotationTypeElement = MoreElements.asType(annotationMirror.annotationType.asElement())
+ annotations.any { annotationTypeElement.qualifiedName.contentEquals(it) }
+}
internal val Element.nullability: XNullability
get() {
@@ -53,9 +66,9 @@
else -> it
}
}
- return if (asType.kind.isPrimitive || hasAnyOf(NONNULL_ANNOTATIONS)) {
+ return if (asType.kind.isPrimitive || hasAnyOf(NONNULL_ANNOTATIONS, asType)) {
XNullability.NONNULL
- } else if (hasAnyOf(NULLABLE_ANNOTATIONS)) {
+ } else if (hasAnyOf(NULLABLE_ANNOTATIONS, asType)) {
XNullability.NULLABLE
} else {
XNullability.UNKNOWN
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
index 17ba896..3f1da51 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
@@ -412,6 +412,69 @@
}
}
+ @Test
+ fun nullability_from_jspecify() {
+ val src =
+ Source.java(
+ "foo.bar.Foo",
+ """
+ package foo.bar;
+ import org.jspecify.annotations.NonNull;
+ import org.jspecify.annotations.Nullable;
+ class Foo {
+ public @NonNull String nonNullField = "";
+ public @NonNull String returnsNonNull() {
+ return "";
+ }
+ public String @Nullable [] returnsNullableArray() {
+ return null;
+ }
+ public void hasNullableParam(@Nullable String param) {}
+ }
+ """
+ .trimIndent()
+ )
+ val nonNullSrc =
+ Source.java(
+ "NonNull",
+ """
+ package org.jspecify.annotations;
+ import java.lang.annotation.ElementType;
+ import java.lang.annotation.Target;
+ @Target(ElementType.TYPE_USE)
+ public @interface NonNull {}
+ """
+ .trimIndent()
+ )
+ val nullableSrc =
+ Source.java(
+ "Nullable",
+ """
+ package org.jspecify.annotations;
+ import java.lang.annotation.ElementType;
+ import java.lang.annotation.Target;
+ @Target(ElementType.TYPE_USE)
+ public @interface Nullable {}
+ """
+ .trimIndent()
+ )
+ runProcessorTestWithoutKsp(sources = listOf(src, nonNullSrc, nullableSrc)) { invocation ->
+ val element = invocation.processingEnv.requireTypeElement("foo.bar.Foo")
+ element.getField("nonNullField").let { field ->
+ assertThat(field.type.nullability).isEqualTo(NONNULL)
+ }
+ element.getMethodByJvmName("returnsNonNull").let { method ->
+ assertThat(method.returnType.nullability).isEqualTo(NONNULL)
+ }
+ element.getMethodByJvmName("returnsNullableArray").let { method ->
+ assertThat(method.returnType.nullability).isEqualTo(NULLABLE)
+ }
+ element.getMethodByJvmName("hasNullableParam").getParameter("param").let { param ->
+ assertThat(param.type.nullability).isEqualTo(NULLABLE)
+ }
+ }
+ }
+
companion object {
val PRIMITIVE_JTYPE_NAMES =
listOf(
diff --git a/room/room-runtime/api/restricted_current.txt b/room/room-runtime/api/restricted_current.txt
index df44066..bb0324f 100644
--- a/room/room-runtime/api/restricted_current.txt
+++ b/room/room-runtime/api/restricted_current.txt
@@ -377,6 +377,7 @@
method public void bindLong(int index, long value);
method public void bindNull(int index);
method public void bindString(int index, String value);
+ method public void bindText(int index, String value);
method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram statement);
method public void bindTo(androidx.sqlite.SQLiteStatement statement);
method public void clearBindings();
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomSQLiteQuery.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomSQLiteQuery.android.kt
index a423a69..e88a1cd 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomSQLiteQuery.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomSQLiteQuery.android.kt
@@ -129,6 +129,8 @@
stringBindings[index] = value
}
+ fun bindText(index: Int, value: String) = bindString(index, value)
+
override fun bindBlob(index: Int, value: ByteArray) {
bindingTypes[index] = BLOB
blobBindings[index] = value
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteConnectionPool.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteConnectionPool.android.kt
index 194d4047..f795a9d 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteConnectionPool.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteConnectionPool.android.kt
@@ -30,10 +30,10 @@
*/
internal class SupportSQLiteConnectionPool(internal val supportDriver: SupportSQLiteDriver) :
ConnectionPool {
- private val supportConnection by
- lazy(LazyThreadSafetyMode.PUBLICATION) {
+ private val supportConnection: SupportSQLitePooledConnection
+ get() {
val fileName = supportDriver.openHelper.databaseName ?: ":memory:"
- SupportSQLitePooledConnection(supportDriver.open(fileName))
+ return SupportSQLitePooledConnection(supportDriver.open(fileName))
}
override suspend fun <R> useConnection(
diff --git a/security/security-state-provider/build.gradle b/security/security-state-provider/build.gradle
index 2524d32..e941c91 100644
--- a/security/security-state-provider/build.gradle
+++ b/security/security-state-provider/build.gradle
@@ -32,7 +32,7 @@
dependencies {
implementation(libs.kotlinSerializationJson)
- implementation project(":security:security-state")
+ implementation(project(":security:security-state"))
testImplementation(libs.testExtJunit)
testImplementation(libs.mockitoKotlin4)
diff --git a/settings.gradle b/settings.gradle
index 7bdea24..4d3a6fa 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -188,13 +188,35 @@
return null
}
-private String getRequestedProjectPrefix() {
- def envProp = providers.environmentVariable("PROJECT_PREFIX")
- if (envProp.isPresent()) {
- return envProp.get()
+/**
+ * Utility class to handle PROJECT_PREFIX environment variable.
+ */
+class ProjectPrefixFilter {
+ // list of projects parsed from the PROJECT_PREFIX env environmentVariable
+ private final List<String> projectPrefixes
+ // set to true if the environment variable is present
+ final boolean isConfigured
+ ProjectPrefixFilter(providers) {
+ def envProp = providers.environmentVariable("PROJECT_PREFIX")
+ if (envProp.isPresent()) {
+ isConfigured = true
+ def value = envProp.get()
+ projectPrefixes = value?.split(',')?.collect { it.trim() } ?: []
+ } else {
+ isConfigured = false
+ projectPrefixes = []
+ }
}
- return null
+
+
+ boolean matches(String name) {
+ return projectPrefixes.any { prefix ->
+ name.startsWith(prefix)
+ }
+ }
}
+ext.projectPrefixFilter = new ProjectPrefixFilter(providers)
+
boolean isAllProjects() {
return requestedProjectSubsetName == null || requestedProjectSubsetName == "ALL"
@@ -313,9 +335,13 @@
// the Maven artifactId
//
def includeProject(String name, filePath, List<BuildType> filter = []) {
- if (getRequestedProjectPrefix() != null) {
- if (name.startsWith(getRequestedProjectPrefix())) filteredProjects.add(name)
- } else if (shouldIncludeForFilter(filter)) filteredProjects.add(name)
+ if (projectPrefixFilter.isConfigured) {
+ if (projectPrefixFilter.matches(name)) {
+ filteredProjects.add(name)
+ }
+ } else if (shouldIncludeForFilter(filter)) {
+ filteredProjects.add(name)
+ }
def file
if (filePath != null) {
if (filePath instanceof String) {
@@ -395,6 +421,7 @@
includeProject(":benchmark:benchmark-macro", [BuildType.MAIN, BuildType.COMPOSE])
includeProject(":benchmark:benchmark-macro-junit4", [BuildType.MAIN, BuildType.COMPOSE])
includeProject(":benchmark:benchmark-samples", "benchmark/samples", [BuildType.MAIN, BuildType.COMPOSE])
+includeProject(":benchmark:benchmark-traceprocessor", [BuildType.MAIN, BuildType.COMPOSE])
includeProject(":benchmark:integration-tests:baselineprofile-producer", [BuildType.MAIN])
includeProject(":benchmark:integration-tests:baselineprofile-consumer", [BuildType.MAIN])
includeProject(":benchmark:integration-tests:baselineprofile-flavors-producer", [BuildType.MAIN])
@@ -428,7 +455,6 @@
includeProject(":camera:camera-compose:camera-compose-samples", "camera/camera-compose/samples", [BuildType.CAMERA])
includeProject(":camera:camera-core", [BuildType.CAMERA])
includeProject(":camera:camera-effects", [BuildType.CAMERA])
-includeProject(":camera:camera-effects-still-portrait", [BuildType.CAMERA])
includeProject(":camera:camera-extensions", [BuildType.CAMERA])
includeProject(":camera:camera-extensions-stub", [BuildType.CAMERA])
includeProject(":camera:camera-feature-combination-query", [BuildType.CAMERA])
@@ -650,7 +676,6 @@
includeProject(":credentials:credentials-samples", "credentials/credentials/samples", [BuildType.MAIN])
includeProject(":credentials:credentials-fido", [BuildType.MAIN])
includeProject(":credentials:credentials-play-services-auth", [BuildType.MAIN])
-includeProject(":credentials:credentials-provider", [BuildType.MAIN])
includeProject(":credentials:credentials-e2ee", [BuildType.MAIN])
includeProject(":credentials:credentials-play-services-e2ee", [BuildType.MAIN])
includeProject(":credentials:registry:registry-digitalcredentials-mdoc", [BuildType.MAIN])
@@ -845,7 +870,6 @@
includeProject(":navigation:navigation-runtime", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE])
includeProject(":navigation:navigation-runtime-lint", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE])
includeProject(":navigation:navigation-runtime-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE])
-includeProject(":navigation:navigation-runtime-truth", [BuildType.MAIN, BuildType.FLAN])
includeProject(":navigation:navigation-safe-args-generator", [BuildType.MAIN, BuildType.FLAN])
includeProject(":navigation:navigation-safe-args-gradle-plugin", [BuildType.MAIN, BuildType.FLAN])
includeProject(":navigation:navigation-testing", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE])
@@ -886,6 +910,7 @@
includeProject(":privacysandbox:ads:ads-adservices-java", [BuildType.MAIN])
includeProject(":privacysandbox:plugins:plugins-privacysandbox-library", [BuildType.MAIN])
includeProject(":privacysandbox:sdkruntime:integration-tests:testaidl", [BuildType.MAIN])
+includeProject(":privacysandbox:sdkruntime:integration-tests:macrobenchmark", [BuildType.MAIN])
includeProject(":privacysandbox:sdkruntime:integration-tests:testapp", [BuildType.MAIN])
includeProject(":privacysandbox:sdkruntime:integration-tests:testsdk", [BuildType.MAIN])
includeProject(":privacysandbox:sdkruntime:integration-tests:testsdk-asb", [BuildType.MAIN])
@@ -1129,7 +1154,6 @@
includeProject(":window:window-testing", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.WINDOW])
includeProject(":work:integration-tests:testapp", [BuildType.MAIN])
includeProject(":work:work-benchmark", [BuildType.MAIN])
-includeProject(":work:work-datatransfer", [BuildType.MAIN])
includeProject(":work:work-gcm", [BuildType.MAIN])
includeProject(":work:work-inspection", [BuildType.MAIN])
includeProject(":work:work-multiprocess", [BuildType.MAIN])
@@ -1139,18 +1163,17 @@
includeProject(":work:work-rxjava2", [BuildType.MAIN])
includeProject(":work:work-rxjava3", [BuildType.MAIN])
includeProject(":work:work-testing", [BuildType.MAIN])
-
-/////////////////////////////
-//
-// Optional projects
-//
-/////////////////////////////
-
-File repoRoot = new File(rootDir, "../..").canonicalFile
-
-includeOptionalProject(":xr:xr", new File(repoRoot, "xr/xr"), [BuildType.XR])
-includeOptionalProject(":xr:integration-tests:compose-adaptive-sample", new File(repoRoot, "xr/integration-tests/compose-adaptive-sample"), [BuildType.XR])
-includeOptionalProject(":xr:xr-material3-adaptive", new File(repoRoot, "xr/xr-material3-adaptive"), [BuildType.XR])
+includeProject(":xr:arcore:arcore", [BuildType.XR])
+includeProject(":xr:compose:compose", [BuildType.XR])
+includeProject(":xr:compose:compose-testing", [BuildType.XR])
+includeProject(":xr:compose:material3:material3", [BuildType.XR])
+includeProject(":xr:compose:material3:integration-tests:testapp", [BuildType.XR])
+includeProject(":xr:runtime:runtime", [BuildType.XR])
+includeProject(":xr:runtime:runtime-openxr", [BuildType.XR])
+includeProject(":xr:runtime:runtime-testing", [BuildType.XR])
+includeProject(":xr:scenecore:scenecore", [BuildType.XR])
+includeProject(":xr:scenecore:scenecore-testing", [BuildType.XR])
+includeProject(":xr:xr-stubs", [BuildType.XR])
/////////////////////////////
//
diff --git a/slice/slice-remotecallback/build.gradle b/slice/slice-remotecallback/build.gradle
index 6e12825..02cb09a 100644
--- a/slice/slice-remotecallback/build.gradle
+++ b/slice/slice-remotecallback/build.gradle
@@ -40,7 +40,7 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.dexmakerMockito)
- androidTestAnnotationProcessor project(":remotecallback:remotecallback-processor")
+ androidTestAnnotationProcessor(project(":remotecallback:remotecallback-processor"))
}
androidx {
diff --git a/sqlite/sqlite-bundled/build.gradle b/sqlite/sqlite-bundled/build.gradle
index 9fbba67..ae6ed6b 100644
--- a/sqlite/sqlite-bundled/build.gradle
+++ b/sqlite/sqlite-bundled/build.gradle
@@ -249,5 +249,4 @@
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2023"
description = "The implementation of SQLite library using the bundled SQLite."
- metalavaK2UastEnabled = false
}
diff --git a/test/uiautomator/integration-tests/testapp/build.gradle b/test/uiautomator/integration-tests/testapp/build.gradle
index 3f4be03..27a3777 100644
--- a/test/uiautomator/integration-tests/testapp/build.gradle
+++ b/test/uiautomator/integration-tests/testapp/build.gradle
@@ -27,16 +27,16 @@
implementation(libs.androidx.annotation)
implementation("androidx.core:core:1.6.0")
- implementation project(":activity:activity")
+ implementation(project(":activity:activity"))
implementation(project(":activity:activity-compose"))
- implementation project(":compose:foundation:foundation")
+ implementation(project(":compose:foundation:foundation"))
implementation(project(":compose:foundation:foundation-layout"))
implementation(project(":compose:material:material"))
implementation(project(":compose:runtime:runtime"))
implementation(project(":compose:ui:ui"))
- implementation project(":compose:ui:ui-graphics")
- implementation project(":compose:ui:ui-text")
- implementation project(":compose:ui:ui-unit")
+ implementation(project(":compose:ui:ui-graphics"))
+ implementation(project(":compose:ui:ui-text"))
+ implementation(project(":compose:ui:ui-unit"))
androidTestImplementation(project(":test:uiautomator:uiautomator"))
androidTestImplementation(libs.testCore)
diff --git a/tv/tv-foundation/build.gradle b/tv/tv-foundation/build.gradle
deleted file mode 100644
index 871d662..0000000
--- a/tv/tv-foundation/build.gradle
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2022 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
- *
- * http://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.
- */
-
-/**
- * This file was created using the `create_project.py` script located in the
- * `<AndroidX root>/development/project-creator` directory.
- *
- * Please use that script when creating a new project, rather than copying an existing project and
- * modifying its settings.
- */
-import androidx.build.LibraryType
-
-plugins {
- id("AndroidXPlugin")
- id("AndroidXComposePlugin")
- id("com.android.library")
- id("org.jetbrains.kotlin.android")
-}
-
-dependencies {
- api(libs.kotlinStdlib)
-
- def composeVersion = "1.6.8"
-
- api("androidx.annotation:annotation:1.8.1")
- api("androidx.compose.animation:animation:$composeVersion")
- api("androidx.compose.foundation:foundation:$composeVersion")
- api("androidx.compose.foundation:foundation-layout:$composeVersion")
- api("androidx.compose.runtime:runtime:$composeVersion")
- api("androidx.compose.ui:ui-util:$composeVersion")
- api("androidx.compose.ui:ui:$composeVersion")
- api("androidx.compose.ui:ui-graphics:$composeVersion")
- api("androidx.compose.ui:ui-text:$composeVersion")
-
- implementation("androidx.profileinstaller:profileinstaller:1.4.0")
-
- androidTestImplementation(libs.truth)
- androidTestImplementation(project(":compose:runtime:runtime"))
- androidTestImplementation(project(":compose:ui:ui-test"))
- androidTestImplementation(project(":compose:ui:ui-test-junit4"))
- androidTestImplementation(project(":compose:test-utils"))
- androidTestImplementation(libs.testRunner)
-}
-
-android {
- compileSdk = 35
- namespace = "androidx.tv.foundation"
-}
-
-androidx {
- name = "TV Foundation"
- type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
- mavenVersion = LibraryVersions.TV
- inceptionYear = "2022"
- description = "This library makes it easier for developers" +
- "to write Jetpack Compose applications for TV devices by providing " +
- "functionality to support TV specific devices sizes, shapes and d-pad navigation " +
- "supported components. It builds upon the Jetpack Compose libraries."
- legacyDisableKotlinStrictApiMode = true
- metalavaK2UastEnabled = false
-}
diff --git a/tv/tv-foundation/build.gradle.kts b/tv/tv-foundation/build.gradle.kts
new file mode 100644
index 0000000..b9bfdd7
--- /dev/null
+++ b/tv/tv-foundation/build.gradle.kts
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2022 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
+ *
+ * http://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.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("AndroidXComposePlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ api("androidx.annotation:annotation:1.8.1")
+
+ val composeVersion = "1.6.8"
+ api("androidx.compose.animation:animation:$composeVersion")
+ api("androidx.compose.foundation:foundation:$composeVersion")
+ api("androidx.compose.foundation:foundation-layout:$composeVersion")
+ api("androidx.compose.runtime:runtime:$composeVersion")
+ api("androidx.compose.ui:ui-util:$composeVersion")
+ api("androidx.compose.ui:ui:$composeVersion")
+ api("androidx.compose.ui:ui-graphics:$composeVersion")
+ api("androidx.compose.ui:ui-text:$composeVersion")
+
+ implementation("androidx.profileinstaller:profileinstaller:1.4.0")
+
+ androidTestImplementation(libs.truth)
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:ui:ui-test"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:test-utils"))
+ androidTestImplementation(libs.testRunner)
+}
+
+android {
+ compileSdk = 35
+ namespace = "androidx.tv.foundation"
+}
+
+androidx {
+ name = "TV Foundation"
+ type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
+ mavenVersion = LibraryVersions["TV"]
+ inceptionYear = "2022"
+ description = "This library makes it easier for developers" +
+ "to write Jetpack Compose applications for TV devices by providing " +
+ "functionality to support TV specific devices sizes, shapes and d-pad navigation " +
+ "supported components. It builds upon the Jetpack Compose libraries."
+ legacyDisableKotlinStrictApiMode = true
+}
diff --git a/tv/tv-material/build.gradle b/tv/tv-material/build.gradle
deleted file mode 100644
index a4d04af..0000000
--- a/tv/tv-material/build.gradle
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2022 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
- *
- * http://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.
- */
-
-/**
- * This file was created using the `create_project.py` script located in the
- * `<AndroidX root>/development/project-creator` directory.
- *
- * Please use that script when creating a new project, rather than copying an existing project and
- * modifying its settings.
- */
-import androidx.build.LibraryType
-
-plugins {
- id("AndroidXPlugin")
- id("AndroidXComposePlugin")
- id("com.android.library")
- id("org.jetbrains.kotlin.android")
-}
-
-dependencies {
- api(libs.kotlinStdlib)
-
- def annotationVersion = "1.8.0"
- def composeVersion = "1.6.8"
- def profileInstallerVersion = "1.4.0"
-
- api("androidx.annotation:annotation:1.8.1")
- api("androidx.compose.animation:animation:$composeVersion")
- api("androidx.compose.foundation:foundation:$composeVersion")
- api("androidx.compose.foundation:foundation-layout:$composeVersion")
- api("androidx.compose.runtime:runtime:$composeVersion")
- api("androidx.compose.material:material-icons-core:$composeVersion")
- api("androidx.compose.ui:ui-util:$composeVersion")
- api("androidx.compose.ui:ui:$composeVersion")
- api("androidx.compose.ui:ui-graphics:$composeVersion")
- api("androidx.compose.ui:ui-text:$composeVersion")
-
- implementation("androidx.profileinstaller:profileinstaller:1.4.0")
-
- androidTestImplementation(libs.truth)
- androidTestImplementation(project(":compose:runtime:runtime"))
- androidTestImplementation(project(":compose:ui:ui-test"))
- androidTestImplementation(project(":compose:ui:ui-test-junit4"))
- androidTestImplementation(project(":compose:test-utils"))
- androidTestImplementation(project(":test:screenshot:screenshot"))
- androidTestImplementation(project(":tv:tv-foundation"))
- androidTestImplementation(libs.testRunner)
-}
-
-android {
- compileSdk = 35
- namespace = "androidx.tv.material"
- sourceSets.androidTest.assets.srcDirs +=
- project.rootDir.absolutePath + "/../../golden/tv/compose/material3"
-}
-
-androidx {
- name = "TV Material"
- type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
- mavenVersion = LibraryVersions.TV_MATERIAL
- inceptionYear = "2022"
- description = "build TV applications using controls that adhere to Material Design Language."
- legacyDisableKotlinStrictApiMode = true
- metalavaK2UastEnabled = false
- samples(project(":tv:tv-material-samples"))
-}
diff --git a/tv/tv-material/build.gradle.kts b/tv/tv-material/build.gradle.kts
new file mode 100644
index 0000000..78a44d3
--- /dev/null
+++ b/tv/tv-material/build.gradle.kts
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2022 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
+ *
+ * http://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.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("AndroidXComposePlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+
+ api("androidx.annotation:annotation:1.8.1")
+
+ val composeVersion = "1.6.8"
+ api("androidx.compose.animation:animation:$composeVersion")
+ api("androidx.compose.foundation:foundation:$composeVersion")
+ api("androidx.compose.foundation:foundation-layout:$composeVersion")
+ api("androidx.compose.runtime:runtime:$composeVersion")
+ api("androidx.compose.material:material-icons-core:$composeVersion")
+ api("androidx.compose.ui:ui-util:$composeVersion")
+ api("androidx.compose.ui:ui:$composeVersion")
+ api("androidx.compose.ui:ui-graphics:$composeVersion")
+ api("androidx.compose.ui:ui-text:$composeVersion")
+
+ implementation("androidx.profileinstaller:profileinstaller:1.4.0")
+
+ androidTestImplementation(libs.truth)
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:ui:ui-test"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:test-utils"))
+ androidTestImplementation(project(":test:screenshot:screenshot"))
+ androidTestImplementation(project(":tv:tv-foundation"))
+ androidTestImplementation(libs.testRunner)
+}
+
+android {
+ compileSdk = 35
+ namespace = "androidx.tv.material"
+}
+
+androidx {
+ name = "TV Material"
+ type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
+ mavenVersion = LibraryVersions["TV_MATERIAL"]
+ inceptionYear = "2022"
+ description = "build TV applications using controls that adhere to Material Design Language."
+ legacyDisableKotlinStrictApiMode = true
+ metalavaK2UastEnabled = false
+ samples(project(":tv:tv-material-samples"))
+ addGoldenImageAssets()
+}
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/GoldenCommon.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/GoldenCommon.kt
index ebb26c0..4ffce1d 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/GoldenCommon.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/GoldenCommon.kt
@@ -16,4 +16,4 @@
package androidx.tv.material3
-internal const val TV_GOLDEN_MATERIAL3 = "tv/compose/material3"
+internal const val TV_GOLDEN_MATERIAL3 = "tv/tv-material"
diff --git a/versionedparcelable/versionedparcelable/build.gradle b/versionedparcelable/versionedparcelable/build.gradle
index 83adae3..5ad222b 100644
--- a/versionedparcelable/versionedparcelable/build.gradle
+++ b/versionedparcelable/versionedparcelable/build.gradle
@@ -40,7 +40,7 @@
androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
- androidTestAnnotationProcessor project(":versionedparcelable:versionedparcelable-compiler")
+ androidTestAnnotationProcessor(project(":versionedparcelable:versionedparcelable-compiler"))
}
android {
diff --git a/viewpager/viewpager/build.gradle b/viewpager/viewpager/build.gradle
index a9e7a6b..cbfe218 100644
--- a/viewpager/viewpager/build.gradle
+++ b/viewpager/viewpager/build.gradle
@@ -24,7 +24,7 @@
androidTestImplementation(libs.espressoCore)
androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation project(":internal-testutils-espresso")
+ androidTestImplementation(project(":internal-testutils-espresso"))
}
androidx {
diff --git a/viewpager2/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java b/viewpager2/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java
index f2a17f1..5cc3b59 100644
--- a/viewpager2/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java
+++ b/viewpager2/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java
@@ -175,7 +175,6 @@
}
@RequiresApi(21)
- @SuppressLint("ClassVerificationFailure")
public ViewPager2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
@@ -390,7 +389,6 @@
Parcelable mAdapterState;
@RequiresApi(24)
- @SuppressLint("ClassVerificationFailure")
SavedState(Parcel source, ClassLoader loader) {
super(source, loader);
readValues(source, loader);
diff --git a/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle b/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
index 7db4232..4acae42 100644
--- a/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
+++ b/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
@@ -39,7 +39,7 @@
dependencies {
implementation(libs.kotlinStdlib)
implementation(libs.constraintLayout)
- implementation project(":activity:activity-ktx")
+ implementation(project(":activity:activity-ktx"))
implementation("androidx.core:core-ktx")
implementation(libs.material)
implementation(project(":profileinstaller:profileinstaller"))
diff --git a/wear/compose/compose-foundation/benchmark/build.gradle b/wear/compose/compose-foundation/benchmark/build.gradle
index 2cdab7f..b69e6d1 100644
--- a/wear/compose/compose-foundation/benchmark/build.gradle
+++ b/wear/compose/compose-foundation/benchmark/build.gradle
@@ -45,13 +45,13 @@
dependencies {
- androidTestImplementation project(":benchmark:benchmark-junit4")
- androidTestImplementation project(":compose:runtime:runtime")
- androidTestImplementation project(":compose:ui:ui-text:ui-text-benchmark")
- androidTestImplementation project(":compose:foundation:foundation")
- androidTestImplementation project(":compose:runtime:runtime")
- androidTestImplementation project(":compose:benchmark-utils")
- androidTestImplementation project(":wear:compose:compose-foundation")
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:ui:ui-text:ui-text-benchmark"))
+ androidTestImplementation(project(":compose:foundation:foundation"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:benchmark-utils"))
+ androidTestImplementation(project(":wear:compose:compose-foundation"))
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index a3c1ff9..104ffff 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -78,9 +78,6 @@
}
// Use Robolectric 4.+
testOptions.unitTests.includeAndroidResources = true
- sourceSets.androidTest.assets.srcDirs +=
- project.rootDir.absolutePath + "/../../golden/wear/compose/foundation"
-
buildTypes.configureEach {
consumerProguardFiles("proguard-rules.pro")
}
@@ -98,4 +95,5 @@
samples(project(":wear:compose:compose-foundation-samples"))
legacyDisableKotlinStrictApiMode = true
metalavaK2UastEnabled = false
+ addGoldenImageAssets()
}
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/CurvedScreenshotTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/CurvedScreenshotTest.kt
index 3360d61..2f7c7b2 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/CurvedScreenshotTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/CurvedScreenshotTest.kt
@@ -309,4 +309,4 @@
}
}
-internal const val SCREENSHOT_GOLDEN_PATH = "wear/compose/foundation"
+internal const val SCREENSHOT_GOLDEN_PATH = "wear/compose/compose-foundation"
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/Haptics.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/Haptics.kt
index 28bce2c..96cac7a 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/Haptics.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/Haptics.kt
@@ -62,7 +62,7 @@
hapticsEnabled: Boolean
): RotaryHapticHandler =
if (hapticsEnabled) {
- if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.VANILLA_ICE_CREAM)
rememberCustomRotaryHapticHandler(scrollableState)
else rememberPlatformRotaryHapticHandler(scrollableState)
} else rememberDisabledRotaryHapticHandler()
@@ -171,7 +171,7 @@
* @param hapticsChannel Channel to which haptic events will be sent
* @param hapticsThresholdPx A scroll threshold after which haptic is produced.
*/
-private class CustomRotaryHapticHandler(
+internal class CustomRotaryHapticHandler(
private val scrollableState: ScrollableState,
private val hapticsChannel: Channel<RotaryHapticsType>,
private val hapticsThresholdPx: Long = 50
diff --git a/wear/compose/compose-foundation/src/test/kotlin/androidx/wear/compose/foundation/rotary/HapticsTest.kt b/wear/compose/compose-foundation/src/test/kotlin/androidx/wear/compose/foundation/rotary/HapticsTest.kt
index ed52c17..2700d9d 100644
--- a/wear/compose/compose-foundation/src/test/kotlin/androidx/wear/compose/foundation/rotary/HapticsTest.kt
+++ b/wear/compose/compose-foundation/src/test/kotlin/androidx/wear/compose/foundation/rotary/HapticsTest.kt
@@ -18,8 +18,11 @@
import android.R
import android.app.Activity
+import android.os.Build
import android.provider.Settings
import android.view.View
+import androidx.compose.foundation.gestures.rememberScrollableState
+import androidx.compose.ui.test.junit4.createComposeRule
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
@@ -29,6 +32,7 @@
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -127,6 +131,9 @@
@RunWith(RobolectricTestRunner::class)
class HapticsTest {
+
+ @get:Rule val rule = createComposeRule()
+
@Test
@Config(sdk = [33])
fun testPixelWatch1Wear4() {
@@ -211,6 +218,17 @@
assertEquals(HapticConstants.GalaxyWatchConstants, getHapticConstants())
}
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.VANILLA_ICE_CREAM, Build.VERSION_CODES.UPSIDE_DOWN_CAKE])
+ fun testCustomHapticsHandler() {
+ var rotaryHapticsHandler: RotaryHapticHandler? = null
+ rule.setContent {
+ val scrollableState = rememberScrollableState { 0f }
+ rotaryHapticsHandler = rememberRotaryHapticHandler(scrollableState, true)
+ }
+ assertEquals(rotaryHapticsHandler?.javaClass, CustomRotaryHapticHandler::class.java)
+ }
+
private fun getHapticConstants(): HapticConstants {
val activity = Robolectric.buildActivity(Activity::class.java).get()
val view = activity.findViewById<View>(R.id.content)
diff --git a/wear/compose/compose-material-core/build.gradle b/wear/compose/compose-material-core/build.gradle
index bc7615d..849fbd2 100644
--- a/wear/compose/compose-material-core/build.gradle
+++ b/wear/compose/compose-material-core/build.gradle
@@ -48,8 +48,6 @@
androidTestImplementation(project(":compose:ui:ui-test"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
androidTestImplementation(project(":compose:test-utils"))
-
- androidTestImplementation(project(":test:screenshot:screenshot"))
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.truth)
@@ -66,8 +64,6 @@
}
// Use Robolectric 4.+
testOptions.unitTests.includeAndroidResources = true
- sourceSets.androidTest.assets.srcDirs +=
- project.rootDir.absolutePath + "/../../golden/wear/compose/materialcore"
buildTypes.configureEach {
consumerProguardFiles("proguard-rules.pro")
}
diff --git a/wear/compose/compose-material/benchmark/build.gradle b/wear/compose/compose-material/benchmark/build.gradle
index 77ff797..7d71235 100644
--- a/wear/compose/compose-material/benchmark/build.gradle
+++ b/wear/compose/compose-material/benchmark/build.gradle
@@ -45,15 +45,15 @@
dependencies {
- androidTestImplementation project(":benchmark:benchmark-junit4")
- androidTestImplementation project(":compose:runtime:runtime")
- androidTestImplementation project(":compose:ui:ui-text:ui-text-benchmark")
- androidTestImplementation project(":compose:foundation:foundation")
- androidTestImplementation project(":compose:material:material")
- androidTestImplementation project(":compose:runtime:runtime")
- androidTestImplementation project(":compose:benchmark-utils")
- androidTestImplementation project(":wear:compose:compose-foundation")
- androidTestImplementation project(":wear:compose:compose-material")
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:ui:ui-text:ui-text-benchmark"))
+ androidTestImplementation(project(":compose:foundation:foundation"))
+ androidTestImplementation(project(":compose:material:material"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:benchmark-utils"))
+ androidTestImplementation(project(":wear:compose:compose-foundation"))
+ androidTestImplementation(project(":wear:compose:compose-material"))
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
diff --git a/wear/compose/compose-material/build.gradle b/wear/compose/compose-material/build.gradle
index d09f201..4393637 100644
--- a/wear/compose/compose-material/build.gradle
+++ b/wear/compose/compose-material/build.gradle
@@ -68,15 +68,10 @@
}
// Use Robolectric 4.+
testOptions.unitTests.includeAndroidResources = true
- sourceSets.androidTest.assets.srcDirs +=
- project.rootDir.absolutePath + "/../../golden/wear/compose/material"
buildTypes.configureEach {
consumerProguardFiles("proguard-rules.pro")
}
namespace = "androidx.wear.compose.material"
- lint {
- baseline = file("lint-baseline.xml")
- }
}
androidx {
@@ -90,4 +85,5 @@
legacyDisableKotlinStrictApiMode = true
metalavaK2UastEnabled = false
samples(project(":wear:compose:compose-material-samples"))
+ addGoldenImageAssets()
}
diff --git a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/Screenshot.kt b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/Screenshot.kt
index 53370ef..732c9f6 100644
--- a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/Screenshot.kt
+++ b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/Screenshot.kt
@@ -16,4 +16,4 @@
package androidx.wear.compose.material
-internal const val SCREENSHOT_GOLDEN_PATH = "wear/compose/material"
+internal const val SCREENSHOT_GOLDEN_PATH = "wear/compose/compose-material"
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
index f8be644..fc1325b 100644
--- a/wear/compose/compose-material3/api/current.txt
+++ b/wear/compose/compose-material3/api/current.txt
@@ -317,7 +317,7 @@
public final class CircularProgressIndicatorKt {
method @androidx.compose.runtime.Composable public static void CircularProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize);
method @androidx.compose.runtime.Composable public static void CircularProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional boolean allowProgressOverflow, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
- method @androidx.compose.runtime.Composable public static void CircularProgressIndicatorContent(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional boolean allowProgressOverflow, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
+ method @androidx.compose.runtime.Composable public static void CircularProgressIndicatorStatic(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional boolean allowProgressOverflow, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled, optional float overflowColorAlphaFraction, optional kotlin.jvm.functions.Function0<java.lang.Float>? targetProgress);
}
@androidx.compose.runtime.Immutable @androidx.compose.runtime.Stable public final class ColorScheme {
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
index f8be644..fc1325b 100644
--- a/wear/compose/compose-material3/api/restricted_current.txt
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -317,7 +317,7 @@
public final class CircularProgressIndicatorKt {
method @androidx.compose.runtime.Composable public static void CircularProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize);
method @androidx.compose.runtime.Composable public static void CircularProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional boolean allowProgressOverflow, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
- method @androidx.compose.runtime.Composable public static void CircularProgressIndicatorContent(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional boolean allowProgressOverflow, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
+ method @androidx.compose.runtime.Composable public static void CircularProgressIndicatorStatic(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional boolean allowProgressOverflow, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled, optional float overflowColorAlphaFraction, optional kotlin.jvm.functions.Function0<java.lang.Float>? targetProgress);
}
@androidx.compose.runtime.Immutable @androidx.compose.runtime.Stable public final class ColorScheme {
diff --git a/wear/compose/compose-material3/benchmark/build.gradle b/wear/compose/compose-material3/benchmark/build.gradle
index ae72baf..9e88dd9 100644
--- a/wear/compose/compose-material3/benchmark/build.gradle
+++ b/wear/compose/compose-material3/benchmark/build.gradle
@@ -45,14 +45,14 @@
dependencies {
- androidTestImplementation project(":benchmark:benchmark-junit4")
- androidTestImplementation project(":compose:runtime:runtime")
- androidTestImplementation project(":compose:ui:ui-text:ui-text-benchmark")
- androidTestImplementation project(":compose:foundation:foundation")
- androidTestImplementation project(":compose:runtime:runtime")
- androidTestImplementation project(":compose:benchmark-utils")
- androidTestImplementation project(":wear:compose:compose-foundation")
- androidTestImplementation project(":wear:compose:compose-material3")
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:ui:ui-text:ui-text-benchmark"))
+ androidTestImplementation(project(":compose:foundation:foundation"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:benchmark-utils"))
+ androidTestImplementation(project(":wear:compose:compose-foundation"))
+ androidTestImplementation(project(":wear:compose:compose-material3"))
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
diff --git a/wear/compose/compose-material3/build.gradle b/wear/compose/compose-material3/build.gradle
index efbb530a..923e7bf 100644
--- a/wear/compose/compose-material3/build.gradle
+++ b/wear/compose/compose-material3/build.gradle
@@ -76,15 +76,10 @@
}
// Use Robolectric 4.+
testOptions.unitTests.includeAndroidResources = true
- sourceSets.androidTest.assets.srcDirs +=
- project.rootDir.absolutePath + "/../../golden/wear/compose/material3"
buildTypes.configureEach {
consumerProguardFiles("proguard-rules.pro")
}
namespace = "androidx.wear.compose.material3"
- lint {
- baseline = file("lint-baseline.xml")
- }
}
androidx {
@@ -100,6 +95,7 @@
metalavaK2UastEnabled = false
samples(project(":wear:compose:compose-material3-samples"))
kotlinTarget = KotlinTarget.KOTLIN_1_9
+ addGoldenImageAssets()
}
tasks.withType(KotlinCompile).configureEach {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt
index f1b4d1f6..8add3dd 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt
@@ -58,7 +58,7 @@
import androidx.wear.compose.material3.StepperDefaults
import androidx.wear.compose.material3.SwitchButton
import androidx.wear.compose.material3.Text
-import androidx.wear.compose.material3.samples.CircularProgressIndicatorContentSample
+import androidx.wear.compose.material3.samples.CircularProgressIndicatorStaticSample
import androidx.wear.compose.material3.samples.FullScreenProgressIndicatorSample
import androidx.wear.compose.material3.samples.IndeterminateProgressArcSample
import androidx.wear.compose.material3.samples.IndeterminateProgressIndicatorSample
@@ -118,7 +118,7 @@
Centralize { CircularProgressCustomisableFullScreenDemo() }
},
ComposableDemo("Custom animation") {
- Centralize { CircularProgressIndicatorContentSample() }
+ Centralize { CircularProgressIndicatorStaticSample() }
},
)
),
diff --git a/wear/compose/compose-material3/macrobenchmark-common/build.gradle b/wear/compose/compose-material3/macrobenchmark-common/build.gradle
index 6a7a3a7..3d0c2eb 100644
--- a/wear/compose/compose-material3/macrobenchmark-common/build.gradle
+++ b/wear/compose/compose-material3/macrobenchmark-common/build.gradle
@@ -28,6 +28,6 @@
implementation(project(":compose:ui:ui-tooling"))
implementation(project(":wear:compose:compose-foundation"))
implementation(project(":wear:compose:compose-material3"))
- implementation project(":wear:compose:compose-material3-samples")
+ implementation(project(":wear:compose:compose-material3-samples"))
implementation("androidx.compose.material:material-icons-core:1.6.0")
}
\ No newline at end of file
diff --git a/wear/compose/compose-material3/macrobenchmark/src/main/java/androidx/wear/compose/material3/macrobenchmark/metric/FrameCostQuery.kt b/wear/compose/compose-material3/macrobenchmark/src/main/java/androidx/wear/compose/material3/macrobenchmark/metric/FrameCostQuery.kt
index d95a012..a467b50 100644
--- a/wear/compose/compose-material3/macrobenchmark/src/main/java/androidx/wear/compose/material3/macrobenchmark/metric/FrameCostQuery.kt
+++ b/wear/compose/compose-material3/macrobenchmark/src/main/java/androidx/wear/compose/material3/macrobenchmark/metric/FrameCostQuery.kt
@@ -16,7 +16,7 @@
package androidx.wear.compose.material3.macrobenchmark.metric
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
import org.intellij.lang.annotations.Language
/** A copy from aosp/3328563 */
@@ -44,7 +44,7 @@
INNER JOIN slice post on post.name = 'postAndWait' and slice_is_ancestor(doFrame.id, post.id)
WHERE
- ${androidx.benchmark.perfetto.processNameLikePkg(packageName)}
+ ${androidx.benchmark.traceprocessor.processNameLikePkg(packageName)}
AND
-- remove "incomplete" frames
a.dur > 0
@@ -52,7 +52,7 @@
.trimIndent()
internal fun getFrameCost(
- session: PerfettoTraceProcessor.Session,
+ session: TraceProcessor.Session,
packageName: String,
): List<Double> {
val queryResultIterator = session.query(query = getFullQuery(packageName))
diff --git a/wear/compose/compose-material3/macrobenchmark/src/main/java/androidx/wear/compose/material3/macrobenchmark/metric/Metric.kt b/wear/compose/compose-material3/macrobenchmark/src/main/java/androidx/wear/compose/material3/macrobenchmark/metric/Metric.kt
index aaffb35..3a4c952 100644
--- a/wear/compose/compose-material3/macrobenchmark/src/main/java/androidx/wear/compose/material3/macrobenchmark/metric/Metric.kt
+++ b/wear/compose/compose-material3/macrobenchmark/src/main/java/androidx/wear/compose/material3/macrobenchmark/metric/Metric.kt
@@ -14,20 +14,18 @@
* limitations under the License.
*/
-@file:OptIn(androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi::class)
-
package androidx.wear.compose.material3.macrobenchmark.metric
import androidx.benchmark.macro.ExperimentalMetricApi
import androidx.benchmark.macro.TraceMetric
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
/** A copy from aosp/3328563 */
@ExperimentalMetricApi
class FrameCostMetric : TraceMetric() {
override fun getMeasurements(
captureInfo: CaptureInfo,
- traceSession: PerfettoTraceProcessor.Session
+ traceSession: TraceProcessor.Session
): List<Measurement> {
val costs =
FrameCostQuery.getFrameCost(
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ProgressIndicatorSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ProgressIndicatorSample.kt
index c4f7d40..055de9a 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ProgressIndicatorSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ProgressIndicatorSample.kt
@@ -48,8 +48,8 @@
import androidx.wear.compose.material3.ArcProgressIndicatorDefaults
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.CircularProgressIndicator
-import androidx.wear.compose.material3.CircularProgressIndicatorContent
import androidx.wear.compose.material3.CircularProgressIndicatorDefaults
+import androidx.wear.compose.material3.CircularProgressIndicatorStatic
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.IconButton
import androidx.wear.compose.material3.IconButtonDefaults
@@ -165,7 +165,7 @@
@Sampled
@Composable
-fun CircularProgressIndicatorContentSample() {
+fun CircularProgressIndicatorStaticSample() {
val progress = remember { mutableFloatStateOf(0f) }
val animatedProgress = remember { Animatable(0f) }
@@ -187,9 +187,9 @@
label = { Text("Animate") },
)
- // Since CircularProgressIndicatorContent does not have any built-in progress animations,
+ // Since CircularProgressIndicatorStatic does not have any built-in progress animations,
// we can implement a custom progress animation by using an Animatable progress value.
- CircularProgressIndicatorContent(
+ CircularProgressIndicatorStatic(
progress = animatedProgress::value,
startAngle = 120f,
endAngle = 60f,
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorScreenshotTest.kt
index b0f7988..42c5506 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorScreenshotTest.kt
@@ -180,7 +180,7 @@
@Test
fun circular_progress_indicator_content(@TestParameter screenSize: ScreenSize) =
verifyProgressIndicatorScreenshot(screenSize = screenSize) {
- CircularProgressIndicatorContent(
+ CircularProgressIndicatorStatic(
progress = { 0.25f },
modifier = Modifier.aspectRatio(1f).testTag(TEST_TAG),
startAngle = 120f,
@@ -191,7 +191,7 @@
@Test
fun circular_progress_indicator_content_overflow(@TestParameter screenSize: ScreenSize) =
verifyProgressIndicatorScreenshot(screenSize = screenSize) {
- CircularProgressIndicatorContent(
+ CircularProgressIndicatorStatic(
progress = { 1.2f },
modifier = Modifier.aspectRatio(1f).testTag(TEST_TAG),
startAngle = 120f,
@@ -203,7 +203,7 @@
@Test
fun circular_progress_indicator_content_disabled(@TestParameter screenSize: ScreenSize) =
verifyProgressIndicatorScreenshot(screenSize = screenSize) {
- CircularProgressIndicatorContent(
+ CircularProgressIndicatorStatic(
progress = { 0.25f },
modifier = Modifier.aspectRatio(1f).testTag(TEST_TAG),
startAngle = 120f,
@@ -215,7 +215,7 @@
@Test
fun circular_progress_indicator_content_custom_color(@TestParameter screenSize: ScreenSize) =
verifyProgressIndicatorScreenshot(screenSize = screenSize) {
- CircularProgressIndicatorContent(
+ CircularProgressIndicatorStatic(
progress = { 0.75f },
modifier = Modifier.size(200.dp).testTag(TEST_TAG),
startAngle = 120f,
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorTest.kt
index 0b06023..21b257c 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorTest.kt
@@ -266,7 +266,7 @@
@Test
fun supports_testtag() {
setContentWithTheme {
- CircularProgressIndicatorContent(
+ CircularProgressIndicatorStatic(
progress = { 0.5f },
modifier = Modifier.testTag(TEST_TAG)
)
@@ -280,7 +280,7 @@
val progress = mutableStateOf(0f)
setContentWithTheme {
- CircularProgressIndicatorContent(
+ CircularProgressIndicatorStatic(
modifier =
Modifier.testTag(TEST_TAG).semantics {
progressBarRangeInfo = ProgressBarRangeInfo(progress.value, 0f..1f)
@@ -300,7 +300,7 @@
@Test
fun contains_progress_color() {
setContentWithTheme {
- CircularProgressIndicatorContent(
+ CircularProgressIndicatorStatic(
modifier = Modifier.size(COMPONENT_SIZE).testTag(TEST_TAG),
progress = { 1f },
colors =
@@ -323,7 +323,7 @@
@Test
fun contains_progress_incomplete_color() {
setContentWithTheme {
- CircularProgressIndicatorContent(
+ CircularProgressIndicatorStatic(
modifier = Modifier.size(COMPONENT_SIZE).testTag(TEST_TAG),
progress = { 0f },
colors =
@@ -346,7 +346,7 @@
@Test
fun change_start_end_angle() {
setContentWithTheme {
- CircularProgressIndicatorContent(
+ CircularProgressIndicatorStatic(
modifier = Modifier.size(COMPONENT_SIZE).testTag(TEST_TAG),
progress = { 0.5f },
startAngle = 0f,
@@ -375,7 +375,7 @@
@Test
fun set_small_progress_value() {
setContentWithTheme {
- CircularProgressIndicatorContent(
+ CircularProgressIndicatorStatic(
modifier = Modifier.size(COMPONENT_SIZE).testTag(TEST_TAG),
progress = { 0.02f },
colors =
@@ -401,7 +401,7 @@
@Test
fun set_small_stroke_width() {
setContentWithTheme {
- CircularProgressIndicatorContent(
+ CircularProgressIndicatorStatic(
modifier = Modifier.size(COMPONENT_SIZE).testTag(TEST_TAG),
progress = { 0.5f },
strokeWidth = CircularProgressIndicatorDefaults.smallStrokeWidth,
@@ -427,7 +427,7 @@
@Test
fun set_large_stroke_width() {
setContentWithTheme {
- CircularProgressIndicatorContent(
+ CircularProgressIndicatorStatic(
modifier = Modifier.size(COMPONENT_SIZE).testTag(TEST_TAG),
progress = { 0.5f },
strokeWidth = CircularProgressIndicatorDefaults.largeStrokeWidth,
@@ -454,7 +454,7 @@
@Test
fun progress_disabled_contains_disabled_colors() {
setContentWithTheme {
- CircularProgressIndicatorContent(
+ CircularProgressIndicatorStatic(
modifier = Modifier.size(COMPONENT_SIZE).testTag(TEST_TAG),
progress = { 0.5f },
enabled = false,
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Screenshot.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Screenshot.kt
index 0b3bb8d..a5279d4 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Screenshot.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Screenshot.kt
@@ -16,4 +16,4 @@
package androidx.wear.compose.material3
-internal const val SCREENSHOT_GOLDEN_PATH = "wear/compose/material3"
+internal const val SCREENSHOT_GOLDEN_PATH = "wear/compose/compose-material3"
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CircularProgressIndicator.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CircularProgressIndicator.kt
index 85fee0f..007acef 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CircularProgressIndicator.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CircularProgressIndicator.kt
@@ -118,7 +118,7 @@
enabled: Boolean = true,
) {
if (allowProgressOverflow) {
- CircularProgressIndicatorWithOverflowImpl(
+ AnimatedCircularProgressIndicatorWithOverflowImpl(
progress,
modifier,
startAngle,
@@ -129,7 +129,7 @@
enabled
)
} else {
- CircularProgressIndicatorImpl(
+ AnimatedCircularProgressIndicatorImpl(
progress,
modifier,
startAngle,
@@ -143,11 +143,13 @@
}
/**
- * Simple circular progress indicator without any progress animations.
+ * [CircularProgressIndicatorStatic] is a non-animating circular progress indicator. Prefer to use
+ * [CircularProgressIndicator] directly instead of this overload in order to access the recommended
+ * animations, but this overload can be used when custom animations are required.
*
- * Example of [CircularProgressIndicatorContent] with custom progress animation:
+ * Example of [CircularProgressIndicatorStatic] with custom progress animation:
*
- * @sample androidx.wear.compose.material3.samples.CircularProgressIndicatorContentSample
+ * @sample androidx.wear.compose.material3.samples.CircularProgressIndicatorStaticSample
* @param progress The progress of this progress indicator where 0.0 represents no progress and 1.0
* represents completion.
* @param modifier Modifier to be applied to the CircularProgressIndicator.
@@ -173,9 +175,14 @@
* @param enabled controls the enabled state. Although this component is not clickable, it can be
* contained within a clickable component. When enabled is `false`, this component will appear
* visually disabled.
+ * @param overflowColorAlphaFraction Alpha fraction to apply to the overflow color. This can be used
+ * to implement animated transition from progress color to overflow color. For no animation should
+ * be set to 1.
+ * @param targetProgress Target value if the progress value is to be animated. Used to determine if
+ * the min progress values should be enforced. For no animation this should be null.
*/
@Composable
-fun CircularProgressIndicatorContent(
+fun CircularProgressIndicatorStatic(
progress: () -> Float,
modifier: Modifier = Modifier,
allowProgressOverflow: Boolean = false,
@@ -185,17 +192,91 @@
strokeWidth: Dp = CircularProgressIndicatorDefaults.largeStrokeWidth,
gapSize: Dp = CircularProgressIndicatorDefaults.calculateRecommendedGapSize(strokeWidth),
enabled: Boolean = true,
+ overflowColorAlphaFraction: Float = 1f,
+ targetProgress: (() -> Float)? = null,
) {
- CircularProgressIndicatorContentImpl(
- progress = progress,
- modifier = modifier,
- allowProgressOverflow = allowProgressOverflow,
- startAngle = startAngle,
- endAngle = endAngle,
- colors = colors,
- strokeWidth = strokeWidth,
- gapSize = gapSize,
- enabled = enabled,
+ // Canvas internally uses Spacer.drawBehind.
+ // Using Spacer.drawWithCache to optimize the stroke allocations.
+ Spacer(
+ modifier
+ .clearAndSetSemantics {}
+ .fillMaxSize()
+ .focusable()
+ .drawWithCache {
+ onDrawWithContent {
+ val fullSweep = 360f - ((startAngle - endAngle) % 360 + 360) % 360
+ val strokePx = strokeWidth.toPx()
+ val gapSizePx = gapSize.toPx()
+ val minSize = min(size.height, size.width)
+ // Sweep angle between two progress indicator segments.
+ val gapSweep =
+ asin((strokePx + gapSizePx) / (minSize - strokePx)).toDegrees() * 2f
+ val currentProgress = progress()
+ val hasOverflow = allowProgressOverflow && currentProgress > 1f
+ val wrappedProgress = wrapProgress(currentProgress, allowProgressOverflow)
+ var progressSweep = fullSweep * wrappedProgress
+
+ // If progress sweep or remaining track sweep is smaller than gap sweep, it
+ // will be shown as a small dot. This dot should never be shown as
+ // static value, only in progress animation transitions.
+ val target = if (targetProgress != null) targetProgress() else currentProgress
+ val wrappedTargetProgress = wrapProgress(target, allowProgressOverflow)
+ val isValidTarget =
+ target.isFullInt() ||
+ (!allowProgressOverflow && target == 1 + GapExtraProgress) ||
+ wrappedTargetProgress * fullSweep in gapSweep..fullSweep - gapSweep
+ if (
+ !wrappedProgress.isFullInt() &&
+ !isValidTarget &&
+ floor(currentProgress) == floor(target)
+ ) {
+ progressSweep = progressSweep.coerceIn(gapSweep, fullSweep - gapSweep)
+ }
+
+ if (hasOverflow) {
+ // Draw the overflow track background.
+ drawIndicatorSegment(
+ startAngle = startAngle + progressSweep,
+ sweep = fullSweep - progressSweep,
+ gapSweep = gapSweep,
+ brush = colors.overflowTrackBrush(enabled, overflowColorAlphaFraction),
+ strokeWidth = strokePx,
+ )
+ } else {
+ // Draw the track background.
+ drawIndicatorSegment(
+ startAngle = startAngle + progressSweep,
+ sweep = fullSweep - progressSweep,
+ gapSweep = gapSweep,
+ brush = colors.trackBrush(enabled),
+ strokeWidth = strokePx,
+ )
+ }
+
+ if (!allowProgressOverflow && startAngle == endAngle && wrappedProgress == 1f) {
+ // Draw the full circle with merged gap.
+ val gapFraction =
+ (1f + GapExtraProgress - currentProgress).absoluteValue /
+ GapExtraProgress
+ drawIndicatorSegment(
+ startAngle = startAngle,
+ sweep = progressSweep,
+ gapSweep = gapFraction,
+ brush = colors.indicatorBrush(enabled),
+ strokeWidth = strokePx,
+ )
+ } else {
+ // Draw the indicator.
+ drawIndicatorSegment(
+ startAngle = startAngle,
+ sweep = progressSweep,
+ gapSweep = gapSweep,
+ brush = colors.indicatorBrush(enabled),
+ strokeWidth = strokePx,
+ )
+ }
+ }
+ }
)
}
@@ -272,7 +353,7 @@
/** Animated circular progress indicator implementation without overflow support. */
@Composable
-private fun CircularProgressIndicatorImpl(
+private fun AnimatedCircularProgressIndicatorImpl(
progress: () -> Float,
modifier: Modifier,
startAngle: Float,
@@ -301,7 +382,7 @@
}
}
- CircularProgressIndicatorContentImpl(
+ CircularProgressIndicatorStatic(
progress = { animatedProgress.value },
modifier = modifier,
allowProgressOverflow = false,
@@ -311,13 +392,13 @@
strokeWidth = strokeWidth,
gapSize = gapSize,
enabled = enabled,
- targetProgress = animatedProgress.targetValue
+ targetProgress = { animatedProgress.targetValue }
)
}
/** Animated circular progress indicator implementation with overflow support. */
@Composable
-private fun CircularProgressIndicatorWithOverflowImpl(
+private fun AnimatedCircularProgressIndicatorWithOverflowImpl(
progress: () -> Float,
modifier: Modifier,
startAngle: Float,
@@ -372,7 +453,7 @@
}
}
- CircularProgressIndicatorContentImpl(
+ CircularProgressIndicatorStatic(
progress = { animatedProgress.value },
modifier = modifier,
allowProgressOverflow = true,
@@ -383,140 +464,7 @@
gapSize = gapSize,
enabled = enabled,
overflowColorAlphaFraction = animatedOverflowColor.value,
- targetProgress = animatedProgress.targetValue
- )
-}
-
-/**
- * Internal implementation that draws the circular progress indicator. Can be used by both animated
- * and non-animated version of [CircularProgressIndicator].
- *
- * @param progress The progress of this progress indicator where 0.0 represents no progress and 1.0
- * represents completion.
- * @param modifier Modifier to be applied to the CircularProgressIndicator.
- * @param allowProgressOverflow When progress overflow is allowed, values smaller than 0.0 will be
- * coerced to 0, while values larger than 1.0 will be wrapped around and shown as overflow with a
- * different track color [ProgressIndicatorColors.overflowTrackBrush]. For example values 1.2, 2.2
- * etc will be shown as 20% progress with the overflow color. When progress overflow is not
- * allowed, progress values will be coerced into the range 0..1.
- * @param startAngle The starting position of the progress arc, measured clockwise in degrees (0
- * to 360) from the 3 o'clock position. For example, 0 and 360 represent 3 o'clock, 90 and 180
- * represent 6 o'clock and 9 o'clock respectively. Default is 270 degrees
- * [CircularProgressIndicatorDefaults.StartAngle] (top of the screen).
- * @param endAngle The ending position of the progress arc, measured clockwise in degrees (0 to 360)
- * from the 3 o'clock position. For example, 0 and 360 represent 3 o'clock, 90 and 180 represent 6
- * o'clock and 9 o'clock respectively. By default equal to [startAngle].
- * @param colors [ProgressIndicatorColors] that will be used to resolve the indicator and track
- * color for this progress indicator in different states.
- * @param strokeWidth The stroke width for the progress indicator. The recommended values are
- * [CircularProgressIndicatorDefaults.largeStrokeWidth] and
- * [CircularProgressIndicatorDefaults.smallStrokeWidth].
- * @param gapSize The size (in Dp) of the gap between the ends of the progress indicator and the
- * track. The stroke endcaps are not included in this distance.
- * @param enabled controls the enabled state. Although this component is not clickable, it can be
- * contained within a clickable component. When enabled is `false`, this component will appear
- * visually disabled.
- * @param overflowColorAlphaFraction color alpha that is applied to the overflow color to allow for
- * animated transition from progress color to overflow color. For no animation should be set to 1.
- * @param targetProgress target progress for the animated progress value. Used to determine if the
- * min progress values should be enforced. For no animation this should be null.
- */
-@Composable
-private fun CircularProgressIndicatorContentImpl(
- progress: () -> Float,
- modifier: Modifier = Modifier,
- allowProgressOverflow: Boolean = false,
- startAngle: Float = CircularProgressIndicatorDefaults.StartAngle,
- endAngle: Float = startAngle,
- colors: ProgressIndicatorColors = ProgressIndicatorDefaults.colors(),
- strokeWidth: Dp = CircularProgressIndicatorDefaults.largeStrokeWidth,
- gapSize: Dp = CircularProgressIndicatorDefaults.calculateRecommendedGapSize(strokeWidth),
- enabled: Boolean = true,
- overflowColorAlphaFraction: Float = 1f,
- targetProgress: Float? = null
-) {
- // Canvas internally uses Spacer.drawBehind.
- // Using Spacer.drawWithCache to optimize the stroke allocations.
- Spacer(
- modifier
- .clearAndSetSemantics {}
- .fillMaxSize()
- .focusable()
- .drawWithCache {
- onDrawWithContent {
- val fullSweep = 360f - ((startAngle - endAngle) % 360 + 360) % 360
- val strokePx = strokeWidth.toPx()
- val gapSizePx = gapSize.toPx()
- val minSize = min(size.height, size.width)
- // Sweep angle between two progress indicator segments.
- val gapSweep =
- asin((strokePx + gapSizePx) / (minSize - strokePx)).toDegrees() * 2f
- val hasOverflow = allowProgressOverflow && progress() > 1f
- val currentProgress = progress()
- val wrappedProgress = wrapProgress(currentProgress, allowProgressOverflow)
- var progressSweep = fullSweep * wrappedProgress
-
- // If progress sweep or remaining track sweep is smaller than gap sweep, it
- // will be shown as a small dot. This dot should never be shown as
- // static value, only in progress animation transitions.
- val target = targetProgress ?: currentProgress
- val wrappedTargetProgress = wrapProgress(target, allowProgressOverflow)
- val isValidTarget =
- target.isFullInt() ||
- (!allowProgressOverflow && target == 1 + GapExtraProgress) ||
- wrappedTargetProgress * fullSweep in gapSweep..fullSweep - gapSweep
- if (
- !wrappedProgress.isFullInt() &&
- !isValidTarget &&
- floor(currentProgress) == floor(target)
- ) {
- progressSweep = progressSweep.coerceIn(gapSweep, fullSweep - gapSweep)
- }
-
- if (hasOverflow) {
- // Draw the overflow track background.
- drawIndicatorSegment(
- startAngle = startAngle + progressSweep,
- sweep = fullSweep - progressSweep,
- gapSweep = gapSweep,
- brush = colors.overflowTrackBrush(enabled, overflowColorAlphaFraction),
- strokeWidth = strokePx,
- )
- } else {
- // Draw the track background.
- drawIndicatorSegment(
- startAngle = startAngle + progressSweep,
- sweep = fullSweep - progressSweep,
- gapSweep = gapSweep,
- brush = colors.trackBrush(enabled),
- strokeWidth = strokePx,
- )
- }
-
- if (!allowProgressOverflow && startAngle == endAngle && wrappedProgress == 1f) {
- // Draw the full circle with merged gap.
- val gapFraction =
- (1f + GapExtraProgress - currentProgress).absoluteValue /
- GapExtraProgress
- drawIndicatorSegment(
- startAngle = startAngle,
- sweep = progressSweep,
- gapSweep = gapFraction,
- brush = colors.indicatorBrush(enabled),
- strokeWidth = strokePx,
- )
- } else {
- // Draw the indicator.
- drawIndicatorSegment(
- startAngle = startAngle,
- sweep = progressSweep,
- gapSweep = gapSweep,
- brush = colors.indicatorBrush(enabled),
- strokeWidth = strokePx,
- )
- }
- }
- }
+ targetProgress = { animatedProgress.targetValue }
)
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/OpenOnPhoneDialog.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/OpenOnPhoneDialog.kt
index 7f2ffef..4cf45b2 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/OpenOnPhoneDialog.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/OpenOnPhoneDialog.kt
@@ -393,7 +393,7 @@
.background(iconContainerColor)
)
- CircularProgressIndicatorContent(
+ CircularProgressIndicatorStatic(
modifier = Modifier.graphicsLayer { alpha = progressAlphaAnimationFraction.value },
progress = progress,
strokeWidth = strokeWidth,
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwipeToReveal.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwipeToReveal.kt
index 3653409..f972659 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwipeToReveal.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwipeToReveal.kt
@@ -56,6 +56,7 @@
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.RevealActionType
+import androidx.wear.compose.foundation.RevealScope
import androidx.wear.compose.foundation.RevealState
import androidx.wear.compose.foundation.RevealValue
import androidx.wear.compose.foundation.SwipeDirection
@@ -402,7 +403,7 @@
}
@Composable
-internal fun ActionButton(
+internal fun RevealScope.ActionButton(
revealState: RevealState,
action: SwipeToRevealAction,
revealActionType: RevealActionType,
@@ -513,7 +514,6 @@
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
- val density = LocalDensity.current
val primaryActionTextRevealed = remember { mutableStateOf(false) }
action.icon?.let {
ActionIconWrapper(revealState, iconStartFadeInFraction, iconEndFadeInFraction, it)
@@ -529,12 +529,8 @@
}
LaunchedEffect(revealState.offset) {
- val minimumOffsetToRevealPx =
- with(density) {
- SwipeToRevealDefaults.DoubleActionAnchorWidth.toPx().toInt()
- }
primaryActionTextRevealed.value =
- abs(revealState.offset) > minimumOffsetToRevealPx &&
+ abs(revealState.offset) > revealOffset &&
(revealState.targetValue == RevealValue.RightRevealed ||
revealState.targetValue == RevealValue.LeftRevealed)
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/lazy/ContentTransformation.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/lazy/ContentTransformation.kt
index 11de1de..1c1835b 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/lazy/ContentTransformation.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/lazy/ContentTransformation.kt
@@ -16,10 +16,10 @@
package androidx.wear.compose.material3.lazy
-import androidx.compose.runtime.State
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Shape
@@ -33,6 +33,7 @@
) =
with(behavior) {
scrollProgress()?.let {
+ compositingStrategy = CompositingStrategy.Offscreen
clip = true
shape =
object : Shape {
@@ -57,16 +58,17 @@
}
translationX = size.width * it.contentXOffsetFraction * it.scale
translationY = -1f * size.height * (1f - it.scale) / 2f
- alpha = it.contentAlpha.coerceAtMost(0.99f) // Alpha hack.
+ alpha = it.contentAlpha
scaleX = it.scale
scaleY = it.scale
}
}
internal fun GraphicsLayerScope.contentTransformation(
- transformState: State<TransformationState?>,
+ transformState: TransformationState?,
) =
- transformState.value?.let {
+ transformState?.let {
+ compositingStrategy = CompositingStrategy.Offscreen
clip = true
shape =
object : Shape {
@@ -90,7 +92,7 @@
}
translationX = size.width * it.contentXOffsetFraction * it.scale
translationY = -1f * size.height * (1f - it.scale) / 2f
- alpha = it.contentAlpha.coerceAtMost(0.99f) // Alpha hack.
+ alpha = it.contentAlpha
scaleX = it.scale
scaleY = it.scale
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/lazy/ScrollTransform.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/lazy/ScrollTransform.kt
index 232d900..cdf51a8 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/lazy/ScrollTransform.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/lazy/ScrollTransform.kt
@@ -19,7 +19,6 @@
import androidx.compose.animation.core.Easing
import androidx.compose.foundation.BorderStroke
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -35,6 +34,7 @@
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.util.fastRoundToInt
+import androidx.compose.ui.util.lerp
import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
import androidx.wear.compose.foundation.lazy.TransformingLazyColumnItemScope
import androidx.wear.compose.foundation.lazy.TransformingLazyColumnItemScrollProgress
@@ -185,33 +185,17 @@
.transformedHeight { height, scrollProgress ->
// TODO: may be better to create once and update. Still need to ensure readers
// are reading a state so they register to updates.
- val transformProgress = transformProgress(scrollProgress, updatedSpec)
- val scale = transformProgress.compute(updatedSpec.scale, updatedSpec.easing)
- val morphedHeight = height.toFloat() // TODO: Implement morphing
- transformation.value =
- TransformationState(
- scale = scale,
- containerAlpha =
- transformProgress.compute(
- updatedSpec.containerAlpha,
- updatedSpec.easing
- ),
- contentAlpha =
- transformProgress.compute(
- updatedSpec.contentAlpha,
- updatedSpec.easing
- ),
- morphWidth =
- transformProgress.compute(
- updatedSpec.containerWidth,
- updatedSpec.easing
- ),
- minMorphingHeight = minMorphingHeight,
- morphedHeight = morphedHeight
+ transformationState(
+ spec = updatedSpec,
+ itemHeight = height.toFloat(),
+ minMorphingHeight = minMorphingHeight.value,
+ scrollProgress = scrollProgress
)
- (morphedHeight * scale).fastRoundToInt()
+ .also { transformation.value = it }
+ .placementHeight
+ .fastRoundToInt()
}
- .graphicsLayer { contentTransformation(transformation) }
+ .graphicsLayer { contentTransformation(transformation.value) }
}
/**
@@ -253,33 +237,17 @@
.transformedHeight { height, scrollProgress ->
// TODO: may be better to create once and update. Still need to ensure readers
// are reading a state so they register to updates.
- val transformProgress = transformProgress(scrollProgress, updatedSpec)
- val scale = transformProgress.compute(updatedSpec.scale, updatedSpec.easing)
- val morphedHeight = height.toFloat() // TODO: Implement morphing
- transformation.value =
- TransformationState(
- scale = scale,
- containerAlpha =
- transformProgress.compute(
- updatedSpec.containerAlpha,
- updatedSpec.easing
- ),
- contentAlpha =
- transformProgress.compute(
- updatedSpec.contentAlpha,
- updatedSpec.easing
- ),
- morphWidth =
- transformProgress.compute(
- updatedSpec.containerWidth,
- updatedSpec.easing
- ),
- minMorphingHeight = minMorphingHeight,
- morphedHeight = morphedHeight
+ transformationState(
+ spec = updatedSpec,
+ itemHeight = height.toFloat(),
+ minMorphingHeight = minMorphingHeight.value,
+ scrollProgress = scrollProgress
)
- (morphedHeight * scale).fastRoundToInt()
+ .also { transformation.value = it }
+ .placementHeight
+ .fastRoundToInt()
}
- .graphicsLayer { contentTransformation(transformation) }
+ .graphicsLayer { contentTransformation(transformState = transformation.value) }
.clip(shape)
}
@@ -288,7 +256,7 @@
* top transition area, the bottom transition area, or neither.
*/
@JvmInline
-private value class TransitionAreaProgress(private val encodedProgress: Float) {
+internal value class TransitionAreaProgress(private val encodedProgress: Float) {
// encodedProgress is, going from top to bottom:
// -1 to 0 for top transition
// 0 in the center of the screen, no transition
@@ -317,11 +285,7 @@
variable.transformationZoneExitFraction,
progress
)
- return androidx.compose.ui.util.lerp(
- edgeValue,
- 1f,
- easing.transform(transformationZoneProgress)
- )
+ return lerp(edgeValue, 1f, easing.transform(transformationZoneProgress))
}
companion object {
@@ -342,6 +306,34 @@
}
}
+/** Uses a TransformationSpec to compute a TransformationState. */
+internal fun transformationState(
+ spec: TransformationSpec,
+ itemHeight: Float,
+ minMorphingHeight: Float?,
+ scrollProgress: TransformingLazyColumnItemScrollProgress
+): TransformationState {
+ val transformProgress = transformProgress(scrollProgress, spec)
+ val scale = transformProgress.compute(spec.scale, spec.easing)
+ val morphedHeight =
+ minMorphingHeight?.let {
+ morphedHeight(
+ scrollProgress = scrollProgress,
+ spec = spec,
+ itemHeight = itemHeight,
+ minMorphingHeight = it
+ )
+ } ?: itemHeight
+
+ return TransformationState(
+ scale = scale,
+ containerAlpha = transformProgress.compute(spec.containerAlpha, spec.easing),
+ contentAlpha = transformProgress.compute(spec.contentAlpha, spec.easing),
+ morphWidth = transformProgress.compute(spec.containerWidth, spec.easing),
+ morphedHeight = morphedHeight
+ )
+}
+
/** Uses a TransformationSpec to convert a scrollProgress into a transitionProgress. */
private fun transformProgress(
scrollProgress: TransformingLazyColumnItemScrollProgress?,
@@ -360,8 +352,7 @@
// Size of each transition area.
val scalingLine =
- androidx.compose.ui.util
- .lerp(spec.minTransitionArea, spec.maxTransitionArea, sizeRatio)
+ lerp(spec.minTransitionArea, spec.maxTransitionArea, sizeRatio)
// Ensure the top & bottom transition areas don't overlap.
.coerceAtMost((1f + relativeItemHeight) / 2f)
@@ -373,14 +364,43 @@
}
}
+/** Compute new morphing height for the item based on its size and position on the screen. */
+private fun morphedHeight(
+ scrollProgress: TransformingLazyColumnItemScrollProgress,
+ spec: TransformationSpec,
+ itemHeight: Float,
+ minMorphingHeight: Float
+): Float {
+ // Size of the item, relative to the screen
+ val relativeItemHeight = scrollProgress.bottomOffsetFraction - scrollProgress.topOffsetFraction
+ val screenSize = itemHeight / relativeItemHeight
+
+ val growthStartTopOffsetFraction =
+ spec.growthStartScreenFraction - minMorphingHeight / screenSize
+ val growthEndTopOffsetFraction = spec.growthEndScreenFraction - relativeItemHeight
+ // Fraction of how item has grown so far.
+ val heightMorphProgress =
+ // growthStartTopOffsetFraction > growthEndTopOffsetFraction since item has minimum size at
+ // the bottom of the screen.
+ inverseLerp(
+ growthStartTopOffsetFraction,
+ growthEndTopOffsetFraction,
+ scrollProgress.topOffsetFraction
+ )
+ .coerceIn(0f, 1f)
+ return lerp(minMorphingHeight, itemHeight, heightMorphProgress)
+}
+
// TODO: Decide what we want to compute & store vs compute when needed.
internal data class TransformationState(
val containerAlpha: Float,
val contentAlpha: Float,
val scale: Float,
val morphWidth: Float,
- val minMorphingHeight: State<Float?>,
val morphedHeight: Float, // Height after morphing, before scaling
- val contentXOffsetFraction: Float = 0f, // TODO: Implement morphing
- val backgroundXOffsetFraction: Float = 1f // TODO: Implement morphing
-)
+ val contentXOffsetFraction: Float = 0f, // TODO: Implement horizontal morphing
+ val backgroundXOffsetFraction: Float = 1f // TODO: Implement horizontal morphing
+) {
+ internal val placementHeight: Float
+ get() = morphedHeight * scale
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/lazy/TransformationSpec.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/lazy/TransformationSpec.kt
index 461eb9a..c0091b4 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/lazy/TransformationSpec.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/lazy/TransformationSpec.kt
@@ -89,17 +89,28 @@
/** Configuration for the width of the container. */
val containerWidth: TransformVariableSpec,
- // TBD
- val morphHeight: Float,
- val morphHeightDriftFactor: Float,
- val morphPivotX: Float,
- val morphPivotY: Float,
+ /**
+ * Configuration for the screen point where the height morphing starts (item is touching this
+ * screen point with its bottom edge).
+ */
+ val growthStartScreenFraction: Float,
+
+ /**
+ * Configuration for the screen point where the height morphing ends and item is fully expanded
+ * (item is touching this screen point with its bottom edge).
+ */
+ val growthEndScreenFraction: Float,
) {
init {
// The element height range must be non-empty.
require(minElementHeight < maxElementHeight) {
"minElementHeight must be smaller than maxElementHeight"
}
+
+ // Morphing start point should be below the growth end.
+ require(growthEndScreenFraction < growthStartScreenFraction) {
+ "growthEndScreenFraction must be smaller than growthStartScreenFraction"
+ }
}
}
@@ -203,10 +214,8 @@
lerp(start.contentAlpha, stop.contentAlpha, progress),
lerp(start.scale, stop.scale, progress),
lerp(start.containerWidth, stop.containerWidth, progress),
- lerp(start.morphHeight, stop.morphHeight, progress),
- lerp(start.morphHeightDriftFactor, stop.morphHeightDriftFactor, progress),
- lerp(start.morphPivotX, stop.morphPivotX, progress),
- lerp(start.morphPivotY, stop.morphPivotY, progress)
+ lerp(start.growthStartScreenFraction, stop.growthStartScreenFraction, progress),
+ lerp(start.growthEndScreenFraction, stop.growthEndScreenFraction, progress),
)
/**
diff --git a/wear/compose/compose-material3/src/test/kotlin/androidx/wear/compose/material3/TransformatioSpecTest.kt b/wear/compose/compose-material3/src/test/kotlin/androidx/wear/compose/material3/TransformatioSpecTest.kt
deleted file mode 100644
index 8da26a2..0000000
--- a/wear/compose/compose-material3/src/test/kotlin/androidx/wear/compose/material3/TransformatioSpecTest.kt
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright 2024 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
- *
- * http://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 androidx.wear.compose.material3
-
-import androidx.compose.animation.core.CubicBezierEasing
-import androidx.wear.compose.material3.lazy.TransformVariableSpec
-import androidx.wear.compose.material3.lazy.TransformationSpec
-import androidx.wear.compose.material3.lazy.responsiveTransformationSpec
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class TransformatioSpecTest {
-
- private val SPEC1 =
- TransformationSpec(
- 0.01f,
- 0.02f,
- 0.03f,
- 0.04f,
- CubicBezierEasing(0.015f, 0.025f, 0.035f, 0.045f),
- TransformVariableSpec(0.011f, 0.021f, 0.031f, 0.041f),
- TransformVariableSpec(0.012f, 0.022f, 0.032f, 0.042f),
- TransformVariableSpec(0.013f, 0.023f, 0.033f, 0.043f),
- TransformVariableSpec(0.014f, 0.024f, 0.034f, 0.044f),
- 0.05f,
- 0.06f,
- 0.07f,
- 0.08f
- )
-
- private val SPEC2 =
- TransformationSpec(
- 0.1f,
- 0.2f,
- 0.3f,
- 0.4f,
- CubicBezierEasing(0.15f, 0.25f, 0.35f, 0.45f),
- TransformVariableSpec(0.11f, 0.21f, 0.31f, 0.41f),
- TransformVariableSpec(0.12f, 0.22f, 0.32f, 0.42f),
- TransformVariableSpec(0.13f, 0.23f, 0.33f, 0.43f),
- TransformVariableSpec(0.14f, 0.24f, 0.34f, 0.44f),
- 0.5f,
- 0.6f,
- 0.7f,
- 0.8f
- )
-
- private val SPECS = listOf(200 to SPEC1, 220 to SPEC2)
-
- @Test fun responsive_spec_coerced_to_min_screen_size() = check_responsive_spec(180, SPEC1)
-
- @Test fun responsive_spec_for_min_screen_size() = check_responsive_spec(200, SPEC1)
-
- @Test fun responsive_spec_for_max_screen_size() = check_responsive_spec(220, SPEC2)
-
- @Test fun responsive_spec_coerced_to_max_screen_size() = check_responsive_spec(221, SPEC2)
-
- @Test
- fun responsive_spec_for_mid_screen_size() {
- val spec = responsiveTransformationSpec(210, SPECS)
-
- assertEquals(0.055f, spec.minElementHeight, EPSILON)
- assertEquals(0.11f, spec.maxElementHeight, EPSILON)
- assertEquals(0.165f, spec.minTransitionArea, EPSILON)
- assertEquals(0.22f, spec.maxTransitionArea, EPSILON)
-
- assertEquals(0.066f, spec.contentAlpha.topValue, EPSILON)
- assertEquals(0.121f, spec.contentAlpha.bottomValue, EPSILON)
- assertEquals(0.176f, spec.contentAlpha.transformationZoneEnterFraction, EPSILON)
- assertEquals(0.231f, spec.contentAlpha.transformationZoneExitFraction, EPSILON)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun responsive_with_no_spec() {
- responsiveTransformationSpec(200, emptyList())
- }
-
- @Test
- fun responsive_with_one_spec() {
- val specs1 = listOf(200 to SPEC1)
-
- assertEquals(SPEC1, responsiveTransformationSpec(199, specs1))
- assertEquals(SPEC1, responsiveTransformationSpec(200, specs1))
- assertEquals(SPEC1, responsiveTransformationSpec(201, specs1))
- }
-
- @Test
- fun responsive_with_three_specs() {
- val specs3 =
- listOf(
- 100 to SPEC1, // 0.01f, 0.02f, 0.03f, 0.04f
- 200 to SPEC2, // 0.1f, 0.2f, 0.3f, 0.4f
- 300 to
- SPEC2.copy(
- minElementHeight = 0.5f,
- maxElementHeight = 0.6f,
- minTransitionArea = 0.7f,
- maxTransitionArea = 0.7f,
- )
- )
-
- assertEquals(SPEC1, responsiveTransformationSpec(100, specs3))
- assertEquals(0.11f, responsiveTransformationSpec(150, specs3).maxElementHeight, EPSILON)
- assertEquals(0.1f, responsiveTransformationSpec(200, specs3).minElementHeight, EPSILON)
- assertEquals(0.55f, responsiveTransformationSpec(250, specs3).maxTransitionArea, EPSILON)
- assertEquals(specs3.last().second, responsiveTransformationSpec(300, specs3))
- }
-
- @Test
- fun copy_overrides_transformation_area_configuration() {
- val spec =
- SPEC2.copy(
- minElementHeight = 0.51f,
- maxElementHeight = 0.52f,
- minTransitionArea = 0.53f,
- maxTransitionArea = 0.54f
- )
-
- assertEquals(0.51f, spec.minElementHeight)
- assertEquals(0.52f, spec.maxElementHeight)
- assertEquals(0.53f, spec.minTransitionArea)
- assertEquals(0.54f, spec.maxTransitionArea)
- assertEquals(spec.easing, SPEC2.easing)
- assertEquals(spec.scale, SPEC2.scale)
- assertEquals(spec.containerAlpha, SPEC2.containerAlpha)
- assertEquals(spec.contentAlpha, SPEC2.contentAlpha)
- }
-
- private val EPSILON = 1e-5f
-
- private fun check_responsive_spec(screenSize: Int, expectedSpec: TransformationSpec) {
- val spec = responsiveTransformationSpec(screenSize, SPECS)
- assertEquals(expectedSpec, spec)
- }
-}
diff --git a/wear/compose/compose-material3/src/test/kotlin/androidx/wear/compose/material3/TransformationSpecTest.kt b/wear/compose/compose-material3/src/test/kotlin/androidx/wear/compose/material3/TransformationSpecTest.kt
new file mode 100644
index 0000000..771846e
--- /dev/null
+++ b/wear/compose/compose-material3/src/test/kotlin/androidx/wear/compose/material3/TransformationSpecTest.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.wear.compose.material3
+
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.wear.compose.material3.lazy.TransformVariableSpec
+import androidx.wear.compose.material3.lazy.TransformationSpec
+import androidx.wear.compose.material3.lazy.responsiveTransformationSpec
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class TransformationSpecTest {
+
+ private val SPEC1 =
+ TransformationSpec(
+ 0.01f,
+ 0.02f,
+ 0.03f,
+ 0.04f,
+ CubicBezierEasing(0.015f, 0.025f, 0.035f, 0.045f),
+ TransformVariableSpec(0.011f, 0.021f, 0.031f, 0.041f),
+ TransformVariableSpec(0.012f, 0.022f, 0.032f, 0.042f),
+ TransformVariableSpec(0.013f, 0.023f, 0.033f, 0.043f),
+ TransformVariableSpec(0.014f, 0.024f, 0.034f, 0.044f),
+ 0.95f,
+ 0.86f,
+ )
+
+ private val SPEC2 =
+ TransformationSpec(
+ 0.1f,
+ 0.2f,
+ 0.3f,
+ 0.4f,
+ CubicBezierEasing(0.15f, 0.25f, 0.35f, 0.45f),
+ TransformVariableSpec(0.11f, 0.21f, 0.31f, 0.41f),
+ TransformVariableSpec(0.12f, 0.22f, 0.32f, 0.42f),
+ TransformVariableSpec(0.13f, 0.23f, 0.33f, 0.43f),
+ TransformVariableSpec(0.14f, 0.24f, 0.34f, 0.44f),
+ 0.95f,
+ 0.86f,
+ )
+
+ private val SPECS = listOf(200 to SPEC1, 220 to SPEC2)
+
+ @Test fun responsive_spec_coerced_to_min_screen_size() = check_responsive_spec(180, SPEC1)
+
+ @Test fun responsive_spec_for_min_screen_size() = check_responsive_spec(200, SPEC1)
+
+ @Test fun responsive_spec_for_max_screen_size() = check_responsive_spec(220, SPEC2)
+
+ @Test fun responsive_spec_coerced_to_max_screen_size() = check_responsive_spec(221, SPEC2)
+
+ @Test
+ fun responsive_spec_for_mid_screen_size() {
+ val spec = responsiveTransformationSpec(210, SPECS)
+
+ assertEquals(0.055f, spec.minElementHeight, EPSILON)
+ assertEquals(0.11f, spec.maxElementHeight, EPSILON)
+ assertEquals(0.165f, spec.minTransitionArea, EPSILON)
+ assertEquals(0.22f, spec.maxTransitionArea, EPSILON)
+
+ assertEquals(0.066f, spec.contentAlpha.topValue, EPSILON)
+ assertEquals(0.121f, spec.contentAlpha.bottomValue, EPSILON)
+ assertEquals(0.176f, spec.contentAlpha.transformationZoneEnterFraction, EPSILON)
+ assertEquals(0.231f, spec.contentAlpha.transformationZoneExitFraction, EPSILON)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun responsive_with_no_spec() {
+ responsiveTransformationSpec(200, emptyList())
+ }
+
+ @Test
+ fun responsive_with_one_spec() {
+ val specs1 = listOf(200 to SPEC1)
+
+ assertEquals(SPEC1, responsiveTransformationSpec(199, specs1))
+ assertEquals(SPEC1, responsiveTransformationSpec(200, specs1))
+ assertEquals(SPEC1, responsiveTransformationSpec(201, specs1))
+ }
+
+ @Test
+ fun responsive_with_three_specs() {
+ val specs3 =
+ listOf(
+ 100 to SPEC1, // 0.01f, 0.02f, 0.03f, 0.04f
+ 200 to SPEC2, // 0.1f, 0.2f, 0.3f, 0.4f
+ 300 to
+ SPEC2.copy(
+ minElementHeight = 0.5f,
+ maxElementHeight = 0.6f,
+ minTransitionArea = 0.7f,
+ maxTransitionArea = 0.7f,
+ )
+ )
+
+ assertEquals(SPEC1, responsiveTransformationSpec(100, specs3))
+ assertEquals(0.11f, responsiveTransformationSpec(150, specs3).maxElementHeight, EPSILON)
+ assertEquals(0.1f, responsiveTransformationSpec(200, specs3).minElementHeight, EPSILON)
+ assertEquals(0.55f, responsiveTransformationSpec(250, specs3).maxTransitionArea, EPSILON)
+ assertEquals(specs3.last().second, responsiveTransformationSpec(300, specs3))
+ }
+
+ @Test
+ fun copy_overrides_transformation_area_configuration() {
+ val spec =
+ SPEC2.copy(
+ minElementHeight = 0.51f,
+ maxElementHeight = 0.52f,
+ minTransitionArea = 0.53f,
+ maxTransitionArea = 0.54f
+ )
+
+ assertEquals(0.51f, spec.minElementHeight)
+ assertEquals(0.52f, spec.maxElementHeight)
+ assertEquals(0.53f, spec.minTransitionArea)
+ assertEquals(0.54f, spec.maxTransitionArea)
+ assertEquals(spec.easing, SPEC2.easing)
+ assertEquals(spec.scale, SPEC2.scale)
+ assertEquals(spec.containerAlpha, SPEC2.containerAlpha)
+ assertEquals(spec.contentAlpha, SPEC2.contentAlpha)
+ }
+
+ private val EPSILON = 1e-5f
+
+ private fun check_responsive_spec(screenSize: Int, expectedSpec: TransformationSpec) {
+ val spec = responsiveTransformationSpec(screenSize, SPECS)
+ assertEquals(expectedSpec, spec)
+ }
+}
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index df57a96..17c8635 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -26,8 +26,8 @@
defaultConfig {
applicationId = "androidx.wear.compose.integration.demos"
minSdk = 25
- versionCode = 58
- versionName = "1.58"
+ versionCode = 59
+ versionName = "1.59"
}
buildTypes {
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
index 9c2032d..bb846d3 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
@@ -16,7 +16,6 @@
package androidx.wear.compose.integration.demos
-import android.annotation.SuppressLint
import android.os.Build
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.getValue
@@ -95,7 +94,6 @@
import java.time.LocalDate
import java.time.LocalTime
-@SuppressLint("ClassVerificationFailure")
val WearMaterialDemos =
DemoCategory(
"Material",
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
index 5ef0915..223bcbe 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
@@ -16,7 +16,6 @@
package androidx.wear.compose.integration.demos
-import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.view.accessibility.AccessibilityManager
@@ -96,7 +95,6 @@
* @param modifier the modifiers for the `Box` containing the UI elements.
* @param time the initial value to seed the picker with.
*/
-@SuppressLint("ClassVerificationFailure")
@RequiresApi(Build.VERSION_CODES.O)
@Composable
public fun TimePicker(
@@ -270,7 +268,6 @@
* @param modifier the modifiers for the `Column` containing the UI elements.
* @param time the initial value to seed the picker with.
*/
-@SuppressLint("ClassVerificationFailure")
@RequiresApi(Build.VERSION_CODES.O)
@Composable
public fun TimePickerWith12HourClock(
@@ -471,7 +468,6 @@
* @param fromDate the minimum date to be selected in picker
* @param toDate the maximum date to be selected in picker
*/
-@SuppressLint("ClassVerificationFailure")
@RequiresApi(Build.VERSION_CODES.O)
@Composable
public fun DatePicker(
@@ -871,7 +867,6 @@
require(date in fromDate..toDate) { "date should lie between fromDate and toDate" }
}
-@SuppressLint("ClassVerificationFailure")
@RequiresApi(Build.VERSION_CODES.O)
private fun getMonthNames(pattern: String): List<String> {
val monthFormatter = DateTimeFormatter.ofPattern(pattern)
diff --git a/wear/compose/integration-tests/macrobenchmark-target/build.gradle b/wear/compose/integration-tests/macrobenchmark-target/build.gradle
index 25eb26b49..dbbe105 100644
--- a/wear/compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/wear/compose/integration-tests/macrobenchmark-target/build.gradle
@@ -44,10 +44,10 @@
implementation(project(":compose:ui:ui-tooling"))
implementation(project(":activity:activity-compose"))
implementation(project(":profileinstaller:profileinstaller"))
- implementation project(":wear:compose:compose-foundation")
- implementation project(":wear:compose:compose-material")
- implementation project(":wear:compose:compose-material3")
- implementation project(":wear:compose:compose-navigation")
+ implementation(project(":wear:compose:compose-foundation"))
+ implementation(project(":wear:compose:compose-material"))
+ implementation(project(":wear:compose:compose-material3"))
+ implementation(project(":wear:compose:compose-navigation"))
implementation(project(":compose:runtime:runtime-tracing"))
implementation(project(":tracing:tracing-perfetto"))
implementation(project(":tracing:tracing-perfetto-binary"))
diff --git a/wear/compose/integration-tests/macrobenchmark/build.gradle b/wear/compose/integration-tests/macrobenchmark/build.gradle
index 0ff8699..6526e1b 100644
--- a/wear/compose/integration-tests/macrobenchmark/build.gradle
+++ b/wear/compose/integration-tests/macrobenchmark/build.gradle
@@ -47,5 +47,5 @@
implementation(libs.testCore)
implementation(libs.testRunner)
implementation(libs.testUiautomator)
- implementation project(":wear:compose:compose-material")
+ implementation(project(":wear:compose:compose-material"))
}
diff --git a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/CompositionMetric.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/CompositionMetric.kt
index 25f237b..760ca99 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/CompositionMetric.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/CompositionMetric.kt
@@ -17,17 +17,16 @@
import androidx.benchmark.macro.ExperimentalMetricApi
import androidx.benchmark.macro.TraceMetric
-import androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi
-import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.traceprocessor.TraceProcessor
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.DurationUnit
@OptIn(ExperimentalMetricApi::class)
internal class CompositionMetric(private val composable: String) : TraceMetric() {
- @OptIn(ExperimentalMetricApi::class, ExperimentalPerfettoTraceProcessorApi::class)
+ @OptIn(ExperimentalMetricApi::class)
override fun getMeasurements(
captureInfo: CaptureInfo,
- traceSession: PerfettoTraceProcessor.Session
+ traceSession: TraceProcessor.Session
): List<Measurement> {
val shortName = composable.substringAfterLast(".")
diff --git a/wear/protolayout/protolayout-material/build.gradle b/wear/protolayout/protolayout-material/build.gradle
index 464a703..da521c6 100644
--- a/wear/protolayout/protolayout-material/build.gradle
+++ b/wear/protolayout/protolayout-material/build.gradle
@@ -67,9 +67,6 @@
defaultConfig {
minSdk = 26
}
- sourceSets {
- androidTest.assets.srcDirs += project.rootDir.absolutePath + "/../../golden/wear/wear-protolayout-material"
- }
namespace = "androidx.wear.protolayout.material"
}
@@ -78,4 +75,5 @@
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2023"
description = "Material components library for ProtoLayout."
+ addGoldenImageAssets()
}
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenTest.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenTest.java
index 7dd4feb..92472c0 100644
--- a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenTest.java
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenTest.java
@@ -21,6 +21,7 @@
import static androidx.wear.protolayout.material.RunnerUtils.convertToTestParameters;
import static androidx.wear.protolayout.material.RunnerUtils.runSingleScreenshotTest;
import static androidx.wear.protolayout.material.RunnerUtils.waitForNotificationToDisappears;
+import static androidx.wear.protolayout.material.ScreenshotKt.SCREENSHOT_GOLDEN_PATH;
import static androidx.wear.protolayout.material.TestCasesGenerator.generateTestCases;
import static androidx.wear.protolayout.material.TestCasesGenerator.generateTextTestCasesLtrOnly;
import static androidx.wear.protolayout.material.TestCasesGenerator.generateTextTestCasesRtlOnly;
@@ -53,7 +54,7 @@
@Rule
public AndroidXScreenshotTestRule mScreenshotRule =
- new AndroidXScreenshotTestRule("wear/wear-protolayout-material");
+ new AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH);
public MaterialGoldenTest(String expected, TestCase testCase) {
mTestCase = testCase;
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java
index 24dd65b..aafaea8 100644
--- a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java
@@ -23,6 +23,7 @@
import static androidx.wear.protolayout.material.RunnerUtils.runSingleScreenshotTest;
import static androidx.wear.protolayout.material.RunnerUtils.setFontScale;
import static androidx.wear.protolayout.material.RunnerUtils.waitForNotificationToDisappears;
+import static androidx.wear.protolayout.material.ScreenshotKt.SCREENSHOT_GOLDEN_PATH;
import static androidx.wear.protolayout.material.TestCasesGenerator.XXXL_SCALE_SUFFIX;
import static androidx.wear.protolayout.material.TestCasesGenerator.generateTestCases;
import static androidx.wear.protolayout.material.TestCasesGenerator.generateTextTestCasesLtrOnly;
@@ -63,7 +64,7 @@
@Rule
public AndroidXScreenshotTestRule mScreenshotRule =
- new AndroidXScreenshotTestRule("wear/wear-protolayout-material");
+ new AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH);
public MaterialGoldenXLTest(String expected, TestCase testCase) {
mTestCase = testCase;
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/Screenshot.kt b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/Screenshot.kt
new file mode 100644
index 0000000..0d86e36
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/Screenshot.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.wear.protolayout.material
+
+internal const val SCREENSHOT_GOLDEN_PATH = "wear/protolayout/protolayout-material"
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenTest.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenTest.java
index 9685084e..38df639 100644
--- a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenTest.java
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenTest.java
@@ -21,6 +21,7 @@
import static androidx.wear.protolayout.material.RunnerUtils.convertToTestParameters;
import static androidx.wear.protolayout.material.RunnerUtils.runSingleScreenshotTest;
import static androidx.wear.protolayout.material.RunnerUtils.waitForNotificationToDisappears;
+import static androidx.wear.protolayout.material.ScreenshotKt.SCREENSHOT_GOLDEN_PATH;
import static androidx.wear.protolayout.material.layouts.TestCasesGenerator.generateTestCases;
import android.content.Context;
@@ -51,7 +52,7 @@
@Rule
public AndroidXScreenshotTestRule mScreenshotRule =
- new AndroidXScreenshotTestRule("wear/wear-protolayout-material");
+ new AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH);
public LayoutsGoldenTest(String expected, TestCase testCase) {
mTestCase = testCase;
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java
index dc82303..a10bb47 100644
--- a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java
@@ -23,6 +23,7 @@
import static androidx.wear.protolayout.material.RunnerUtils.runSingleScreenshotTest;
import static androidx.wear.protolayout.material.RunnerUtils.setFontScale;
import static androidx.wear.protolayout.material.RunnerUtils.waitForNotificationToDisappears;
+import static androidx.wear.protolayout.material.ScreenshotKt.SCREENSHOT_GOLDEN_PATH;
import static androidx.wear.protolayout.material.layouts.TestCasesGenerator.XXXL_SCALE_SUFFIX;
import static androidx.wear.protolayout.material.layouts.TestCasesGenerator.generateTestCases;
@@ -59,7 +60,7 @@
@Rule
public AndroidXScreenshotTestRule mScreenshotRule =
- new AndroidXScreenshotTestRule("wear/wear-protolayout-material");
+ new AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH);
public LayoutsGoldenXLTest(String expected, TestCase testCase) {
mTestCase = testCase;
diff --git a/wear/protolayout/protolayout-material3/build.gradle b/wear/protolayout/protolayout-material3/build.gradle
index e6c36ce..f10a7fb 100644
--- a/wear/protolayout/protolayout-material3/build.gradle
+++ b/wear/protolayout/protolayout-material3/build.gradle
@@ -71,10 +71,6 @@
defaultConfig {
minSdk = 26
}
-
- sourceSets {
- androidTest.assets.srcDirs += project.rootDir.absolutePath + "/../../golden/wear/wear-protolayout-material3"
- }
namespace = "androidx.wear.protolayout.material3"
}
@@ -85,4 +81,5 @@
inceptionYear = "2024"
description = "Material3 components library for ProtoLayout."
samples(project(":wear:protolayout:protolayout-material3-samples"))
+ addGoldenImageAssets()
}
diff --git a/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/MaterialGoldenTest.kt b/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/MaterialGoldenTest.kt
index 50f3ef6..12a274b 100644
--- a/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/MaterialGoldenTest.kt
+++ b/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/MaterialGoldenTest.kt
@@ -30,7 +30,7 @@
@JvmField
@Rule
var mScreenshotRule: AndroidXScreenshotTestRule =
- AndroidXScreenshotTestRule("wear/wear-protolayout-material3")
+ AndroidXScreenshotTestRule("wear/protolayout/protolayout-material3")
@Test
fun test() {
diff --git a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt
new file mode 100644
index 0000000..298260a9
--- /dev/null
+++ b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.wear.protolayout.material3
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.wear.protolayout.ActionBuilders.LaunchAction
+import androidx.wear.protolayout.DeviceParametersBuilders
+import androidx.wear.protolayout.LayoutElementBuilders.Image
+import androidx.wear.protolayout.ModifiersBuilders.Clickable
+import androidx.wear.protolayout.TypeBuilders.StringProp
+import androidx.wear.protolayout.expression.AppDataKey
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32
+import androidx.wear.protolayout.material3.EdgeButtonDefaults.BOTTOM_MARGIN_DP
+import androidx.wear.protolayout.material3.EdgeButtonDefaults.EDGE_BUTTON_HEIGHT_DP
+import androidx.wear.protolayout.testing.LayoutElementAssertionsProvider
+import androidx.wear.protolayout.testing.LayoutElementMatcher
+import androidx.wear.protolayout.testing.hasColor
+import androidx.wear.protolayout.testing.hasContentDescription
+import androidx.wear.protolayout.testing.hasHeight
+import androidx.wear.protolayout.testing.hasImage
+import androidx.wear.protolayout.testing.hasText
+import androidx.wear.protolayout.testing.hasWidth
+import androidx.wear.protolayout.testing.isClickable
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(AndroidJUnit4::class)
+@DoNotInstrument
+class EdgeButtonTest {
+ @Test
+ fun containerSize() {
+ LayoutElementAssertionsProvider(ICON_EDGE_BUTTON)
+ .onRoot()
+ .assert(hasWidth(DEVICE_CONFIGURATION.screenWidthDp.toDp()))
+ .assert(hasHeight((EDGE_BUTTON_HEIGHT_DP + BOTTOM_MARGIN_DP).toDp()))
+ }
+
+ @Test
+ fun visibleHeight() {
+ LayoutElementAssertionsProvider(ICON_EDGE_BUTTON)
+ .onElement(isClickable())
+ .assert(hasHeight(EDGE_BUTTON_HEIGHT_DP.toDp()))
+ }
+
+ @Test
+ fun contentDescription() {
+ LayoutElementAssertionsProvider(ICON_EDGE_BUTTON)
+ .onElement(isClickable())
+ .assert(hasContentDescription(CONTENT_DESCRIPTION.value))
+ }
+
+ @Test
+ fun defaultBackgroundColor() {
+ LayoutElementAssertionsProvider(ICON_EDGE_BUTTON)
+ .onElement(isClickable())
+ .assert(hasColor(COLOR_SCHEME.primary.argb))
+ }
+
+ @Test
+ fun icon() {
+ LayoutElementAssertionsProvider(ICON_EDGE_BUTTON).onElement(hasImage(RES_ID)).assertExists()
+ }
+
+ @Test
+ fun iconColor() {
+ LayoutElementAssertionsProvider(ICON_EDGE_BUTTON)
+ .onElement(hasImage(RES_ID))
+ .assert(hasColor(COLOR_SCHEME.onPrimary.argb))
+ }
+
+ @Test
+ fun customColors() {
+ val queryProvider =
+ LayoutElementAssertionsProvider(
+ materialScope(CONTEXT, DEVICE_CONFIGURATION, allowDynamicTheme = false) {
+ iconEdgeButton(
+ onClick = CLICKABLE,
+ contentDescription = CONTENT_DESCRIPTION,
+ colors =
+ EdgeButtonColors(
+ COLOR_SCHEME.tertiaryContainer,
+ COLOR_SCHEME.onTertiary
+ ),
+ ) {
+ icon(RES_ID)
+ }
+ }
+ )
+
+ queryProvider.onElement(isClickable()).assert(hasColor(COLOR_SCHEME.tertiaryContainer.argb))
+ queryProvider
+ .onElement(LayoutElementMatcher("Element type is Image") { it is Image })
+ .assert(hasColor(COLOR_SCHEME.onTertiary.argb))
+ }
+
+ @Test
+ fun staticText() {
+ val label = "static text"
+ val textEdgeButton =
+ materialScope(CONTEXT, DEVICE_CONFIGURATION, allowDynamicTheme = false) {
+ textEdgeButton(onClick = CLICKABLE, contentDescription = CONTENT_DESCRIPTION) {
+ text(label.prop())
+ }
+ }
+
+ LayoutElementAssertionsProvider(textEdgeButton)
+ .onElement(hasText(label))
+ .assert(hasColor(COLOR_SCHEME.onPrimary.argb))
+ }
+
+ @Test
+ fun dynamicText() {
+ val label = "test text"
+ val stateKey = AppDataKey<DynamicInt32>("testKey")
+ val dynamicLabel =
+ StringProp.Builder(label)
+ .setDynamicValue(DynamicInt32.from(stateKey).times(2).format())
+ .build()
+
+ val queryProvider =
+ LayoutElementAssertionsProvider(
+ materialScope(CONTEXT, DEVICE_CONFIGURATION, allowDynamicTheme = false) {
+ textEdgeButton(onClick = CLICKABLE, contentDescription = CONTENT_DESCRIPTION) {
+ text(dynamicLabel)
+ }
+ }
+ )
+
+ queryProvider.onElement(hasText(dynamicLabel)).assert(hasColor(COLOR_SCHEME.onPrimary.argb))
+ }
+
+ companion object {
+ private val CONTEXT = getApplicationContext() as Context
+ private val COLOR_SCHEME = ColorScheme()
+
+ private val DEVICE_CONFIGURATION =
+ DeviceParametersBuilders.DeviceParameters.Builder()
+ .setScreenWidthDp(192)
+ .setScreenHeightDp(192)
+ .build()
+
+ private val CLICKABLE =
+ Clickable.Builder()
+ .setOnClick(LaunchAction.Builder().build())
+ .setId("action_id")
+ .build()
+
+ private val CONTENT_DESCRIPTION = "it is an edge button".prop()
+
+ private val RES_ID = "resId"
+ private val ICON_EDGE_BUTTON =
+ materialScope(CONTEXT, DEVICE_CONFIGURATION, allowDynamicTheme = false) {
+ iconEdgeButton(onClick = CLICKABLE, contentDescription = CONTENT_DESCRIPTION) {
+ icon(RES_ID)
+ }
+ }
+ }
+}
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/layout.proto b/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
index 67b8f26..a81a412 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
@@ -736,16 +736,16 @@
Shadow shadow = 2;
}
-// A dashed arc line that can be placed in an Arc container. It is an arc line made up of arc line
-// segments separated by gaps.
+// A dashed arc line. It is an arc line made up of arc line segments separated by gaps, and can be
+// placed in an Arc container.
message DashedArcLine {
// The length of this line in degrees, including gaps. <setter> If not defined,
// defaults to 0.</setter>
//
- // When using a dynamic value, make sure to specify the bounding constraints
+ // <setter>When using a dynamic value, make sure to specify the bounding constraints
// for the affected layout element through {@code
// setLayoutConstraintsForDynamicLength(AngularLayoutConstraint)} otherwise
- // {@code build()} fails.
+ // {@code build()} fails.<setter>
DegreesProp length = 1;
// The thickness of this line. <setter> If not defined, defaults to 0.</setter>
@@ -757,8 +757,8 @@
// Modifiers for this element.
ArcModifiers modifiers = 4;
- // The direction in which this line is drawn.<setter> If not set, defaults to
- // ARC_DIRECTION_CLOCKWISE.</setter>
+ // The direction in which this line is drawn.<setter> If not set, defaults to the Arc container's
+ // direction where it is placed in.</setter>
ArcDirectionProp arc_direction = 5;
// The dashed line pattern which describes how the arc line is segmented by gaps.
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ArcWidgetHelper.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ArcWidgetHelper.java
new file mode 100644
index 0000000..d39ba13b
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ArcWidgetHelper.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.wear.protolayout.renderer.inflater;
+
+import static androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.isRtlLayoutDirectionFromLocale;
+import static java.lang.Math.min;
+
+import android.view.View;
+import androidx.wear.protolayout.proto.LayoutElementProto.ArcDirection;
+
+import org.jspecify.annotations.NonNull;
+
+final class ArcWidgetHelper {
+ /**
+ * Returns true when the given point is inside the arc. In particular, the coordinates should be
+ * considered as if the arc was drawn centered at the default angle (12 o clock).
+ */
+ // This is Copy from
+ // https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:wear/wear/src/main/java/androidx/wear/widget/CurvedTextView.java;l=248;bpv=1;bpt=0;drc=03209658917989bcc65d389ee19f83ec6a53174e
+ static boolean isPointInsideArcArea(
+ View view, float x, float y, float arcWidth, float arcSweepAngle) {
+ float radius2 = min(view.getWidth(), view.getHeight()) / 2f - view.getPaddingTop();
+ float radius1 = radius2 - arcWidth;
+
+ float dx = x - view.getWidth() / 2f;
+ float dy = y - view.getHeight() / 2f;
+
+ float r2 = dx * dx + dy * dy;
+ if (r2 < radius1 * radius1 || r2 > radius2 * radius2) {
+ return false;
+ }
+
+ // Since we are symmetrical on the Y-axis, we can constrain the angle to the x>=0 quadrants.
+ float angle = (float) Math.toDegrees(Math.atan2(Math.abs(dx), -dy));
+ return angle < arcSweepAngle / 2;
+ }
+
+ static int getSignForClockwise(
+ @NonNull View view, @NonNull ArcDirection arcDirection, int defaultValue) {
+ switch (arcDirection) {
+ case ARC_DIRECTION_CLOCKWISE:
+ return 1;
+ case ARC_DIRECTION_COUNTER_CLOCKWISE:
+ return -1;
+ case ARC_DIRECTION_NORMAL:
+ return isRtlLayoutDirectionFromLocale() ? -1 : 1;
+ case UNRECOGNIZED:
+ return view.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? 1 : -1;
+ }
+ return defaultValue;
+ }
+
+ private ArcWidgetHelper() {}
+}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
index 8e799ce..1262b78 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
@@ -131,6 +131,8 @@
import androidx.wear.protolayout.proto.LayoutElementProto.Box;
import androidx.wear.protolayout.proto.LayoutElementProto.Column;
import androidx.wear.protolayout.proto.LayoutElementProto.ContentScaleMode;
+import androidx.wear.protolayout.proto.LayoutElementProto.DashedArcLine;
+import androidx.wear.protolayout.proto.LayoutElementProto.DashedLinePattern;
import androidx.wear.protolayout.proto.LayoutElementProto.ExtensionLayoutElement;
import androidx.wear.protolayout.proto.LayoutElementProto.FontFeatureSetting;
import androidx.wear.protolayout.proto.LayoutElementProto.FontSetting;
@@ -191,6 +193,7 @@
import androidx.wear.protolayout.renderer.common.ProviderStatsLogger.InflaterStatsLogger;
import androidx.wear.protolayout.renderer.common.RenderingArtifact;
import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline;
+import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline.PipelineMaker;
import androidx.wear.protolayout.renderer.inflater.RenderedMetadata.LayoutInfo;
import androidx.wear.protolayout.renderer.inflater.RenderedMetadata.LinearLayoutProperties;
import androidx.wear.protolayout.renderer.inflater.RenderedMetadata.PendingFrameLayoutParams;
@@ -350,12 +353,12 @@
public static final class InflateResult {
public final ViewGroup inflateParent;
public final View firstChild;
- private final Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> mPipelineMaker;
+ private final Optional<PipelineMaker> mPipelineMaker;
InflateResult(
ViewGroup inflateParent,
View firstChild,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
this.inflateParent = inflateParent;
this.firstChild = firstChild;
this.mPipelineMaker = pipelineMaker;
@@ -379,13 +382,13 @@
final List<InflatedView> mInflatedViews;
final RenderedMetadata mRenderedMetadataAfterMutation;
final NodeFingerprint mPreMutationRootNodeFingerprint;
- final Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> mPipelineMaker;
+ final Optional<PipelineMaker> mPipelineMaker;
ViewGroupMutation(
List<InflatedView> inflatedViews,
RenderedMetadata renderedMetadataAfterMutation,
NodeFingerprint preMutationRootNodeFingerprint,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
this.mInflatedViews = inflatedViews;
this.mRenderedMetadataAfterMutation = renderedMetadataAfterMutation;
this.mPreMutationRootNodeFingerprint = preMutationRootNodeFingerprint;
@@ -1245,7 +1248,7 @@
FontStyle style,
TextView textView,
String posId,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker,
+ Optional<PipelineMaker> pipelineMaker,
boolean isAutoSizeAllowed) {
// Note: Underline must be applied as a Span to work correctly (as opposed to using
// TextPaint#setTextUnderline). This is applied in the caller instead.
@@ -1705,7 +1708,7 @@
@NonNull View view,
@NonNull Transformation transformation,
@NonNull String posId,
- @NonNull Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ @NonNull Optional<PipelineMaker> pipelineMaker) {
// In a composite transformation, the order of applying the individual transformations
// does not affect the result, as Android view does the transformation in fixed order by
// first scale, then rotate then translate.
@@ -1786,7 +1789,7 @@
Consumer<Float> consumerOffsetDp,
Consumer<Float> consumerLocationRatio,
@NonNull String posId,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
switch (pivotDimension.getInnerCase()) {
case OFFSET_DP:
DpProp offset = pivotDimension.getOffsetDp();
@@ -1819,7 +1822,7 @@
@Nullable View wrapper, // The wrapper view for layout sizing, if any
@NonNull Modifiers modifiers,
@NonNull String posId,
- @NonNull Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ @NonNull Optional<PipelineMaker> pipelineMaker) {
if (modifiers.hasVisible()) {
applyVisible(
view,
@@ -1896,7 +1899,7 @@
View view,
BoolProp visible,
String posId,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker,
+ Optional<PipelineMaker> pipelineMaker,
Function<Boolean, Integer> toViewVisibility) {
handleProp(
visible,
@@ -2359,7 +2362,7 @@
String rowPosId,
boolean includeChildren,
LayoutInfo.Builder layoutInfoBuilder,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
ContainerDimension width = row.hasWidth() ? row.getWidth() : CONTAINER_DIMENSION_DEFAULT;
ContainerDimension height = row.hasHeight() ? row.getHeight() : CONTAINER_DIMENSION_DEFAULT;
@@ -2418,7 +2421,7 @@
String boxPosId,
boolean includeChildren,
LayoutInfo.Builder layoutInfoBuilder,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
ContainerDimension width = box.hasWidth() ? box.getWidth() : CONTAINER_DIMENSION_DEFAULT;
ContainerDimension height = box.hasHeight() ? box.getHeight() : CONTAINER_DIMENSION_DEFAULT;
@@ -2717,8 +2720,8 @@
ParentViewWrapper parentViewWrapper,
ArcSpacer spacer,
String posId,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
- float lengthDegrees = 0;
+ Optional<PipelineMaker> pipelineMaker) {
+ float length = 0;
int thicknessPx = safeDpToPx(spacer.getThickness());
WearCurvedSpacer space = new WearCurvedSpacer(mUiContext);
ArcLayout.LayoutParams layoutParams =
@@ -2728,7 +2731,8 @@
final AngularDimension angularLength = spacer.getAngularLength();
switch (angularLength.getInnerCase()) {
case DEGREES:
- lengthDegrees = max(0, angularLength.getDegrees().getValue());
+ length = max(0, angularLength.getDegrees().getValue());
+ space.setSweepAngleDegrees(length);
break;
case EXPANDED_ANGULAR_DIMENSION:
@@ -2758,20 +2762,21 @@
}
case DP:
- // TODO: b/377325905 - ArcSpacer accepts Dp length.
+ length = max(0, safeDpToPx(angularLength.getDp().getValue()));
+ space.setLengthPx(length);
break;
case INNER_NOT_SET:
break;
}
} else {
- lengthDegrees = max(0, spacer.getLength().getValue());
+ length = max(0, spacer.getLength().getValue());
+ space.setSweepAngleDegrees(length);
}
- if (lengthDegrees == 0 && thicknessPx == 0) {
+ if (length == 0 && thicknessPx == 0) {
return null;
}
- space.setSweepAngleDegrees(lengthDegrees);
space.setThickness(thicknessPx);
View wrappedView =
@@ -2809,7 +2814,7 @@
ParentViewWrapper parentViewWrapper,
Text text,
String posId,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
TextView textView = newThemedTextView();
LayoutParams layoutParams = generateDefaultLayoutParams();
@@ -3018,7 +3023,7 @@
ParentViewWrapper parentViewWrapper,
ArcText text,
String posId,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
CurvedTextView textView = newThemedCurvedTextView();
LayoutParams layoutParams = generateDefaultLayoutParams();
@@ -3325,7 +3330,7 @@
ParentViewWrapper parentViewWrapper,
ArcLine line,
String posId,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
float lengthDegrees = 0;
if (line.hasAngularLength()) {
if (line.getAngularLength().getInnerCase() == ArcLineLength.InnerCase.DEGREES) {
@@ -3346,13 +3351,6 @@
try {
lineView.setUpdatesEnabled(false);
- // A ArcLineView must always be the same width/height as its parent, so it can draw the
- // line properly inside of those bounds.
- ArcLayout.LayoutParams layoutParams =
- new ArcLayout.LayoutParams(generateDefaultLayoutParams());
- layoutParams.width = LayoutParams.MATCH_PARENT;
- layoutParams.height = LayoutParams.MATCH_PARENT;
-
if (line.hasBrush()) {
lineView.setBrush(line.getBrush());
} else if (line.hasColor()) {
@@ -3392,6 +3390,7 @@
DegreesProp length = DegreesProp.getDefaultInstance();
+ float arcLayoutWeight = 0;
if (line.hasAngularLength()) {
final ArcLineLength angularLength = line.getAngularLength();
switch (angularLength.getInnerCase()) {
@@ -3405,10 +3404,10 @@
{
ExpandedAngularDimensionProp expandedAngularDimension =
angularLength.getExpandedAngularDimension();
- layoutParams.setWeight(
+ arcLayoutWeight =
expandedAngularDimension.hasLayoutWeight()
? expandedAngularDimension.getLayoutWeight().getValue()
- : 1.0f);
+ : 1.0f;
length = DegreesProp.getDefaultInstance();
break;
}
@@ -3427,54 +3426,130 @@
lineView.setLineDirection(arcLineDirection);
- SizedArcContainer sizeWrapper = null;
- SizedArcContainer.LayoutParams sizedLp =
- new SizedArcContainer.LayoutParams(
- LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
- Float sizeForLayout = resolveSizeForLayoutIfNeeded(length);
+ @Nullable Float sizeForLayout = resolveSizeForLayoutIfNeeded(length);
if (sizeForLayout != null) {
- sizeWrapper = new SizedArcContainer(mUiContext);
- sizeWrapper.setArcDirection(arcLineDirection);
- if (sizeForLayout <= 0f) {
- Log.w(
- TAG,
- "ArcLine length's value_for_layout is not a positive value. Element"
- + " won't be visible.");
- }
- sizeWrapper.setSweepAngleDegrees(sizeForLayout);
- sizedLp.setAngularAlignment(
- angularAlignmentProtoToAngularAlignment(
- length.getAngularAlignmentForLayout()));
-
- // Also clamp the line to that angle...
lineView.setMaxSweepAngleDegrees(sizeForLayout);
}
-
View wrappedView =
- applyModifiersToArcLayoutView(
- lineView, line.getModifiers(), posId, pipelineMaker);
-
- if (sizeWrapper != null) {
- sizeWrapper.addView(wrappedView, sizedLp);
- parentViewWrapper.maybeAddView(sizeWrapper, layoutParams);
- return new InflatedView(
- sizeWrapper,
- parentViewWrapper
- .getParentProperties()
- .applyPendingChildLayoutParams(layoutParams));
- } else {
- parentViewWrapper.maybeAddView(wrappedView, layoutParams);
- return new InflatedView(
- wrappedView,
- parentViewWrapper
- .getParentProperties()
- .applyPendingChildLayoutParams(layoutParams));
- }
+ applyModifiersToArcLayoutView(lineView, line.getModifiers(), posId,
+ pipelineMaker);
+ return addLineViewToParentArc(
+ parentViewWrapper,
+ wrappedView,
+ sizeForLayout,
+ arcLineDirection,
+ angularAlignmentProtoToAngularAlignment(length.getAngularAlignmentForLayout()),
+ arcLayoutWeight);
} finally {
lineView.setUpdatesEnabled(true);
}
}
+ private @Nullable InflatedView inflateDashedArcLine(
+ @NonNull ParentViewWrapper parentViewWrapper,
+ @NonNull DashedArcLine dashedLine,
+ @NonNull String posId,
+ @NonNull Optional<PipelineMaker> pipelineMaker) {
+ float lengthDegrees = max(0, dashedLine.getLength().getValue());
+ int thicknessPx = safeDpToPx(dashedLine.getThickness());
+ if (lengthDegrees == 0 && thicknessPx == 0) {
+ return null;
+ }
+
+ WearDashedArcLineView dashedLineView = new WearDashedArcLineView(mUiContext);
+ dashedLineView.setThickness(thicknessPx);
+
+ if (dashedLine.hasColor()) {
+ handleProp(dashedLine.getColor(), dashedLineView::setColor, posId, pipelineMaker);
+ } else {
+ dashedLineView.setColor(LINE_COLOR_DEFAULT);
+ }
+
+ if (dashedLine.hasLinePattern()) {
+ DashedLinePattern linePattern = dashedLine.getLinePattern();
+ if (linePattern.getGapLocationsCount() > 0) {
+ List<Float> gapLocations = new ArrayList<>();
+ for (DegreesProp degree : linePattern.getGapLocationsList()) {
+ gapLocations.add(degree.getValue());
+ }
+ dashedLineView.setGapLocations(gapLocations);
+ }
+ dashedLineView.setGapSize(safeDpToPx(linePattern.getGapSize()));
+ }
+
+ ArcDirection arcLineDirection =
+ dashedLine.hasArcDirection()
+ ? dashedLine.getArcDirection().getValue()
+ : ArcDirection.UNRECOGNIZED;
+ dashedLineView.setLineDirection(arcLineDirection);
+
+ DegreesProp length = dashedLine.getLength();
+ handleProp(length, dashedLineView::setLineSweepAngleDegrees, posId, pipelineMaker);
+
+ @Nullable Float sizeForLayout = resolveSizeForLayoutIfNeeded(length);
+ if (sizeForLayout != null) {
+ dashedLineView.setMaxSweepAngleDegrees(sizeForLayout);
+ }
+ View wrappedView =
+ applyModifiersToArcLayoutView(
+ dashedLineView, dashedLine.getModifiers(), posId, pipelineMaker);
+ return addLineViewToParentArc(
+ parentViewWrapper,
+ wrappedView,
+ sizeForLayout,
+ arcLineDirection,
+ angularAlignmentProtoToAngularAlignment(length.getAngularAlignmentForLayout()),
+ /* arcLayoutWeight= */ 0
+ // Zero weight in ArcLayout means the view should not be stretched.
+ );
+ }
+
+ private InflatedView addLineViewToParentArc(
+ @NonNull ParentViewWrapper parentArc,
+ @NonNull View lineView,
+ @Nullable Float sizeForLayout,
+ @NonNull ArcDirection arcLineDirection,
+ @SizedArcContainer.LayoutParams.AngularAlignment int angularAlignment,
+ float arcLayoutWeight) {
+ SizedArcContainer sizeWrapper = null;
+ SizedArcContainer.LayoutParams sizedLayoutParams =
+ new SizedArcContainer.LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ if (sizeForLayout != null) {
+ sizeWrapper = new SizedArcContainer(mUiContext);
+ sizeWrapper.setArcDirection(arcLineDirection);
+ if (sizeForLayout <= 0f) {
+ Log.w(
+ TAG,
+ "Arc Line length's value_for_layout is not a positive value. Element won't"
+ + " be visible.");
+ }
+ sizeWrapper.setSweepAngleDegrees(sizeForLayout);
+ sizedLayoutParams.setAngularAlignment(angularAlignment);
+ }
+
+ // A WearDashedArcLineView or WearCurvedLineView must always be the same width/height as its
+ // parent, so it can draw the line properly inside of those bounds.
+ ArcLayout.LayoutParams layoutParams = new ArcLayout.LayoutParams(
+ generateDefaultLayoutParams());
+ layoutParams.width = LayoutParams.MATCH_PARENT;
+ layoutParams.height = LayoutParams.MATCH_PARENT;
+ layoutParams.setWeight(arcLayoutWeight);
+
+ if (sizeWrapper != null) {
+ sizeWrapper.addView(lineView, sizedLayoutParams);
+ parentArc.maybeAddView(sizeWrapper, layoutParams);
+ return new InflatedView(
+ sizeWrapper,
+ parentArc.getParentProperties().applyPendingChildLayoutParams(layoutParams));
+ } else {
+ parentArc.maybeAddView(lineView, layoutParams);
+ return new InflatedView(
+ lineView,
+ parentArc.getParentProperties().applyPendingChildLayoutParams(layoutParams));
+ }
+ }
+
// dereference of possibly-null reference childLayoutParams
@SuppressWarnings("nullness:dereference.of.nullable")
private @Nullable InflatedView inflateArc(
@@ -3483,7 +3558,7 @@
String arcPosId,
boolean includeChildren,
LayoutInfo.Builder layoutInfoBuilder,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
ArcLayout arcLayout = new ArcLayout(mUiContext);
int anchorAngleSign = 1;
@@ -3929,7 +4004,10 @@
break;
case DASHED_LINE:
- // TODO: b/360314390 - inflate a dashed arc here with WearDashedArcLineView
+ inflatedView =
+ inflateDashedArcLine(
+ parentViewWrapper, element.getDashedLine(), nodePosId,
+ pipelineMaker);
break;
case INNER_NOT_SET:
@@ -4001,7 +4079,7 @@
String nodePosId,
boolean includeChildren,
LayoutInfo.Builder layoutInfoBuilder,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
InflatedView inflatedView = null;
// What is it?
switch (element.getInnerCase()) {
@@ -4162,7 +4240,7 @@
StringProp stringProp,
Consumer<String> consumer,
String posId,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
if (stringProp.hasDynamicValue() && pipelineMaker.isPresent()) {
try {
pipelineMaker
@@ -4186,7 +4264,7 @@
DegreesProp degreesProp,
Consumer<Float> consumer,
String posId,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
if (degreesProp.hasDynamicValue() && pipelineMaker.isPresent()) {
try {
pipelineMaker
@@ -4205,7 +4283,7 @@
DpProp dpProp,
Consumer<Float> consumer,
String posId,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
handleProp(dpProp, consumer, consumer, posId, pipelineMaker);
}
@@ -4214,7 +4292,7 @@
Consumer<Float> staticValueConsumer,
Consumer<Float> dynamicValueConsumer,
String posId,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
if (dpProp.hasDynamicValue() && pipelineMaker.isPresent()) {
try {
pipelineMaker
@@ -4233,7 +4311,7 @@
ColorProp colorProp,
Consumer<Integer> consumer,
String posId,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
if (colorProp.hasDynamicValue() && pipelineMaker.isPresent()) {
try {
pipelineMaker.get().addPipelineFor(colorProp, colorProp.getArgb(), posId, consumer);
@@ -4250,7 +4328,7 @@
BoolProp boolProp,
Consumer<Boolean> consumer,
String posId,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
if (boolProp.hasDynamicValue() && pipelineMaker.isPresent()) {
try {
pipelineMaker.get().addPipelineFor(boolProp, boolProp.getValue(), posId, consumer);
@@ -4267,7 +4345,7 @@
FloatProp floatProp,
Consumer<Float> consumer,
String posId,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
handleProp(floatProp, consumer, consumer, posId, pipelineMaker);
}
@@ -4276,7 +4354,7 @@
Consumer<Float> staticValueConsumer,
Consumer<Float> dynamicValueconsumer,
String posId,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
if (floatProp.hasDynamicValue() && pipelineMaker.isPresent()) {
try {
pipelineMaker
@@ -4499,7 +4577,7 @@
List<LayoutElement> childElements,
String parentPosId,
LayoutInfo.Builder layoutInfoBuilder,
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
+ Optional<PipelineMaker> pipelineMaker) {
int index = FIRST_CHILD_INDEX;
for (LayoutElement childElement : childElements) {
String childPosId = ProtoLayoutDiffer.createNodePosId(parentPosId, index++);
@@ -4595,7 +4673,7 @@
LayoutInfo.Builder layoutInfoBuilder =
new LayoutInfo.Builder(prevRenderedMetadata.getLayoutInfo());
LayoutInfo prevLayoutInfo = prevRenderedMetadata.getLayoutInfo();
- Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker =
+ Optional<PipelineMaker> pipelineMaker =
mDataPipeline.map(
p ->
p.newPipelineMaker(
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/SizedArcContainer.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/SizedArcContainer.java
index b0f4843..b75615f 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/SizedArcContainer.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/SizedArcContainer.java
@@ -16,7 +16,7 @@
package androidx.wear.protolayout.renderer.inflater;
-import static androidx.wear.protolayout.renderer.inflater.WearCurvedLineView.getSignForClockwise;
+import static androidx.wear.protolayout.renderer.inflater.ArcWidgetHelper.getSignForClockwise;
import android.content.Context;
import android.content.res.TypedArray;
@@ -244,7 +244,7 @@
float childSweep = ((ArcLayout.Widget) child).getSweepAngleDegrees();
float offsetDegrees = (mSweepAngleDegrees - childSweep) / 2;
- int sign = getSignForClockwise(mArcDirection, /* defaultValue= */ 1);
+ int sign = getSignForClockwise(this, mArcDirection, /* defaultValue= */ 1);
switch (alignment) {
case LayoutParams.ANGULAR_ALIGNMENT_START:
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java
index dd5da33..c74813f 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java
@@ -20,7 +20,6 @@
import static java.lang.Math.max;
-import android.annotation.SuppressLint;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Build.VERSION;
@@ -126,7 +125,6 @@
return eventForwarded;
}
- @SuppressLint("ClassVerificationFailure")
@Override
public AccessibilityNodeInfo.@NonNull TouchDelegateInfo getTouchDelegateInfo() {
if (VERSION.SDK_INT >= VERSION_CODES.Q && !mDelegates.isEmpty()) {
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearCurvedLineView.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearCurvedLineView.java
index bb019b0..367acba 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearCurvedLineView.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearCurvedLineView.java
@@ -17,6 +17,8 @@
package androidx.wear.protolayout.renderer.inflater;
import static androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.isRtlLayoutDirectionFromLocale;
+import static androidx.wear.protolayout.renderer.inflater.ArcWidgetHelper.getSignForClockwise;
+import static androidx.wear.protolayout.renderer.inflater.ArcWidgetHelper.isPointInsideArcArea;
import static java.lang.Math.min;
@@ -183,7 +185,7 @@
bounds,
clampedSweepAngle,
mBasePaint.getStrokeWidth(),
- getSignForClockwise(mLineDirection, /* defaultValue= */ 1),
+ getSignForClockwise(this, mLineDirection, /* defaultValue= */ 1),
mBasePaint,
mSweepGradientHelper,
mCapShadow);
@@ -338,35 +340,8 @@
@Override
public boolean isPointInsideClickArea(float x, float y) {
- // Stolen from WearCurvedTextView...
- float radius2 = min(getWidth(), getHeight()) / 2f - getPaddingTop();
- float radius1 = radius2 - mBasePaint.getStrokeWidth();
-
- float dx = x - getWidth() / 2f;
- float dy = y - getHeight() / 2f;
-
- float r2 = dx * dx + dy * dy;
- if (r2 < radius1 * radius1 || r2 > radius2 * radius2) {
- return false;
- }
-
- // Since we are symmetrical on the Y-axis, we can constrain the angle to the x>=0 quadrants.
- float angle = (float) Math.toDegrees(Math.atan2(Math.abs(dx), -dy));
- return angle < resolveSweepAngleDegrees() / 2;
- }
-
- static int getSignForClockwise(@NonNull ArcDirection arcDirection, int defaultValue) {
- switch (arcDirection) {
- case ARC_DIRECTION_CLOCKWISE:
- return 1;
- case ARC_DIRECTION_COUNTER_CLOCKWISE:
- return -1;
- case ARC_DIRECTION_NORMAL:
- return isRtlLayoutDirectionFromLocale() ? -1 : 1;
- case UNRECOGNIZED:
- return defaultValue;
- }
- return defaultValue;
+ return isPointInsideArcArea(
+ this, x, y, mBasePaint.getStrokeWidth(), resolveSweepAngleDegrees());
}
static class SweepGradientHelper {
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearCurvedSpacer.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearCurvedSpacer.java
index 1dcc99f..f67adc8 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearCurvedSpacer.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearCurvedSpacer.java
@@ -16,6 +16,8 @@
package androidx.wear.protolayout.renderer.inflater;
+import static java.lang.Math.min;
+
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
@@ -37,6 +39,7 @@
private static final int DEFAULT_THICKNESS_PX = 0;
private float mSweepAngleDegrees;
+ private float mLengthPx = 0;
private int mThicknessPx;
public WearCurvedSpacer(@NonNull Context context) {
@@ -72,6 +75,27 @@
}
@Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
+
+ // convert length in px to degrees.
+ if (mLengthPx == 0) {
+ return;
+ }
+
+ int size = min(MeasureSpec.getSize(widthMeasureSpec),
+ MeasureSpec.getSize(heightMeasureSpec));
+ if (size == 0) {
+ return;
+ }
+
+ float radius = (size - mThicknessPx) / 2F;
+ // Calculate angle in radian from arc length and radius:
+ // ArcAngleInRadian = ArcLength / Radius
+ mSweepAngleDegrees = (float) Math.toDegrees(mLengthPx / radius);
+ }
+
+ @Override
public float getSweepAngleDegrees() {
return mSweepAngleDegrees;
}
@@ -87,6 +111,14 @@
this.mSweepAngleDegrees = sweepAngleDegrees;
}
+ /**
+ * Sets the length this spacer, in pixels. If dp length is set, it overrides the degrees
+ * value set by {@link #setSweepAngleDegrees(float)}
+ */
+ public void setLengthPx(float lengthPx) {
+ this.mLengthPx = lengthPx;
+ }
+
/** Sets the thickness of this spacer, in DP. */
public void setThickness(int thickness) {
this.mThicknessPx = thickness;
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearDashedArcLineView.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearDashedArcLineView.java
new file mode 100644
index 0000000..ff9e5ad
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearDashedArcLineView.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.wear.protolayout.renderer.inflater;
+
+import static androidx.wear.protolayout.renderer.inflater.ArcWidgetHelper.getSignForClockwise;
+import static androidx.wear.protolayout.renderer.inflater.ArcWidgetHelper.isPointInsideArcArea;
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Cap;
+import android.graphics.Paint.Style;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.util.Log;
+import android.view.View;
+import androidx.annotation.ColorInt;
+import androidx.wear.protolayout.proto.LayoutElementProto.ArcDirection;
+import androidx.wear.widget.ArcLayout.Widget;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * A line, drawn inside an arc. It can be divided into segments by specifying gap size and gap
+ * locations.
+ */
+public class WearDashedArcLineView extends View implements Widget {
+ private static final String TAG = "WearDashedArcLineView";
+ private static final int DEFAULT_THICKNESS_PX = 0;
+ @ColorInt private static final int DEFAULT_COLOR = 0xFFFFFFFF;
+
+ /**
+ * The base angle for drawings. The zero angle in Android corresponds to the "3 o clock" position,
+ * while ProtoLayout and ArcLayout use the "12 o clock" position as zero.
+ */
+ private static final float BASE_DRAW_ANGLE_SHIFT = -90f;
+
+ @NonNull private final Paint mPaint;
+ @NonNull private final Paint mScaledPaint;
+ private final ArrayList<Float> mGapLocationsInDegrees = new ArrayList<>();
+ private final ArrayList<LineSegment> mSegments = new ArrayList<>();
+ private ArcDirection mLineDirection = ArcDirection.ARC_DIRECTION_NORMAL;
+ private float mMaxSweepAngleDegrees = 360F;
+ private float mLineSweepAngleDegrees = 0F;
+ private int mGapSizePx = 0;
+ private float mStrokeWidthPx;
+ @Nullable private Path mPath;
+ @Nullable private Path mScaledPath;
+
+ public WearDashedArcLineView(@NonNull Context context) {
+ super(context, null, 0, 0);
+
+ mPaint = new Paint();
+ mPaint.setStyle(Style.STROKE);
+ mPaint.setStrokeCap(Cap.ROUND);
+ mPaint.setColor(DEFAULT_COLOR);
+ mPaint.setStrokeWidth(DEFAULT_THICKNESS_PX);
+ mPaint.setAntiAlias(true);
+
+ mScaledPaint = new Paint(mPaint);
+ }
+
+ private void updatePath() {
+ if (this.getMeasuredWidth() <= 0) {
+ return;
+ }
+
+ mPath = null;
+ mScaledPath = null;
+
+ float lineLength = resolveSweepAngleDegrees();
+ float insetPx = mStrokeWidthPx / 2f;
+ float radius = this.getMeasuredWidth() / 2f - insetPx;
+ // Calculate arc length from angle and radius: ArcLength = Radius * ArcAngleInRadian
+ float dotInDegree = (float) Math.toDegrees(mStrokeWidthPx / radius);
+
+ float startAngleDegrees = mSegments.get(0).startAngleDegrees;
+ if (lineLength <= startAngleDegrees) {
+ return;
+ }
+
+ int arcDirectionSign = getSignForClockwise(this, mLineDirection, /* defaultValue= */ 1);
+
+ RectF rect =
+ new RectF(
+ insetPx,
+ insetPx,
+ this.getMeasuredWidth() - insetPx,
+ this.getMeasuredHeight() - insetPx);
+
+ // The arc needs to be offset by -90 degrees. The ArcContainer will rotate this widget such
+ // that the "12 o clock" position on the canvas is aligned to the center of our requested
+ // angle, but 0 degrees in Android corresponds to the "3 o clock" position.
+ float angleShift = BASE_DRAW_ANGLE_SHIFT - (lineLength / 2f) * arcDirectionSign;
+ for (int i = 0; i < mSegments.size(); i++) {
+ if (mSegments.get(i).startAngleDegrees > lineLength) {
+ return;
+ }
+
+ float segmentArcLength =
+ min(mSegments.get(i).lengthInDegrees, lineLength - mSegments.get(i).startAngleDegrees);
+ float scale = min(segmentArcLength / dotInDegree, 1F);
+ // The android arc line does not count the caps as part of the length.
+ segmentArcLength -= dotInDegree;
+ // For each segment, during the enter transition, we set the stroke width to be equal to
+ // the required arc length, and in the same time, to get the dot displayed stably, we set the
+ // segment to be 0.1 degree to make the dot get displayed.
+ segmentArcLength = max(segmentArcLength, 0.1F);
+
+ float segmentStartAngle = mSegments.get(i).startAngleDegrees + dotInDegree * scale / 2f;
+ if (scale == 1F) {
+ if (mPath == null) {
+ mPath = new Path();
+ }
+ mPath.addArc(
+ rect,
+ segmentStartAngle * arcDirectionSign + angleShift,
+ segmentArcLength * arcDirectionSign);
+ } else {
+ // Apply scale to the stroke width for dot transition.
+ if (mStrokeWidthPx * scale < 1f) { // stroke width is smaller than 1 pixel
+ return;
+ }
+ mScaledPaint.setStrokeWidth(mStrokeWidthPx * scale);
+ mScaledPaint.setAlpha((int) (scale * Color.alpha(mPaint.getColor())));
+ if (mScaledPath == null) {
+ mScaledPath = new Path();
+ }
+ mScaledPath.addArc(
+ rect,
+ segmentStartAngle * arcDirectionSign + angleShift,
+ segmentArcLength * arcDirectionSign);
+ }
+ }
+ }
+
+ private void updateSegments() {
+ if (this.getMeasuredWidth() <= 0) {
+ return;
+ }
+
+ mSegments.clear();
+
+ if (mGapLocationsInDegrees.isEmpty()) {
+ mSegments.add(new LineSegment(0F, 360F));
+ return;
+ }
+
+ float segmentStart = 0;
+ int gapLocIndex = 0;
+ float radius = this.getMeasuredWidth() / 2f - mStrokeWidthPx / 2f;
+ float minSegmentLength = (float) Math.toDegrees(mStrokeWidthPx / radius);
+ float gapInDegrees = (float) Math.toDegrees(mGapSizePx / radius);
+ float halfGapInDegrees = gapInDegrees / 2;
+ if (mGapLocationsInDegrees.get(0) <= halfGapInDegrees) {
+ // start with gap
+ segmentStart = mGapLocationsInDegrees.get(0) + halfGapInDegrees;
+ gapLocIndex = 1;
+ }
+
+ while (gapLocIndex < mGapLocationsInDegrees.size()
+ && mGapLocationsInDegrees.get(gapLocIndex) < 360F + halfGapInDegrees) {
+ float gapLoc = mGapLocationsInDegrees.get(gapLocIndex);
+ float segmentLength = gapLoc - halfGapInDegrees - segmentStart;
+ if (segmentLength > minSegmentLength) {
+ mSegments.add(new LineSegment(segmentStart, segmentLength));
+ segmentStart = gapLoc + halfGapInDegrees;
+ } else {
+ // We could not fit a segment before the gap, ignoring this gap and log an error.
+ Log.e(
+ TAG,
+ "Ignoring the gap at the location "
+ + mGapLocationsInDegrees.get(gapLocIndex)
+ + ", as the arc segment before this gap is shorter than its thickness.");
+ }
+ gapLocIndex++;
+ }
+
+ if (segmentStart < 360F && 360F - segmentStart > minSegmentLength) {
+ mSegments.add(new LineSegment(segmentStart, 360F - segmentStart));
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ updateSegments();
+ updatePath();
+ }
+
+ /** Gets the list of each gap's center location in degrees. */
+ @NonNull
+ public List<Float> getGapLocations() {
+ return mGapLocationsInDegrees;
+ }
+
+ /**
+ * Sets the list of each gap's center location in degrees.
+ *
+ * <p>The interval between any two locations could not be shorter than thickness plus gap size.
+ * Any gap for which the distance from its previous gap is shorter than thickness plus gap will be
+ * ignored.
+ */
+ public void setGapLocations(@NonNull List<Float> gapLocations) {
+ mGapLocationsInDegrees.clear();
+ mGapLocationsInDegrees.addAll(gapLocations);
+ Collections.sort(mGapLocationsInDegrees);
+
+ refresh(/* updateSegments= */ true);
+ }
+
+ /** Gets the size of the gap between the segments. */
+ public int getGapSize() {
+ return mGapSizePx;
+ }
+
+ /** Sets the size of the gap between the segments. If not defined, defaults to 0. */
+ public void setGapSize(int lengthInDp) {
+ mGapSizePx = max(0, lengthInDp);
+
+ refresh(/* updateSegments= */ true);
+ }
+
+ /** Sets the length of the line contained within this CurvedLineView. */
+ public void setLineSweepAngleDegrees(float lineLengthDegrees) {
+ this.mLineSweepAngleDegrees = lineLengthDegrees;
+
+ refresh(/* updateSegments= */ false);
+ }
+
+ private float resolveSweepAngleDegrees() {
+ return min(mLineSweepAngleDegrees, mMaxSweepAngleDegrees);
+ }
+
+ private void refresh(boolean updateSegments) {
+ if (updateSegments) {
+ updateSegments();
+ }
+ updatePath();
+ requestLayout();
+ postInvalidate();
+ }
+
+ /** Sets the direction which the line is drawn. */
+ public void setLineDirection(@NonNull ArcDirection direction) {
+ mLineDirection = direction;
+ refresh(/* updateSegments= */ false);
+ }
+
+ /** Gets the direction which the line is drawn. */
+ @NonNull
+ public ArcDirection getLineDirection() {
+ return mLineDirection;
+ }
+
+ /** Returns the sweep angle that this widget is drawn with. */
+ @Override
+ public float getSweepAngleDegrees() {
+ return resolveSweepAngleDegrees();
+ }
+
+ @Override
+ public void setSweepAngleDegrees(float sweepAngleDegrees) {
+ this.mLineSweepAngleDegrees = sweepAngleDegrees;
+ }
+
+ /** Returns the thickness of this widget inside the arc. */
+ @Override
+ public int getThickness() {
+ return (int) mStrokeWidthPx;
+ }
+
+ /** Sets the thickness of this arc in pixels. */
+ public void setThickness(int thickness) {
+ mStrokeWidthPx = max(thickness, 0);
+ mPaint.setStrokeWidth(mStrokeWidthPx);
+ refresh(/* updateSegments= */ true);
+ }
+
+ /**
+ * Check whether the widget contains invalid attributes as a child of ArcLayout, throwing a
+ * Exception if something is wrong. This is important for widgets that can be both standalone or
+ * used inside an ArcLayout, some parameters used when the widget is standalone doesn't make sense
+ * when the widget is inside an ArcLayout.
+ */
+ @Override
+ public void checkInvalidAttributeAsChild() {
+ // Nothing required...
+ }
+
+ /** Gets the maximum sweep angle of the line, in degrees. */
+ public float getMaxSweepAngleDegrees() {
+ return mMaxSweepAngleDegrees;
+ }
+
+ /** Sets the maximum sweep angle of the line, in degrees. */
+ public void setMaxSweepAngleDegrees(float maxSweepAngleDegrees) {
+ this.mMaxSweepAngleDegrees = maxSweepAngleDegrees;
+
+ refresh(/* updateSegments= */ false);
+ }
+
+ /** Returns the color of this arc, in ARGB format. */
+ @ColorInt
+ public int getColor() {
+ return mPaint.getColor();
+ }
+
+ /** Sets the color of this arc, in ARGB format. */
+ public void setColor(@ColorInt int color) {
+ mPaint.setColor(color);
+ mScaledPaint.setColor(color);
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(@NonNull Canvas canvas) {
+ if (mPath != null) {
+ canvas.drawPath(mPath, mPaint);
+ }
+ if (mScaledPath != null) {
+ canvas.drawPath(mScaledPath, mScaledPaint);
+ }
+ }
+
+ /** Return true when the given point is in the clickable area of the child widget. */
+ @Override
+ public boolean isPointInsideClickArea(float x, float y) {
+ return isPointInsideArcArea(this, x, y, mStrokeWidthPx, resolveSweepAngleDegrees());
+ }
+
+ private static class LineSegment {
+ final float startAngleDegrees;
+ final float lengthInDegrees;
+
+ LineSegment(float startAngleDegrees, float lengthInDegrees) {
+ this.startAngleDegrees = startAngleDegrees;
+ this.lengthInDegrees = lengthInDegrees;
+ }
+ }
+}
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
index e4712f9..9322e15 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
@@ -151,6 +151,7 @@
import androidx.wear.protolayout.proto.LayoutElementProto.Box;
import androidx.wear.protolayout.proto.LayoutElementProto.ColorFilter;
import androidx.wear.protolayout.proto.LayoutElementProto.Column;
+import androidx.wear.protolayout.proto.LayoutElementProto.DashedArcLine;
import androidx.wear.protolayout.proto.LayoutElementProto.ExtensionLayoutElement;
import androidx.wear.protolayout.proto.LayoutElementProto.FontFeatureSetting;
import androidx.wear.protolayout.proto.LayoutElementProto.FontSetting;
@@ -2335,7 +2336,7 @@
}
@Test
- public void inflate_arc_withSpacer() {
+ public void inflate_arc_withSpacerInDegrees() {
LayoutElement root =
LayoutElement.newBuilder()
.setArc(
@@ -2359,6 +2360,47 @@
}
@Test
+ public void inflate_arc_withSpacerInDp() {
+ float containerSize = 100;
+ float thickness = 10;
+ float spacerLength = 20;
+ ContainerDimension containerDimension =
+ ContainerDimension.newBuilder().setLinearDimension(dp(containerSize)).build();
+ Arc.Builder arcBuilder =
+ Arc.newBuilder()
+ .setAnchorAngle(degrees(0).build())
+ .addContents(
+ ArcLayoutElement.newBuilder()
+ .setSpacer(
+ ArcSpacer.newBuilder()
+ .setAngularLength(
+ AngularDimension.newBuilder().setDp(
+ dp(spacerLength)))
+ .setThickness(dp(thickness))));
+ LayoutElement root =
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(containerDimension)
+ .setHeight(containerDimension)
+ .addContents(LayoutElement.newBuilder().setArc(arcBuilder)))
+ .build();
+
+ FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
+
+ float radius = (containerSize - thickness) / 2;
+ float sweepAngle = (float) Math.toDegrees(spacerLength / radius);
+
+ ArcLayout arcLayout = (ArcLayout) ((ViewGroup) rootLayout.getChildAt(0)).getChildAt(0);
+ assertThat(arcLayout.getChildCount()).isEqualTo(1);
+ WearCurvedSpacer spacer = (WearCurvedSpacer) arcLayout.getChildAt(0);
+ assertThat(spacer.getSweepAngleDegrees()).isEqualTo(sweepAngle);
+ // Dimensions are in DP, but the density is currently 1 in the tests, so this is fine:
+ assertThat(spacer.getThickness()).isEqualTo((int) thickness);
+ }
+
+
+ @Test
public void inflate_arc_withMaxAngleAndWeights() {
AngularDimension spacerLength =
AngularDimension.newBuilder()
@@ -6195,6 +6237,111 @@
box, textView, inflatedViewParent.getLeft() - textView.getLeft());
}
+ @Test
+ public void inflate_dashedArcLine_dynamicLength() {
+ AppDataKey<DynamicBuilders.DynamicFloat> keyFoo = new AppDataKey<>("foo");
+ mStateStore.setAppStateEntryValuesProto(
+ ImmutableMap.of(
+ keyFoo,
+ DynamicDataValue.newBuilder()
+ .setFloatVal(FixedFloat.newBuilder().setValue(10F))
+ .build()));
+
+ DynamicFloat arcLength =
+ DynamicFloat.newBuilder()
+ .setStateSource(StateFloatSource.newBuilder().setSourceKey("foo").build())
+ .build();
+
+ DashedArcLine dashedArcLine =
+ DashedArcLine.newBuilder()
+ // Shorter than 360 degrees, so should be drawn as an arc:
+ .setLength(degreesDynamic(arcLength, /* valueForLayout= */ 180f))
+ .setThickness(dp(12))
+ .build();
+
+ WearDashedArcLineView lineView = inflateDashedArcLine(dashedArcLine);
+ assertThat(lineView.getSweepAngleDegrees()).isEqualTo(10);
+
+ mStateStore.setAppStateEntryValuesProto(
+ ImmutableMap.of(
+ keyFoo,
+ DynamicDataValue.newBuilder()
+ .setFloatVal(FixedFloat.newBuilder().setValue(20F))
+ .build()));
+
+ assertThat(lineView.getSweepAngleDegrees()).isEqualTo(20);
+ }
+
+ @Test
+ public void inflate_dashedArcLine_usesZeroValueForLayout() {
+ DynamicFloat arcLength =
+ DynamicFloat.newBuilder().setFixed(FixedFloat.newBuilder().setValue(45f)).build();
+
+ DashedArcLine dashedArcLine =
+ DashedArcLine.newBuilder()
+ .setLength(degreesDynamic(arcLength, /* valueForLayout= */ 0f))
+ .setThickness(dp(12))
+ .build();
+
+ WearDashedArcLineView lineView = inflateDashedArcLine(dashedArcLine);
+ expect.that(lineView.getMaxSweepAngleDegrees()).isEqualTo(0f);
+ }
+
+ @Test
+ public void inflate_dashedArcLine_dynamicColor() {
+ AppDataKey<DynamicBuilders.DynamicColor> keyFoo = new AppDataKey<>("foo");
+ mStateStore.setAppStateEntryValuesProto(
+ ImmutableMap.of(
+ keyFoo,
+ DynamicDataValue.newBuilder()
+ .setColorVal(FixedColor.newBuilder().setArgb(Color.CYAN))
+ .build()));
+
+ DynamicColor arcColor =
+ DynamicColor.newBuilder()
+ .setStateSource(StateColorSource.newBuilder().setSourceKey("foo").build())
+ .build();
+
+ DashedArcLine dashedArcLine =
+ DashedArcLine.newBuilder()
+ // Shorter than 360 degrees, so should be drawn as an arc:
+ .setLength(degrees(180))
+ .setColor(ColorProp.newBuilder().setDynamicValue(arcColor))
+ .setThickness(dp(12))
+ .build();
+
+ WearDashedArcLineView lineView = inflateDashedArcLine(dashedArcLine);
+ assertThat(lineView.getColor()).isEqualTo(Color.CYAN);
+
+ mStateStore.setAppStateEntryValuesProto(
+ ImmutableMap.of(
+ keyFoo,
+ DynamicDataValue.newBuilder()
+ .setColorVal(FixedColor.newBuilder().setArgb(Color.MAGENTA))
+ .build()));
+
+ assertThat(lineView.getColor()).isEqualTo(Color.MAGENTA);
+ }
+
+ private WearDashedArcLineView inflateDashedArcLine(DashedArcLine dashedArcLine) {
+ LayoutElement root =
+ LayoutElement.newBuilder()
+ .setArc(
+ Arc.newBuilder()
+ .addContents(ArcLayoutElement.newBuilder().setDashedLine(
+ dashedArcLine)))
+ .build();
+
+ FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
+ ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0);
+
+ if (arcLayout.getChildAt(0) instanceof SizedArcContainer) {
+ return (WearDashedArcLineView) ((SizedArcContainer) arcLayout.getChildAt(0)).getChildAt(
+ 0);
+ }
+ return (WearDashedArcLineView) arcLayout.getChildAt(0);
+ }
+
private void assertSlideInInitialOffset(
ViewGroup box, TextView textView, float expectedInitialOffset) {
Animation animation = textView.getAnimation();
diff --git a/wear/protolayout/protolayout-testing/src/main/java/androidx/wear/protolayout/testing/filters.kt b/wear/protolayout/protolayout-testing/src/main/java/androidx/wear/protolayout/testing/filters.kt
index c7b9960..2e024cc 100644
--- a/wear/protolayout/protolayout-testing/src/main/java/androidx/wear/protolayout/testing/filters.kt
+++ b/wear/protolayout/protolayout-testing/src/main/java/androidx/wear/protolayout/testing/filters.kt
@@ -91,7 +91,10 @@
*/
public fun hasText(value: StringProp): LayoutElementMatcher =
LayoutElementMatcher("Element text = '$value'") {
- it is Text && it.text?.toProto() == value.toProto()
+ it is Text &&
+ // TODO: b/375448507 - Add dynamic data evaluation and compare the current string value
+ it.text?.toProto()?.value == value.toProto().value &&
+ it.text?.toProto()?.dynamicValue == value.toProto().dynamicValue
}
/**
diff --git a/wear/protolayout/protolayout-testing/src/test/java/androidx/wear/protolayout/testing/FiltersTest.kt b/wear/protolayout/protolayout-testing/src/test/java/androidx/wear/protolayout/testing/FiltersTest.kt
index 927a29f..6ca1a48 100644
--- a/wear/protolayout/protolayout-testing/src/test/java/androidx/wear/protolayout/testing/FiltersTest.kt
+++ b/wear/protolayout/protolayout-testing/src/test/java/androidx/wear/protolayout/testing/FiltersTest.kt
@@ -38,7 +38,9 @@
import androidx.wear.protolayout.ModifiersBuilders.Modifiers
import androidx.wear.protolayout.ModifiersBuilders.Semantics
import androidx.wear.protolayout.StateBuilders
+import androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint
import androidx.wear.protolayout.TypeBuilders.StringProp
+import androidx.wear.protolayout.expression.DynamicBuilders
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -147,6 +149,24 @@
}
@Test
+ fun hasDynamicText() {
+ val textContent =
+ StringProp.Builder("static content")
+ .setDynamicValue(DynamicBuilders.DynamicString.constant("dynamic content"))
+ .build()
+ val testElement =
+ Text.Builder()
+ .setText(textContent)
+ .setLayoutConstraintsForDynamicText(
+ StringLayoutConstraint.Builder("static content").build()
+ )
+ .build()
+
+ assertThat(hasText(textContent).matches(testElement)).isTrue()
+ assertThat(hasText("blabla").matches(testElement)).isFalse()
+ }
+
+ @Test
fun hasImage() {
val resId = "randomRes"
val testElement = Image.Builder().setResourceId(resId).build()
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
index 7fba5d7..afd1fc7 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
@@ -4822,8 +4822,8 @@
}
/**
- * A dashed arc line that can be placed in an {@link Arc} container. It is an arc line made up
- * of arc line segments separated by gaps.
+ * A dashed arc line. It is an arc line made up of arc line segments separated by gaps, and can
+ * be placed in an {@link Arc} container.
*/
@RequiresSchemaVersion(major = 1, minor = 500)
public static final class DashedArcLine implements ArcLayoutElement {
@@ -4837,10 +4837,6 @@
/**
* Gets the length of this line in degrees, including gaps.
- *
- * <p>When using a dynamic value, make sure to specify the bounding constraints for the
- * affected layout element through {@code setLayoutConstraintsForDynamicLength
- * (AngularLayoutConstraint)}, otherwise {@code build()} fails.
*/
public @Nullable DegreesProp getLength() {
if (mImpl.hasLength()) {
@@ -4967,7 +4963,8 @@
public Builder() {}
/**
- * Sets the length of this line, in degrees. If not defined, defaults to 0.
+ * Sets the length of this line in degrees, including gaps. If not defined, defaults to
+ * 0.
*
* <p>When using a dynamic value, make sure to specify the bounding constraints for the
* affected layout element through {@code setLayoutConstraintsForDynamicLength
@@ -5016,8 +5013,8 @@
}
/**
- * Sets the direction in which this line is drawn. If not set, defaults to
- * ARC_DIRECTION_CLOCKWISE.
+ * Sets the direction in which this line is drawn. If not set, defaults to the {@link
+ * Arc} container's direction where it is placed in.
*/
@RequiresSchemaVersion(major = 1, minor = 500)
public @NonNull Builder setArcDirection(@NonNull ArcDirectionProp arcDirection) {
@@ -5028,8 +5025,8 @@
}
/**
- * Sets the direction in which this line is drawn. If not set, defaults to
- * ARC_DIRECTION_CLOCKWISE.
+ * Sets the direction in which this line is drawn. If not set, defaults to the {@link
+ * Arc} container's direction where it is placed in.
*/
@RequiresSchemaVersion(major = 1, minor = 500)
public @NonNull Builder setArcDirection(@ArcDirection int arcDirection) {
@@ -5078,7 +5075,10 @@
}
}
- /** A dashed line pattern which describes how the dashed arc line is segmented by gaps. */
+ /**
+ * A dashed line pattern which describes how the dashed arc line is segmented by gaps. It
+ * determines the gap size and the gap locations.
+ */
@RequiresSchemaVersion(major = 1, minor = 500)
public static final class DashedLinePattern {
private final LayoutElementProto.DashedLinePattern mImpl;
@@ -5186,8 +5186,8 @@
/**
* Sets the list of each gap's center location in degrees.
*
- * <p>The interval between any two locations could not be shorter than thickness plus
- * gap size.
+ * <p>The interval between any two locations must not be shorter than thickness plus gap
+ * size, otherwise the gap is ignored.
*
* <p>Note that calling this method will invalidate the previous call of {@link
* #setGapInterval}
@@ -5207,7 +5207,8 @@
* Sets the interval length in degrees between two consecutive gap center locations. The
* arc line will have arc line segments with equal length.
*
- * <p>The interval could not be shorter than thickness plus gap size.
+ * <p>The interval between any two locations must not be shorter than thickness plus gap
+ * size, otherwise the gap is ignored.
*
* <p>Note that calling this method will remove all the gap locations set previously
* with {@link #setGapLocations}
@@ -5217,7 +5218,7 @@
public @NonNull Builder setGapInterval(float gapIntervalInDegrees) {
mImpl.clearGapLocations();
- float gapLocation = gapIntervalInDegrees;
+ float gapLocation = 0;
while (gapLocation <= 360F) {
addGapLocation(degrees(gapLocation));
gapLocation += gapIntervalInDegrees;
diff --git a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/LayoutElementBuildersTest.java b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/LayoutElementBuildersTest.java
index 1904f9c..dff6d44 100644
--- a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/LayoutElementBuildersTest.java
+++ b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/LayoutElementBuildersTest.java
@@ -803,9 +803,10 @@
LayoutElementProto.DashedLinePattern brush = dashedArcLine.getLinePattern().toProto();
assertThat(brush.getGapSize().getValue()).isEqualTo(4.5F);
List<DimensionProto.DegreesProp> gapLocations =brush.getGapLocationsList();
- assertThat(gapLocations.get(0).getValue()).isEqualTo(111F);
- assertThat(gapLocations.get(1).getValue()).isEqualTo(222F);
- assertThat(gapLocations.get(2).getValue()).isEqualTo(333F);
+ assertThat(gapLocations.get(0).getValue()).isEqualTo(0F);
+ assertThat(gapLocations.get(1).getValue()).isEqualTo(111F);
+ assertThat(gapLocations.get(2).getValue()).isEqualTo(222F);
+ assertThat(gapLocations.get(3).getValue()).isEqualTo(333F);
}
@Test
diff --git a/wear/tiles/tiles-material/build.gradle b/wear/tiles/tiles-material/build.gradle
index 707f6ed..c472f64 100644
--- a/wear/tiles/tiles-material/build.gradle
+++ b/wear/tiles/tiles-material/build.gradle
@@ -63,9 +63,6 @@
defaultConfig {
minSdk = 26
}
- sourceSets {
- androidTest.assets.srcDirs += project.rootDir.absolutePath + "/../../golden/wear/wear-tiles-material"
- }
namespace = "androidx.wear.tiles.material"
}
@@ -75,4 +72,5 @@
inceptionYear = "2021"
description = "Material components library for Android Wear Tiles."
legacyDisableKotlinStrictApiMode = true
+ addGoldenImageAssets()
}
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenTest.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenTest.java
index 6825e35..7b87137 100644
--- a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenTest.java
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenTest.java
@@ -20,6 +20,7 @@
import static androidx.wear.tiles.material.RunnerUtils.SCREEN_WIDTH;
import static androidx.wear.tiles.material.RunnerUtils.runSingleScreenshotTest;
import static androidx.wear.tiles.material.RunnerUtils.waitForNotificationToDisappears;
+import static androidx.wear.tiles.material.ScreenshotKt.SCREENSHOT_GOLDEN_PATH;
import static androidx.wear.tiles.material.TestCasesGenerator.generateTestCases;
import android.content.Context;
@@ -48,7 +49,7 @@
@Rule
public AndroidXScreenshotTestRule mScreenshotRule =
- new AndroidXScreenshotTestRule("wear/wear-tiles-material");
+ new AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH);
public MaterialGoldenTest(
String expected,
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenXLTest.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenXLTest.java
index 2115394..19678f5 100644
--- a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenXLTest.java
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenXLTest.java
@@ -21,6 +21,7 @@
import static androidx.wear.tiles.material.RunnerUtils.SCREEN_WIDTH;
import static androidx.wear.tiles.material.RunnerUtils.runSingleScreenshotTest;
import static androidx.wear.tiles.material.RunnerUtils.waitForNotificationToDisappears;
+import static androidx.wear.tiles.material.ScreenshotKt.SCREENSHOT_GOLDEN_PATH;
import static androidx.wear.tiles.material.TestCasesGenerator.XXXL_SCALE_SUFFIX;
import static androidx.wear.tiles.material.TestCasesGenerator.generateTestCases;
@@ -59,7 +60,7 @@
@Rule
public AndroidXScreenshotTestRule mScreenshotRule =
- new AndroidXScreenshotTestRule("wear/wear-tiles-material");
+ new AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH);
public MaterialGoldenXLTest(
String expected,
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/Screenshot.kt b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/Screenshot.kt
new file mode 100644
index 0000000..49c5dc1
--- /dev/null
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/Screenshot.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.wear.tiles.material
+
+internal const val SCREENSHOT_GOLDEN_PATH = "wear/tiles/tiles-material"
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenTest.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenTest.java
index 74ace96c..393e674 100644
--- a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenTest.java
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenTest.java
@@ -20,6 +20,7 @@
import static androidx.wear.tiles.material.RunnerUtils.SCREEN_WIDTH;
import static androidx.wear.tiles.material.RunnerUtils.runSingleScreenshotTest;
import static androidx.wear.tiles.material.RunnerUtils.waitForNotificationToDisappears;
+import static androidx.wear.tiles.material.ScreenshotKt.SCREENSHOT_GOLDEN_PATH;
import static androidx.wear.tiles.material.layouts.TestCasesGenerator.generateTestCases;
import android.content.Context;
@@ -48,7 +49,7 @@
@Rule
public AndroidXScreenshotTestRule mScreenshotRule =
- new AndroidXScreenshotTestRule("wear/wear-tiles-material");
+ new AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH);
public LayoutsGoldenTest(
String expected,
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenXLTest.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenXLTest.java
index f05d0b2..9e615c3 100644
--- a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenXLTest.java
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenXLTest.java
@@ -21,6 +21,7 @@
import static androidx.wear.tiles.material.RunnerUtils.SCREEN_WIDTH;
import static androidx.wear.tiles.material.RunnerUtils.runSingleScreenshotTest;
import static androidx.wear.tiles.material.RunnerUtils.waitForNotificationToDisappears;
+import static androidx.wear.tiles.material.ScreenshotKt.SCREENSHOT_GOLDEN_PATH;
import static androidx.wear.tiles.material.layouts.TestCasesGenerator.XXXL_SCALE_SUFFIX;
import static androidx.wear.tiles.material.layouts.TestCasesGenerator.generateTestCases;
@@ -59,7 +60,7 @@
@Rule
public AndroidXScreenshotTestRule mScreenshotRule =
- new AndroidXScreenshotTestRule("wear/wear-tiles-material");
+ new AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH);
public LayoutsGoldenXLTest(
String expected,
diff --git a/wear/tiles/tiles-renderer/build.gradle b/wear/tiles/tiles-renderer/build.gradle
index d50ba1d..14aedd7 100644
--- a/wear/tiles/tiles-renderer/build.gradle
+++ b/wear/tiles/tiles-renderer/build.gradle
@@ -81,9 +81,6 @@
// Use Robolectric 4.+
testOptions.unitTests.includeAndroidResources = true
- sourceSets {
- androidTest.assets.srcDirs += project.rootDir.absolutePath + "/../../golden/wear/wear-tiles-renderer"
- }
namespace = "androidx.wear.tiles.renderer"
}
@@ -108,5 +105,6 @@
description = "Android Wear Tiles Renderer components. These components can be used to parse " +
"and render an already constructed Wear Tile."
legacyDisableKotlinStrictApiMode = true
+ addGoldenImageAssets()
}
diff --git a/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java b/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
index c8c66c4..a2640ba 100644
--- a/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
+++ b/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
@@ -120,7 +120,7 @@
@Rule
public AndroidXScreenshotTestRule screenshotRule =
- new AndroidXScreenshotTestRule("wear/wear-tiles-renderer");
+ new AndroidXScreenshotTestRule("wear/tiles/tiles-renderer");
// This isn't totally ideal right now. The screenshot tests run on a phone, so emulate some
// watch dimensions here.
diff --git a/wear/tiles/tiles-tooling/build.gradle b/wear/tiles/tiles-tooling/build.gradle
index 38f2aff..bb2dbec 100644
--- a/wear/tiles/tiles-tooling/build.gradle
+++ b/wear/tiles/tiles-tooling/build.gradle
@@ -33,7 +33,7 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.guava)
- androidTestImplementation project(":wear:protolayout:protolayout-material")
+ androidTestImplementation(project(":wear:protolayout:protolayout-material"))
}
android {
diff --git a/wear/watchface/watchface-client-guava/build.gradle b/wear/watchface/watchface-client-guava/build.gradle
index fecf04a..eba4a21 100644
--- a/wear/watchface/watchface-client-guava/build.gradle
+++ b/wear/watchface/watchface-client-guava/build.gradle
@@ -36,7 +36,7 @@
api(libs.kotlinCoroutinesGuava)
implementation("androidx.concurrent:concurrent-futures:1.0.0")
- androidTestImplementation project(":wear:watchface:watchface-samples")
+ androidTestImplementation(project(":wear:watchface:watchface-samples"))
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
diff --git a/wear/watchface/watchface-client/build.gradle b/wear/watchface/watchface-client/build.gradle
index bfb0889..aefdc88 100644
--- a/wear/watchface/watchface-client/build.gradle
+++ b/wear/watchface/watchface-client/build.gradle
@@ -63,9 +63,6 @@
defaultConfig {
minSdk = 26
}
- sourceSets.androidTest.assets.srcDirs +=
- project.rootDir.absolutePath + "/../../golden/wear/wear-watchface-client"
-
// Use Robolectric 4.+
testOptions.unitTests.includeAndroidResources = true
namespace = "androidx.wear.watchface.client"
@@ -77,4 +74,5 @@
inceptionYear = "2020"
description = "Client library for controlling androidx watchfaces"
legacyDisableKotlinStrictApiMode = true
+ addGoldenImageAssets()
}
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/HeadlessWatchFaceClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/HeadlessWatchFaceClientTest.kt
index bf838f8..52ac483 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/HeadlessWatchFaceClientTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/HeadlessWatchFaceClientTest.kt
@@ -239,7 +239,7 @@
class HeadlessWatchFaceClientScreenshotTest : HeadlessWatchFaceClientTestBase() {
@get:Rule
val screenshotRule: AndroidXScreenshotTestRule =
- AndroidXScreenshotTestRule("wear/wear-watchface-client")
+ AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH)
private val complications = createTestComplications(context)
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/Screenshot.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/Screenshot.kt
new file mode 100644
index 0000000..bd64a51
--- /dev/null
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/Screenshot.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.wear.watchface.client.test
+
+internal const val SCREENSHOT_GOLDEN_PATH = "wear/watchface/watchface-client"
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index 1ac9517..e75ab0d 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -1483,7 +1483,7 @@
class WatchFaceControlClientScreenshotTest : WatchFaceControlClientTestBase() {
@get:Rule
val screenshotRule: AndroidXScreenshotTestRule =
- AndroidXScreenshotTestRule("wear/wear-watchface-client")
+ AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH)
private val exampleOpenGLWatchFaceComponentName =
componentOf<ExampleOpenGLBackgroundInitWatchFaceService>()
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
index 914dc5b..e250425 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
@@ -4210,7 +4210,6 @@
return WireSizeAndDimensions(null, drawable.minimumWidth, drawable.minimumHeight)
}
-@SuppressLint("ClassVerificationFailure")
internal fun Icon.write(dos: DataOutputStream) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
IconHelper.writeToDataOutputStream(this, dos)
diff --git a/wear/watchface/watchface/build.gradle b/wear/watchface/watchface/build.gradle
index fdb7d3e..8efd472 100644
--- a/wear/watchface/watchface/build.gradle
+++ b/wear/watchface/watchface/build.gradle
@@ -69,10 +69,6 @@
defaultConfig {
minSdk = 26
}
-
- sourceSets.androidTest.assets.srcDirs +=
- project.rootDir.absolutePath + "/../../golden/wear/wear-watchface"
-
// Use Robolectric 4.+
testOptions.unitTests.includeAndroidResources = true
namespace = "androidx.wear.watchface"
@@ -85,4 +81,5 @@
description = "Android Wear Watchface"
legacyDisableKotlinStrictApiMode = true
samples(project(":wear:watchface:watchface-samples"))
+ addGoldenImageAssets()
}
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/Screenshot.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/Screenshot.kt
new file mode 100644
index 0000000..ec3b6cb
--- /dev/null
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/Screenshot.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.wear.watchface.test
+
+internal const val SCREENSHOT_GOLDEN_PATH = "wear/watchface/watchface"
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
index aadb9c8..d8d5e33 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
@@ -181,7 +181,7 @@
@MediumTest
public class WatchFaceControlServiceTest {
- @get:Rule internal val screenshotRule = AndroidXScreenshotTestRule("wear/wear-watchface")
+ @get:Rule internal val screenshotRule = AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH)
private lateinit var instance: IHeadlessWatchFace
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
index e204a0f..2dbf636 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
@@ -182,7 +182,7 @@
@get:Rule
public val screenshotRule: AndroidXScreenshotTestRule =
- AndroidXScreenshotTestRule("wear/wear-watchface")
+ AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH)
private val bitmap = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888)
private val canvas = Canvas(bitmap)
diff --git a/wear/wear-input/build.gradle b/wear/wear-input/build.gradle
index 11818b0..3f05ede 100644
--- a/wear/wear-input/build.gradle
+++ b/wear/wear-input/build.gradle
@@ -40,6 +40,7 @@
testImplementation(libs.testRules)
testImplementation(libs.robolectric)
testImplementation(libs.mockitoCore4)
+ testImplementation(fileTree(dir: "../wear_stubs", include: ["com.google.android.wearable-stubs.jar"]))
testImplementation(project(":wear:wear-input-testing"))
compileOnly(fileTree(dir: "../wear_stubs", include: ["com.google.android.wearable-stubs.jar"]))
diff --git a/wear/wear-input/src/main/java/androidx/wear/input/WearableButtons.java b/wear/wear-input/src/main/java/androidx/wear/input/WearableButtons.java
index 37e98bc..0891331 100644
--- a/wear/wear-input/src/main/java/androidx/wear/input/WearableButtons.java
+++ b/wear/wear-input/src/main/java/androidx/wear/input/WearableButtons.java
@@ -21,7 +21,7 @@
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RotateDrawable;
import android.os.Bundle;
-import android.provider.Settings;
+import android.view.Display;
import android.view.Surface;
import android.view.WindowManager;
@@ -254,33 +254,40 @@
// Get the screen size for the locationZone
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Point screenSize = new Point();
- wm.getDefaultDisplay().getSize(screenSize);
+ Display display = wm.getDefaultDisplay();
+ display.getSize(screenSize);
+ int rotation = display.getRotation();
- if (isLeftyModeEnabled(context)) {
- // By default, the rotated placement is exactly the opposite.
- // This may be overridden if there is a remapping of buttons applied as well.
- float screenRotatedX = screenSize.x - screenLocationX;
- float screenRotatedY = screenSize.y - screenLocationY;
-
- if (bundle.containsKey(X_KEY_ROTATED) && bundle.containsKey(Y_KEY_ROTATED)) {
- screenRotatedX = bundle.getFloat(X_KEY_ROTATED);
- screenRotatedY = bundle.getFloat(Y_KEY_ROTATED);
- }
-
- screenLocationX = screenRotatedX;
- screenLocationY = screenRotatedY;
+ float screenRotatedX = screenLocationX;
+ float screenRotatedY = screenLocationY;
+ switch (rotation) {
+ case Surface.ROTATION_90:
+ screenLocationX = (screenRotatedY - screenSize.y / 2f) + screenSize.x / 2f;
+ screenLocationY = -(screenRotatedX - screenSize.x / 2f) + screenSize.y / 2f;
+ break;
+ case Surface.ROTATION_180:
+ if (bundle.containsKey(X_KEY_ROTATED) && bundle.containsKey(Y_KEY_ROTATED)) {
+ // Override if there is a remapping of buttons applied as well.
+ screenLocationX = bundle.getFloat(X_KEY_ROTATED);
+ screenLocationY = bundle.getFloat(Y_KEY_ROTATED);
+ } else {
+ // By default, the rotated placement is exactly the opposite.
+ screenLocationX = screenSize.x - screenRotatedX;
+ screenLocationY = screenSize.y - screenRotatedY;
+ }
+ break;
+ case Surface.ROTATION_270:
+ screenLocationX = -(screenRotatedY - screenSize.y / 2f) + screenSize.x / 2f;
+ screenLocationY = (screenRotatedX - screenSize.x / 2f) + screenSize.y / 2f;
+ break;
}
boolean isRound = context.getResources().getConfiguration().isScreenRound();
-
- ButtonInfo info =
- new ButtonInfo(
- keycode,
- screenLocationX,
- screenLocationY,
- getLocationZone(isRound, screenSize, screenLocationX, screenLocationY));
-
- return info;
+ return new ButtonInfo(
+ keycode,
+ screenLocationX,
+ screenLocationY,
+ getLocationZone(isRound, screenSize, screenLocationX, screenLocationY));
}
/**
@@ -717,14 +724,6 @@
}
}
- private static boolean isLeftyModeEnabled(Context context) {
- return Settings.System.getInt(
- context.getContentResolver(),
- Settings.System.USER_ROTATION,
- Surface.ROTATION_0)
- == Surface.ROTATION_180;
- }
-
/** Metadata for a specific button. */
public static final class ButtonInfo {
private final int mKeycode;
diff --git a/wear/wear-input/src/test/java/androidx/wear/input/WearableButtonsTest.java b/wear/wear-input/src/test/java/androidx/wear/input/WearableButtonsTest.java
index 509a8d6..526b1d5 100644
--- a/wear/wear-input/src/test/java/androidx/wear/input/WearableButtonsTest.java
+++ b/wear/wear-input/src/test/java/androidx/wear/input/WearableButtonsTest.java
@@ -22,7 +22,7 @@
import android.content.Context;
import android.graphics.Point;
import android.graphics.drawable.RotateDrawable;
-import android.provider.Settings;
+import android.os.Bundle;
import android.view.Display;
import android.view.KeyEvent;
import android.view.Surface;
@@ -31,6 +31,9 @@
import androidx.test.core.app.ApplicationProvider;
import androidx.wear.input.testing.TestWearableButtonsProvider;
+import com.google.android.wearable.input.WearableInputDevice;
+
+import org.jspecify.annotations.NonNull;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Shadows;
@@ -252,68 +255,189 @@
WearableButtons.LOCATION_TOP_LEFT, R.drawable.ic_cc_settings_button_top, -90);
}
+ @SuppressWarnings("deprecation")
@Test
- public void testGetButtonsRighty() {
+ public void testGetButtonsRotation0() {
+ Context context = ApplicationProvider.getApplicationContext();
+ WindowManager wm = context.getSystemService(WindowManager.class);
+ Display display = wm.getDefaultDisplay();
+ Shadows.shadowOf(display).setWidth(/* width= */ 500);
+ Shadows.shadowOf(display).setHeight(/* height= */ 600);
+ Shadows.shadowOf(display).setRotation(Surface.ROTATION_0);
+
Map<Integer, TestWearableButtonsProvider.TestWearableButtonLocation> buttons =
new HashMap<>();
buttons.put(
KeyEvent.KEYCODE_STEM_1,
- new TestWearableButtonsProvider.TestWearableButtonLocation(1, 2, 3, 4));
+ new TestWearableButtonsProvider.TestWearableButtonLocation(
+ /* x= */ 100, /* y= */ 200, /* rotatedX= */ 300, /* rotatedY= */ 400));
TestWearableButtonsProvider provider = new TestWearableButtonsProvider(buttons);
WearableButtons.setWearableButtonsProvider(provider);
- setLeftyModeEnabled(false);
WearableButtons.ButtonInfo info =
WearableButtons.getButtonInfo(
ApplicationProvider.getApplicationContext(), KeyEvent.KEYCODE_STEM_1);
assertNotNull(info);
- assertEquals(1, info.getX(), 1.0e-7);
- assertEquals(2, info.getY(), 1.0e-7);
- }
-
- @Test
- public void testGetButtonsLefty() {
- setLeftyModeEnabled(true);
- Map<Integer, TestWearableButtonsProvider.TestWearableButtonLocation> buttons =
- new HashMap<>();
- buttons.put(
- KeyEvent.KEYCODE_STEM_1,
- new TestWearableButtonsProvider.TestWearableButtonLocation(1, 2, 3, 4));
-
- TestWearableButtonsProvider provider = new TestWearableButtonsProvider(buttons);
- WearableButtons.setWearableButtonsProvider(provider);
- WearableButtons.ButtonInfo info =
- WearableButtons.getButtonInfo(
- ApplicationProvider.getApplicationContext(), KeyEvent.KEYCODE_STEM_1);
- assertNotNull(info);
- assertEquals(3, info.getX(), 1.0e-7);
- assertEquals(4, info.getY(), 1.0e-7);
+ // expectedX = x, expectedY = y
+ assertEquals(/* expected= */ 100, info.getX(), 1.0e-7);
+ assertEquals(/* expected= */ 200, info.getY(), 1.0e-7);
}
@SuppressWarnings("deprecation")
@Test
- public void testGetButtonsLeftyNoData() {
+ public void testGetButtonsRotation90() {
Context context = ApplicationProvider.getApplicationContext();
- WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ WindowManager wm = context.getSystemService(WindowManager.class);
Display display = wm.getDefaultDisplay();
- Shadows.shadowOf(display).setWidth(480);
- Shadows.shadowOf(display).setHeight(480);
+ Shadows.shadowOf(display).setWidth(/* width= */ 500);
+ Shadows.shadowOf(display).setHeight(/* height= */ 600);
+ Shadows.shadowOf(display).setRotation(Surface.ROTATION_90);
- setLeftyModeEnabled(true);
Map<Integer, TestWearableButtonsProvider.TestWearableButtonLocation> buttons =
new HashMap<>();
buttons.put(
KeyEvent.KEYCODE_STEM_1,
- new TestWearableButtonsProvider.TestWearableButtonLocation(1, 2));
+ new TestWearableButtonsProvider.TestWearableButtonLocation(
+ /* x= */ 100, /* y= */ 200, /* rotatedX= */ 300, /* rotatedY= */ 400));
TestWearableButtonsProvider provider = new TestWearableButtonsProvider(buttons);
WearableButtons.setWearableButtonsProvider(provider);
+
WearableButtons.ButtonInfo info =
- WearableButtons.getButtonInfo(context, KeyEvent.KEYCODE_STEM_1);
+ WearableButtons.getButtonInfo(
+ ApplicationProvider.getApplicationContext(), KeyEvent.KEYCODE_STEM_1);
assertNotNull(info);
- assertEquals(479, info.getX(), 1.0e-7); // == 480 - 1
- assertEquals(478, info.getY(), 1.0e-7); // == 480 - 2
+ // expectedX = (y - height/2) + width/2, expectedY = -(x - width/2) + height/2
+ assertEquals(/* expected= */ 150, info.getX(), 1.0e-7);
+ assertEquals(/* expected= */ 450, info.getY(), 1.0e-7);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testGetButtonsRotation180_rotatedXAndYCoordinatesPresent() {
+ float rotatedX = 450;
+ float rotatedY = 550;
+ Context context = ApplicationProvider.getApplicationContext();
+ WindowManager wm = context.getSystemService(WindowManager.class);
+ Display display = wm.getDefaultDisplay();
+ Shadows.shadowOf(display).setWidth(/* width= */ 500);
+ Shadows.shadowOf(display).setHeight(/* height= */ 600);
+ Shadows.shadowOf(display).setRotation(Surface.ROTATION_180);
+
+ Map<Integer, TestWearableButtonsProvider.TestWearableButtonLocation> buttons =
+ new HashMap<>();
+ buttons.put(
+ KeyEvent.KEYCODE_STEM_1,
+ new TestWearableButtonsProvider.TestWearableButtonLocation(
+ /* x= */ 100, /* y= */ 200, rotatedX, rotatedY));
+
+ TestWearableButtonsProvider provider = new TestWearableButtonsProvider(buttons);
+ WearableButtons.setWearableButtonsProvider(provider);
+
+ WearableButtons.ButtonInfo info =
+ WearableButtons.getButtonInfo(
+ ApplicationProvider.getApplicationContext(), KeyEvent.KEYCODE_STEM_1);
+ assertNotNull(info);
+ // expectedX = rotatedX, expectedY = rotatedY
+ assertEquals(/* expected= */ rotatedX, info.getX(), 1.0e-7);
+ assertEquals(/* expected= */ rotatedY, info.getY(), 1.0e-7);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testGetButtonsRotation180_onlyRotatedXCoordinatePresent() {
+ float rotatedX = 450;
+ Context context = ApplicationProvider.getApplicationContext();
+ WindowManager wm = context.getSystemService(WindowManager.class);
+ Display display = wm.getDefaultDisplay();
+ Shadows.shadowOf(display).setWidth(/* width= */ 500);
+ Shadows.shadowOf(display).setHeight(/* height= */ 600);
+ Shadows.shadowOf(display).setRotation(Surface.ROTATION_180);
+
+ Map<Integer, TestWearableButtonsProvider.TestWearableButtonLocation> buttons =
+ new HashMap<>();
+ buttons.put(
+ KeyEvent.KEYCODE_STEM_1,
+ new TestWearableButtonsProvider.TestWearableButtonLocation(
+ /* x= */ 100, /* y= */ 200));
+
+ TestWearableButtonsProvider provider =
+ new TestWearableButtonsProvider(buttons) {
+ @Override
+ public @NonNull Bundle getButtonInfo(@NonNull Context context, int keycode) {
+ Bundle bundle = super.getButtonInfo(context, keycode);
+ bundle.putFloat(WearableInputDevice.X_KEY_ROTATED, rotatedX);
+ return bundle;
+ }
+ };
+ WearableButtons.setWearableButtonsProvider(provider);
+
+ WearableButtons.ButtonInfo info =
+ WearableButtons.getButtonInfo(
+ ApplicationProvider.getApplicationContext(), KeyEvent.KEYCODE_STEM_1);
+ assertNotNull(info);
+ // expectedX = width - x, expectedY = height - y
+ assertEquals(/* expected= */ 400, info.getX(), 1.0e-7);
+ assertEquals(/* expected= */ 400, info.getY(), 1.0e-7);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testGetButtonsRotation180_rotatedCoordinatesAbsent() {
+ Context context = ApplicationProvider.getApplicationContext();
+ WindowManager wm = context.getSystemService(WindowManager.class);
+ Display display = wm.getDefaultDisplay();
+ Shadows.shadowOf(display).setWidth(/* width= */ 500);
+ Shadows.shadowOf(display).setHeight(/* height= */ 600);
+ Shadows.shadowOf(display).setRotation(Surface.ROTATION_180);
+
+ Map<Integer, TestWearableButtonsProvider.TestWearableButtonLocation> buttons =
+ new HashMap<>();
+ buttons.put(
+ KeyEvent.KEYCODE_STEM_1,
+ new TestWearableButtonsProvider.TestWearableButtonLocation(
+ /* x= */ 100, /* y= */ 200));
+
+ TestWearableButtonsProvider provider = new TestWearableButtonsProvider(buttons);
+ WearableButtons.setWearableButtonsProvider(provider);
+
+ WearableButtons.ButtonInfo info =
+ WearableButtons.getButtonInfo(
+ ApplicationProvider.getApplicationContext(), KeyEvent.KEYCODE_STEM_1);
+ assertNotNull(info);
+ // expectedX = width - x, expectedY = height - y
+ assertEquals(/* expected= */ 400, info.getX(), 1.0e-7);
+ assertEquals(/* expected= */ 400, info.getY(), 1.0e-7);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testGetButtonsRotation270() {
+ Context context = ApplicationProvider.getApplicationContext();
+ WindowManager wm = context.getSystemService(WindowManager.class);
+ Display display = wm.getDefaultDisplay();
+ Shadows.shadowOf(display).setWidth(/* width= */ 500);
+ Shadows.shadowOf(display).setHeight(/* height= */ 600);
+ Shadows.shadowOf(display).setRotation(Surface.ROTATION_270);
+
+ Map<Integer, TestWearableButtonsProvider.TestWearableButtonLocation> buttons =
+ new HashMap<>();
+ buttons.put(
+ KeyEvent.KEYCODE_STEM_1,
+ new TestWearableButtonsProvider.TestWearableButtonLocation(
+ /* x= */ 100, /* y= */ 200, /* rotatedX= */ 300, /* rotatedY= */ 400));
+
+ TestWearableButtonsProvider provider = new TestWearableButtonsProvider(buttons);
+ WearableButtons.setWearableButtonsProvider(provider);
+
+ WearableButtons.ButtonInfo info =
+ WearableButtons.getButtonInfo(
+ ApplicationProvider.getApplicationContext(), KeyEvent.KEYCODE_STEM_1);
+ assertNotNull(info);
+ // expectedX = -(y - height/2) + width/2, expectedY = (x - width/2) + height/2
+ assertEquals(/* expected= */ 350, info.getX(), 1.0e-7);
+ assertEquals(/* expected= */ 150, info.getY(), 1.0e-7);
}
private void testRotateDrawable(
@@ -328,11 +452,4 @@
assertEquals(rotateDrawableShadow.getCreatedFromResId(), expectedDrawableId);
assertEquals(expectedDegreeRotation, rotateDrawable.getFromDegrees(), .001);
}
-
- private void setLeftyModeEnabled(boolean enabled) {
- Settings.System.putInt(
- ApplicationProvider.getApplicationContext().getContentResolver(),
- Settings.System.USER_ROTATION,
- enabled ? Surface.ROTATION_180 : Surface.ROTATION_0);
- }
}
diff --git a/wear/wear-phone-interactions/OWNERS b/wear/wear-phone-interactions/OWNERS
index 40741ea..b828954 100644
--- a/wear/wear-phone-interactions/OWNERS
+++ b/wear/wear-phone-interactions/OWNERS
@@ -1,3 +1,4 @@
# Bug component: 188444
[email protected]
[email protected]
\ No newline at end of file
[email protected]
[email protected]
\ No newline at end of file
diff --git a/wear/wear-remote-interactions/OWNERS b/wear/wear-remote-interactions/OWNERS
index 40741ea..b828954 100644
--- a/wear/wear-remote-interactions/OWNERS
+++ b/wear/wear-remote-interactions/OWNERS
@@ -1,3 +1,4 @@
# Bug component: 188444
[email protected]
[email protected]
\ No newline at end of file
[email protected]
[email protected]
\ No newline at end of file
diff --git a/wear/wear/build.gradle b/wear/wear/build.gradle
index 0af1071..df29901 100644
--- a/wear/wear/build.gradle
+++ b/wear/wear/build.gradle
@@ -56,12 +56,6 @@
defaultConfig {
minSdk = 23
}
-
- sourceSets {
- main.res.srcDirs += "src/main/res-public"
- androidTest.assets.srcDirs += project.rootDir.absolutePath + "/../../golden/wear/wear"
- }
-
buildTypes.configureEach {
consumerProguardFiles "proguard-rules.pro"
}
@@ -79,4 +73,5 @@
description = "Android Wear Support UI"
failOnDeprecationWarnings = false
legacyDisableKotlinStrictApiMode = true
+ addGoldenImageAssets()
}
diff --git a/wear/wear/src/main/res-public/values-v24/public_styles.xml b/wear/wear/src/main/res/values-v24/public_styles.xml
similarity index 100%
rename from wear/wear/src/main/res-public/values-v24/public_styles.xml
rename to wear/wear/src/main/res/values-v24/public_styles.xml
diff --git a/wear/wear/src/main/res-public/values/public_attrs.xml b/wear/wear/src/main/res/values/public_attrs.xml
similarity index 100%
rename from wear/wear/src/main/res-public/values/public_attrs.xml
rename to wear/wear/src/main/res/values/public_attrs.xml
diff --git a/webkit/integration-tests/testapp/build.gradle b/webkit/integration-tests/testapp/build.gradle
index 91ab43a..74df165 100644
--- a/webkit/integration-tests/testapp/build.gradle
+++ b/webkit/integration-tests/testapp/build.gradle
@@ -38,6 +38,7 @@
implementation(libs.guavaAndroid)
implementation(libs.espressoIdlingNet)
implementation(libs.espressoIdlingResource)
+ implementation(libs.okhttpMockwebserver)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
diff --git a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/WebStorageCompatActivityTestAppTest.java b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/WebStorageCompatActivityTestAppTest.java
new file mode 100644
index 0000000..ba76199
--- /dev/null
+++ b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/WebStorageCompatActivityTestAppTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.androidx.webkit;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.webkit.WebViewFeature;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class WebStorageCompatActivityTestAppTest {
+
+ @Rule
+ public ActivityScenarioRule<WebStorageCompatActivity> mRule =
+ new ActivityScenarioRule<>(WebStorageCompatActivity.class);
+
+ @Before
+ public void setUp() {
+ WebkitTestHelpers.assumeFeature(WebViewFeature.DELETE_BROWSING_DATA);
+ WebkitTestHelpers.enableJavaScript(R.id.web_storage_webview);
+ }
+
+ @Test
+ public void testWebStorageCompatActivityLoadsCurrentDate() {
+ // This may be flaky right around midnight.
+ SimpleDateFormat format = new SimpleDateFormat("yyyy.MM.dd");
+ String dateString = format.format(new Date());
+ WebkitTestHelpers.assertHtmlElementContainsText(R.id.web_storage_webview,
+ "timestamp", dateString);
+ }
+}
diff --git a/webkit/integration-tests/testapp/src/main/AndroidManifest.xml b/webkit/integration-tests/testapp/src/main/AndroidManifest.xml
index 45d5d58..b879b40 100644
--- a/webkit/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/webkit/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -168,6 +168,9 @@
<activity
android:name=".DefaultTrafficStatsTaggingActivity"
android:exported="true" />
+ <activity
+ android:name=".WebStorageCompatActivity"
+ android:exported="true" />
<provider
android:authorities="com.example.androidx.webkit.DropDataProvider"
android:name="androidx.webkit.DropDataContentProvider"
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/MainActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/MainActivity.java
index 6bfe6b5..d71ae39 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/MainActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/MainActivity.java
@@ -106,6 +106,9 @@
new MenuListView.MenuItem(
getResources().getString(R.string.default_trafficstats_tagging_activity),
new Intent(activityContext, DefaultTrafficStatsTaggingActivity.class)),
+ new MenuListView.MenuItem(
+ getResources().getString(R.string.web_storage_activity_title),
+ new Intent(activityContext, WebStorageCompatActivity.class)),
};
listView.setItems(menuItems);
}
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/WebStorageCompatActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/WebStorageCompatActivity.java
new file mode 100644
index 0000000..5505995
--- /dev/null
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/WebStorageCompatActivity.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.androidx.webkit;
+
+import static android.webkit.WebSettings.LOAD_DEFAULT;
+import static android.widget.Toast.LENGTH_SHORT;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.webkit.WebStorage;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.Button;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.webkit.WebStorageCompat;
+import androidx.webkit.WebViewFeature;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import okhttp3.mockwebserver.Dispatcher;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+
+public class WebStorageCompatActivity extends AppCompatActivity {
+
+ private static final String TAG = "WebStorageActivity";
+
+ private WebView mWebView;
+ private MockWebServer mMockWebServer;
+
+ private String mPageUrl;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_web_storage);
+ setTitle(R.string.web_storage_activity_title);
+ WebkitHelpers.appendWebViewVersionToTitle(this);
+ if (!WebViewFeature.isFeatureSupported(WebViewFeature.DELETE_BROWSING_DATA)) {
+ WebkitHelpers.showMessageInActivity(WebStorageCompatActivity.this,
+ R.string.webkit_api_not_available);
+ return;
+ }
+
+
+ mWebView = findViewById(R.id.web_storage_webview);
+ mWebView.getSettings().setCacheMode(LOAD_DEFAULT);
+ mWebView.setWebViewClient(new WebViewClient());
+
+ Button loadButton = findViewById(R.id.web_storage_load_page_button);
+ loadButton.setOnClickListener(this::onLoadButton);
+
+ Button deleteButton = findViewById(R.id.web_storage_delete_data_button);
+ deleteButton.setOnClickListener(this::onDeleteButton);
+
+
+ mMockWebServer = new MockWebServer();
+
+ mMockWebServer.setDispatcher(new Dispatcher() {
+ @Override
+ public MockResponse dispatch(RecordedRequest request) {
+ MockResponse response = new MockResponse();
+ if (!request.getPath().equals("/")) {
+ response.setResponseCode(400);
+ return response;
+ }
+
+ try {
+ String template = readHtmlTemplate();
+ response.setHeader("Cache-Control", "max-age=604800");
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z",
+ Locale.US);
+ response.setBody(String.format(template, dateFormat.format(new Date())));
+ } catch (IOException e) {
+ Log.e(TAG, "Error loading html template", e);
+ response.setResponseCode(500);
+ response.setBody("Error loading html template");
+ }
+ return response;
+ }
+ });
+
+ mPageUrl = startMockServerAndGetPageUrl();
+ mWebView.loadUrl(mPageUrl);
+ }
+
+ private String startMockServerAndGetPageUrl() {
+ // The mockWebServer accesses networking APIs during startup and URL construction that
+ // are not allowed on the main thread.
+ FutureTask<String> urlTask = new FutureTask<>(() -> {
+ try {
+ mMockWebServer.start();
+ return mMockWebServer.url("/").toString();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ Executors.newCachedThreadPool().execute(urlTask);
+ try {
+ return urlTask.get(1, TimeUnit.SECONDS);
+ } catch (ExecutionException | InterruptedException | TimeoutException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @NonNull
+ private String readHtmlTemplate() throws IOException {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(
+ getResources().openRawResource(R.raw.web_storage_html_template)))) {
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line).append("\n");
+ }
+ return sb.toString();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (mMockWebServer != null) {
+ try {
+ mMockWebServer.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Error closing mock web server", e);
+ }
+ }
+ }
+
+ private void onLoadButton(View v) {
+ mWebView.loadUrl(mPageUrl);
+ }
+
+ private void onDeleteButton(View v) {
+ WebStorageCompat.deleteBrowsingData(WebStorage.getInstance(), this::onDeletionComplete);
+ }
+
+ private void onDeletionComplete() {
+ Toast.makeText(this, R.string.web_storage_delete_complete, LENGTH_SHORT).show();
+ }
+}
diff --git a/webkit/integration-tests/testapp/src/main/res/layout/activity_web_storage.xml b/webkit/integration-tests/testapp/src/main/res/layout/activity_web_storage.xml
new file mode 100644
index 0000000..1733f5f
--- /dev/null
+++ b/webkit/integration-tests/testapp/src/main/res/layout/activity_web_storage.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2024 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
+
+ http://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.
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <WebView
+ android:id="@+id/web_storage_webview"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ />
+ <Button
+ android:text="@string/web_storage_load_page_button"
+ android:id="@+id/web_storage_load_page_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+ <Button
+ android:text="@string/web_storage_delete_data_button"
+ android:id="@+id/web_storage_delete_data_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/webkit/integration-tests/testapp/src/main/res/raw/web_storage_html_template.html b/webkit/integration-tests/testapp/src/main/res/raw/web_storage_html_template.html
new file mode 100644
index 0000000..ce98c7d
--- /dev/null
+++ b/webkit/integration-tests/testapp/src/main/res/raw/web_storage_html_template.html
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright 2024 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
+ ~
+ ~ http://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.
+ -->
+
+<!DOCTYPE html>
+<html>
+ <body>
+ <h1>Test page for cache deletion</h1>
+ <p>This page was last generated at <span id="timestamp">%s</span></p>
+ <p>This page is marked to be cached for 1 year. It will only be updated if the HTTP cache is
+ cleared.
+ </p>
+ </body>
+</html>
\ No newline at end of file
diff --git a/webkit/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml b/webkit/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
index 34ef53c..b3e39f0 100644
--- a/webkit/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
+++ b/webkit/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
@@ -131,4 +131,11 @@
<!-- TrafficStats tagging -->
<string name="default_trafficstats_tagging_activity">Default TrafficStats Tagging</string>
<string name="default_trafficstats_tagging_unsupported">Default TrafficStats tagging is not supported.</string>
+
+ <!-- WebStorage -->
+ <string name="web_storage_activity_title">WebStorage</string>
+ <string name="web_storage_load_page_button">Reload page</string>
+ <string name="web_storage_delete_data_button">Delete page data</string>
+ <string name="web_storage_delete_complete">Data deletion complete</string>
+
</resources>
diff --git a/webkit/webkit/lint.xml b/webkit/webkit/lint.xml
index 169aa65..a72b8ce 100644
--- a/webkit/webkit/lint.xml
+++ b/webkit/webkit/lint.xml
@@ -2,8 +2,6 @@
<lint>
<!-- Webkit-specific lint rules: -->
- <!-- We cannot cause ClassVerificationFailure in embedding apps -->
- <issue id="ClassVerificationFailure" severity="fatal" />
<!-- Developers need to call our code from Kotlin code, so nullness is important.-->
<issue id="UnknownNullness" severity="fatal" >
<!-- The boundary interfaces are not annotated, and they represent external chromium code,
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java
index 3f44361..20fac75 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java
@@ -16,7 +16,6 @@
package androidx.window.extensions.embedding;
-import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
@@ -51,7 +50,6 @@
/**
* Checks if the rule is applicable to the provided activity.
*/
- @SuppressLint("ClassVerificationFailure") // Only called by Extensions implementation on device.
@RequiresApi(api = Build.VERSION_CODES.N)
public boolean matchesActivity(@NonNull Activity activity) {
return mActivityPredicate.test(activity);
@@ -60,7 +58,6 @@
/**
* Checks if the rule is applicable to the provided activity intent.
*/
- @SuppressLint("ClassVerificationFailure") // Only called by Extensions implementation on device.
@RequiresApi(api = Build.VERSION_CODES.N)
public boolean matchesIntent(@NonNull Intent intent) {
return mIntentPredicate.test(intent);
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
index ed93963..b3d0d7f 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
@@ -18,7 +18,6 @@
import static androidx.window.extensions.embedding.SplitAttributes.SplitType.createSplitTypeFromLegacySplitRatio;
-import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
@@ -65,7 +64,6 @@
/**
* Checks if the rule is applicable to the provided activities.
*/
- @SuppressLint("ClassVerificationFailure") // Only called by Extensions implementation on device.
@RequiresApi(api = Build.VERSION_CODES.N)
public boolean matchesActivityPair(@NonNull Activity primaryActivity,
@NonNull Activity secondaryActivity) {
@@ -76,7 +74,6 @@
* Checks if the rule is applicable to the provided primary activity and secondary activity
* intent.
*/
- @SuppressLint("ClassVerificationFailure") // Only called by Extensions implementation on device.
@RequiresApi(api = Build.VERSION_CODES.N)
public boolean matchesActivityIntentPair(@NonNull Activity primaryActivity,
@NonNull Intent secondaryActivityIntent) {
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java
index 25fb7fd6..9269fb4 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java
@@ -18,7 +18,6 @@
import static androidx.window.extensions.embedding.SplitAttributes.SplitType.createSplitTypeFromLegacySplitRatio;
-import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
@@ -80,7 +79,6 @@
/**
* Checks if the rule is applicable to the provided activity.
*/
- @SuppressLint("ClassVerificationFailure") // Only called by Extensions implementation on device.
@RequiresApi(api = Build.VERSION_CODES.N)
public boolean matchesActivity(@NonNull Activity activity) {
return mActivityPredicate.test(activity);
@@ -89,7 +87,6 @@
/**
* Checks if the rule is applicable to the provided activity intent.
*/
- @SuppressLint("ClassVerificationFailure") // Only called by Extensions implementation on device.
@RequiresApi(api = Build.VERSION_CODES.N)
public boolean matchesIntent(@NonNull Intent intent) {
return mIntentPredicate.test(intent);
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
index 430eb19..8cc5789 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
@@ -16,7 +16,6 @@
package androidx.window.extensions.embedding;
-import android.annotation.SuppressLint;
import android.os.Build;
import android.view.WindowMetrics;
@@ -102,7 +101,6 @@
* @param parentMetrics the {@link WindowMetrics} of the parent window.
* @return whether the parent window satisfied the {@link SplitRule} requirements.
*/
- @SuppressLint("ClassVerificationFailure") // Only called by Extensions implementation on device.
@RequiresApi(api = Build.VERSION_CODES.N)
public boolean checkParentMetrics(@NonNull WindowMetrics parentMetrics) {
return mParentWindowMetricsPredicate.test(parentMetrics);
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
index bff1309..c700fdc 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
@@ -169,7 +169,7 @@
@RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
@OptIn(ExperimentalWindowApi::class)
- @SuppressLint("NewApi", "ClassVerificationFailure")
+ @SuppressLint("NewApi")
internal fun translate(
parentContainerInfo: OEMParentContainerInfo,
): ParentContainerInfo {
@@ -653,7 +653,7 @@
)
.build()
- @SuppressLint("ClassVerificationFailure", "NewApi")
+ @SuppressLint("NewApi")
private fun translateActivityPairPredicates(splitPairFilters: Set<SplitPairFilter>): Any {
return predicateAdapter.buildPairPredicate(Activity::class, Activity::class) {
first: Activity,
@@ -662,7 +662,7 @@
}
}
- @SuppressLint("ClassVerificationFailure", "NewApi")
+ @SuppressLint("NewApi")
private fun translateActivityIntentPredicates(splitPairFilters: Set<SplitPairFilter>): Any {
return predicateAdapter.buildPairPredicate(Activity::class, Intent::class) {
first,
@@ -712,21 +712,21 @@
attrs.splitType.value != 1.0f &&
attrs.layoutDirection in arrayOf(LEFT_TO_RIGHT, RIGHT_TO_LEFT, LOCALE)
- @SuppressLint("ClassVerificationFailure", "NewApi")
+ @SuppressLint("NewApi")
private fun translateActivityPredicates(activityFilters: Set<ActivityFilter>): Any {
return predicateAdapter.buildPredicate(Activity::class) { activity ->
activityFilters.any { filter -> filter.matchesActivity(activity) }
}
}
- @SuppressLint("ClassVerificationFailure", "NewApi")
+ @SuppressLint("NewApi")
private fun translateIntentPredicates(activityFilters: Set<ActivityFilter>): Any {
return predicateAdapter.buildPredicate(Intent::class) { intent ->
activityFilters.any { filter -> filter.matchesIntent(intent) }
}
}
- @SuppressLint("ClassVerificationFailure", "NewApi")
+ @SuppressLint("NewApi")
private fun translateParentMetricsPredicate(context: Context, splitRule: SplitRule): Any =
predicateAdapter.buildPredicate(AndroidWindowMetrics::class) { windowMetrics ->
splitRule.checkParentMetrics(context, windowMetrics)
diff --git a/work/integration-tests/testapp/build.gradle b/work/integration-tests/testapp/build.gradle
index 1feec57..6b3212c 100644
--- a/work/integration-tests/testapp/build.gradle
+++ b/work/integration-tests/testapp/build.gradle
@@ -44,6 +44,7 @@
}
dependencies {
+ api(libs.jspecify)
annotationProcessor(project(":room:room-compiler"))
implementation(project(":room:room-runtime"))
implementation(project(":room:room-ktx"))
@@ -65,8 +66,3 @@
tasks.withType(JavaCompile) {
options.compilerArgs << "-parameters"
}
-
-androidx {
- // TODO: b/326456246
- optOutJSpecify = true
-}
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundLocationWorker.kt b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundLocationWorker.kt
index feecb7e..7df0473 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundLocationWorker.kt
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundLocationWorker.kt
@@ -17,7 +17,6 @@
package androidx.work.integration.testapp
import android.Manifest.permission.ACCESS_COARSE_LOCATION
-import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
@@ -105,7 +104,6 @@
}
@RequiresApi(Build.VERSION_CODES.O)
- @SuppressLint("ClassVerificationFailure")
private fun createChannel() {
val id = applicationContext.getString(R.string.channel_id)
val name = applicationContext.getString(R.string.channel_name)
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt
index d327c6f..caf433e 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt
@@ -16,7 +16,6 @@
package androidx.work.integration.testapp
-import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
@@ -86,7 +85,6 @@
}
@RequiresApi(Build.VERSION_CODES.O)
- @SuppressLint("ClassVerificationFailure")
private fun createChannel() {
val id = applicationContext.getString(R.string.channel_id)
val name = applicationContext.getString(R.string.channel_name)
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/InfiniteWorker.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/InfiniteWorker.java
index 9ecb6ab..38acaf6 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/InfiniteWorker.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/InfiniteWorker.java
@@ -18,10 +18,11 @@
import android.content.Context;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
public class InfiniteWorker extends Worker {
public InfiniteWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.kt b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.kt
index 18f87d9..de41686 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.kt
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.kt
@@ -15,7 +15,6 @@
*/
package androidx.work.integration.testapp
-import android.annotation.SuppressLint
import android.app.PendingIntent
import android.app.job.JobInfo
import android.app.job.JobScheduler
@@ -69,7 +68,6 @@
private var lastNotificationId = 10
private val workManager: WorkManager by lazy { WorkManager.getInstance(this) }
- @SuppressLint("ClassVerificationFailure")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
@@ -443,7 +441,6 @@
}
}
-@SuppressLint("ClassVerificationFailure")
private fun enqueueWithNetworkRequest(workManager: WorkManager) {
if (Build.VERSION.SDK_INT < 21) {
Log.w(TAG, "Ignoring enqueueWithNetworkRequest on old API levels")
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RecursiveWorker.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RecursiveWorker.java
index 6c32e75..95a254f 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RecursiveWorker.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RecursiveWorker.java
@@ -18,12 +18,13 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
import java.util.concurrent.TimeUnit;
/**
@@ -37,9 +38,8 @@
super(context, parameters);
}
- @NonNull
@Override
- public Result doWork() {
+ public @NonNull Result doWork() {
OneTimeWorkRequest newRequest = new OneTimeWorkRequest.Builder(RecursiveWorker.class)
.addTag(TAG)
.setInitialDelay(100, TimeUnit.MILLISECONDS)
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RemoteWorker.kt b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RemoteWorker.kt
index bc312d9..2e4b4d7 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RemoteWorker.kt
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RemoteWorker.kt
@@ -16,7 +16,6 @@
package androidx.work.integration.testapp
-import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
@@ -110,7 +109,6 @@
}
@RequiresApi(Build.VERSION_CODES.O)
- @SuppressLint("ClassVerificationFailure")
private fun createChannel() {
val id = applicationContext.getString(R.string.channel_id)
val name = applicationContext.getString(R.string.channel_name)
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RetryWorker.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RetryWorker.java
index c9be7b4..70dfdec 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RetryWorker.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RetryWorker.java
@@ -35,10 +35,11 @@
import android.content.Context;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
/**
* A {@link Worker} which always returns a {@link Result#retry()}.
*/
@@ -51,9 +52,8 @@
super(context, parameters);
}
- @NonNull
@Override
- public Result doWork() {
+ public @NonNull Result doWork() {
Log.e(TAG, "Requesting retry (" + getRunAttemptCount() + ")");
return Result.retry();
}
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/SleepWorker.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/SleepWorker.java
index d6122f5..8aed98ac 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/SleepWorker.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/SleepWorker.java
@@ -19,10 +19,11 @@
import android.content.Context;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
/**
* A test worker that sleeps.
*/
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/TestApplication.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/TestApplication.java
index 54cff00..1d97777 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/TestApplication.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/TestApplication.java
@@ -19,9 +19,10 @@
import android.app.Application;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.work.Configuration;
+import org.jspecify.annotations.NonNull;
+
import java.util.concurrent.Executors;
/**
@@ -29,9 +30,8 @@
*/
public class TestApplication extends Application implements Configuration.Provider {
- @NonNull
@Override
- public Configuration getWorkManagerConfiguration() {
+ public @NonNull Configuration getWorkManagerConfiguration() {
return new Configuration.Builder()
.setDefaultProcessName(getPackageName())
.setTaskExecutor(Executors.newCachedThreadPool())
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/TestWorker.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/TestWorker.java
index b9798b6..f8f4ac4 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/TestWorker.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/TestWorker.java
@@ -17,10 +17,11 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
/**
* A Test Worker.
*/
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ToastWorker.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ToastWorker.java
index fb02bd2..cc417a6 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ToastWorker.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ToastWorker.java
@@ -21,12 +21,13 @@
import android.util.Log;
import android.widget.Toast;
-import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.OneTimeWorkRequest;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
import java.util.Date;
/**
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/db/Image.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/db/Image.java
index 5bdbf93..329ba9b 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/db/Image.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/db/Image.java
@@ -17,19 +17,19 @@
import android.graphics.Bitmap;
-import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
+import org.jspecify.annotations.NonNull;
+
/**
* A POJO for a processed image
*/
@Entity
public class Image {
@PrimaryKey
- @NonNull
- public String mOriginalAssetName;
+ public @NonNull String mOriginalAssetName;
public String mProcessedFilePath;
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/db/WordCount.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/db/WordCount.java
index 78f4d86..361a021 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/db/WordCount.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/db/WordCount.java
@@ -15,10 +15,11 @@
*/
package androidx.work.integration.testapp.db;
-import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
+import org.jspecify.annotations.NonNull;
+
/**
* A POJO for a word and its count.
*/
@@ -26,7 +27,6 @@
public class WordCount {
@PrimaryKey
- @NonNull
- public String mWord;
+ public @NonNull String mWord;
public int mCount;
}
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageCleanupWorker.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageCleanupWorker.java
index 1d68a91..f39bb4f 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageCleanupWorker.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageCleanupWorker.java
@@ -20,12 +20,13 @@
import android.text.TextUtils;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import androidx.work.integration.testapp.db.Image;
import androidx.work.integration.testapp.db.TestDatabase;
+import org.jspecify.annotations.NonNull;
+
import java.io.File;
import java.util.List;
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageProcessingWorker.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageProcessingWorker.java
index e4c7cd3..37c317c 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageProcessingWorker.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageProcessingWorker.java
@@ -23,7 +23,6 @@
import android.text.TextUtils;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.OneTimeWorkRequest;
import androidx.work.Worker;
@@ -31,6 +30,8 @@
import androidx.work.integration.testapp.db.Image;
import androidx.work.integration.testapp.db.TestDatabase;
+import org.jspecify.annotations.NonNull;
+
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageSetupWorker.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageSetupWorker.java
index be0cc69..56744d8 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageSetupWorker.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/imageprocessing/ImageSetupWorker.java
@@ -20,7 +20,6 @@
import android.text.TextUtils;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.OneTimeWorkRequest;
import androidx.work.Worker;
@@ -28,6 +27,8 @@
import androidx.work.integration.testapp.db.Image;
import androidx.work.integration.testapp.db.TestDatabase;
+import org.jspecify.annotations.NonNull;
+
/**
* Creates initial {@link Image} entity in db
*/
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/TextMappingWorker.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/TextMappingWorker.java
index 08bde6e..e02fbe4 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/TextMappingWorker.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/TextMappingWorker.java
@@ -18,12 +18,13 @@
import android.content.Context;
import android.content.res.AssetManager;
-import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.OneTimeWorkRequest;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/TextReducingWorker.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/TextReducingWorker.java
index a0c9622..8237e3b 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/TextReducingWorker.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/TextReducingWorker.java
@@ -17,13 +17,14 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import androidx.work.integration.testapp.db.TestDatabase;
import androidx.work.integration.testapp.db.WordCount;
+import org.jspecify.annotations.NonNull;
+
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/TextStartupWorker.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/TextStartupWorker.java
index f2add48..463a2ef 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/TextStartupWorker.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/sherlockholmes/TextStartupWorker.java
@@ -18,11 +18,12 @@
import android.content.Context;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import androidx.work.integration.testapp.db.TestDatabase;
+import org.jspecify.annotations.NonNull;
+
/**
* A Worker that deletes the final results file.
*/
diff --git a/work/work-datatransfer/build.gradle b/work/work-datatransfer/build.gradle
deleted file mode 100644
index 3d6d0a6..0000000
--- a/work/work-datatransfer/build.gradle
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2023 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
- *
- * http://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.
- */
-
-/**
- * This file was created using the `create_project.py` script located in the
- * `<AndroidX root>/development/project-creator` directory.
- *
- * Please use that script when creating a new project, rather than copying an existing project and
- * modifying its settings.
- */
-import androidx.build.Publish
-
-plugins {
- id("AndroidXPlugin")
- id("com.android.library")
- id("kotlin-android")
-}
-
-android {
- compileSdk = 35
- defaultConfig {
- minSdk = 23
- }
- namespace = "androidx.work.datatransfer"
-}
-
-dependencies {
- api(libs.kotlinStdlib)
- api(libs.kotlinCoroutinesAndroid)
- implementation(project(":work:work-runtime-ktx"))
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.testExtJunit)
- androidTestImplementation(libs.testRunner)
-}
-
-androidx {
- name = "WorkManager Data Transfer"
- publish = Publish.SNAPSHOT_ONLY
- inceptionYear = "2023"
- description = "Android WorkManager Data Transfer library"
-}
diff --git a/work/work-datatransfer/src/androidTest/java/androidx/work/datatransfer/ConstraintsTest.kt b/work/work-datatransfer/src/androidTest/java/androidx/work/datatransfer/ConstraintsTest.kt
deleted file mode 100644
index 4945103..0000000
--- a/work/work-datatransfer/src/androidTest/java/androidx/work/datatransfer/ConstraintsTest.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx.work.datatransfer
-
-import android.net.NetworkCapabilities
-import android.net.NetworkRequest
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNotEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-@SmallTest
-class ConstraintsTest {
-
- @Test
- fun testDefaultNetworkRequirements() {
- val constraints1 = Constraints(getDefaultNetworkRequest())
- val constraints2 = Constraints()
- assertEquals(constraints1, constraints2)
-
- val constraints3 =
- Constraints(
- NetworkRequest.Builder()
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
- .build()
- )
- assertNotEquals(constraints2, constraints3)
- }
-
- @Test
- fun testEquals() {
- val constraints1 = Constraints(getDefaultNetworkRequest())
- val constraints2 = Constraints(getDefaultNetworkRequest())
- assertEquals(constraints1, constraints2)
-
- val constraints3 =
- Constraints(
- NetworkRequest.Builder()
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
- .build()
- )
- assertNotEquals(constraints2, constraints3)
- }
-
- @Test
- fun testCopyFrom() {
- val constraints1 = Constraints(getDefaultNetworkRequest())
- val constraints2 = Constraints.copyFrom(constraints1)
- assertEquals(constraints1, constraints2)
-
- val constraints3 =
- Constraints(
- NetworkRequest.Builder()
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
- .build()
- )
- assertNotEquals(constraints2, constraints3)
- }
-
- private fun getDefaultNetworkRequest(): NetworkRequest {
- return NetworkRequest.Builder()
- .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
- .build()
- }
-}
diff --git a/work/work-datatransfer/src/androidTest/java/androidx/work/datatransfer/UserInitiatedTaskRequestTest.kt b/work/work-datatransfer/src/androidTest/java/androidx/work/datatransfer/UserInitiatedTaskRequestTest.kt
deleted file mode 100644
index 1a4a410..0000000
--- a/work/work-datatransfer/src/androidTest/java/androidx/work/datatransfer/UserInitiatedTaskRequestTest.kt
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx.work.datatransfer
-
-import android.content.Intent
-import android.net.NetworkCapabilities
-import android.net.NetworkRequest
-import android.os.IBinder
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import kotlinx.coroutines.runBlocking
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNotEquals
-import org.junit.Assert.assertNull
-import org.junit.Assert.assertTrue
-import org.junit.Assert.fail
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-@SmallTest
-class UserInitiatedTaskRequestTest {
-
- @Test
- fun testDefaultNetworkConstraints() {
- val request = UserInitiatedTaskRequest(MyTask::class.java)
- val networkRequest =
- NetworkRequest.Builder()
- .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
- .build()
- assertEquals(request.constraints.networkRequest, networkRequest)
-
- val networkRequest2 =
- NetworkRequest.Builder()
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
- .build()
- assertNotEquals(request.constraints.networkRequest, networkRequest2)
- }
-
- @Test
- fun testCustomNetworkConstraints() {
- val request =
- UserInitiatedTaskRequest(
- MyTask::class.java,
- _constraints =
- Constraints(
- NetworkRequest.Builder()
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
- .build()
- )
- )
- val networkRequest =
- NetworkRequest.Builder()
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
- .build()
- assertEquals(request.constraints.networkRequest, networkRequest)
-
- val networkRequest2 =
- NetworkRequest.Builder()
- .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
- .build()
- assertNotEquals(request.constraints.networkRequest, networkRequest2)
- }
-
- @Test
- fun testTags() {
- val taskClassName = "androidx.work.datatransfer.UserInitiatedTaskRequestTest\$MyTask"
- var request = UserInitiatedTaskRequest(MyTask::class.java)
- assertEquals(1, request.tags.size)
- assertEquals(taskClassName, request.tags.get(0))
-
- request = UserInitiatedTaskRequest(MyTask::class.java, _tags = mutableListOf("test"))
- assertEquals(2, request.tags.size)
- assertTrue(request.tags.contains("test"))
-
- request =
- UserInitiatedTaskRequest(MyTask::class.java, _tags = mutableListOf("test", "test2"))
- assertEquals(3, request.tags.size)
- assertTrue(request.tags.contains(taskClassName))
- assertTrue(request.tags.contains("test2"))
- assertTrue(request.tags.contains("test"))
- }
-
- @Test
- fun testDefaultTransferInfo() {
- val request = UserInitiatedTaskRequest(MyTask::class.java)
- assertNull(request.transferInfo)
- }
-
- @Test
- fun testCustomTransferInfo() {
- var request =
- UserInitiatedTaskRequest(
- MyTask::class.java,
- _transferInfo = TransferInfo(estimatedDownloadBytes = 1000L)
- )
- val transferInfo = TransferInfo(0L, 1000L)
- assertEquals(request.transferInfo, transferInfo)
-
- request =
- UserInitiatedTaskRequest(
- MyTask::class.java,
- _transferInfo = TransferInfo(estimatedUploadBytes = 1000L)
- )
- val transferInfo2 = TransferInfo(1000L, 0L)
- assertEquals(request.transferInfo, transferInfo2)
- assertNotEquals(request.transferInfo, transferInfo)
-
- request =
- UserInitiatedTaskRequest(MyTask::class.java, _transferInfo = TransferInfo(2000L, 20L))
- val transferInfo3 = TransferInfo(2000L, 20L)
- assertEquals(request.transferInfo, transferInfo3)
- }
-
- @Test
- fun testDefaultFallbackPolicy(): Unit = runBlocking {
- // Default policy FALLBACK_NONE should allow enqueue
- val request = UserInitiatedTaskRequest(MyTask::class.java)
- request.enqueue(ApplicationProvider.getApplicationContext())
- }
-
- @Test
- fun testCustomFallbackPolicy(): Unit = runBlocking {
- val request =
- UserInitiatedTaskRequest(
- MyTask::class.java,
- fallbackPolicy =
- UserInitiatedTaskRequest.FallbackPolicy.FALLBACK_TO_FOREGROUND_SERVICE
- )
- try {
- request.enqueue(ApplicationProvider.getApplicationContext())
- fail("Expected enqueue to fail without setting a foreground service")
- } catch (_: IllegalArgumentException) {
- // expected
- }
-
- request.setForegroundService(
- MyFgs::class.java,
- UserInitiatedTaskRequest.ForegroundServiceOnTaskFinishPolicy.FOREGROUND_SERVICE_DETACH
- )
- request.enqueue(ApplicationProvider.getApplicationContext())
- }
-
- private class MyTask :
- UserInitiatedTask("test_task", ApplicationProvider.getApplicationContext()) {
- override suspend fun performTask() {
- // test stub
- }
-
- override suspend fun createForegroundInfo(): UitForegroundInfo {
- // test stub
- TODO()
- }
- }
-
- private class MyFgs : AbstractUitService() {
- override fun handleOnStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- // test stub
- return START_STICKY
- }
-
- override fun handleOnDestroyCommand() {
- // test stub
- }
-
- override fun onBind(p0: Intent?): IBinder? {
- return null
- }
- }
-}
diff --git a/work/work-datatransfer/src/main/java/androidx/work/datatransfer/AbstractUitService.kt b/work/work-datatransfer/src/main/java/androidx/work/datatransfer/AbstractUitService.kt
deleted file mode 100644
index dd51382..0000000
--- a/work/work-datatransfer/src/main/java/androidx/work/datatransfer/AbstractUitService.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx.work.datatransfer
-
-import android.app.Notification
-import android.app.Service
-import android.content.Intent
-
-/**
- * App developers should migrate their existing foreground service implementation to this new base
- * class instead of [Service].
- *
- * App developers are not supposed to call [Service.stopForeground] (or even [Service.stopService])
- * on their own, otherwise this library will crash unexpectedly.
- */
-abstract class AbstractUitService : Service() {
-
- /**
- * This is an equivalent of the [Service.onStartCommand], however, developers should override
- * this method instead of the [Service.onStartCommand]. Its return value will be honored if
- * there are no pending/active [UserInitiatedTask]s, otherwise the return value will be ignored.
- */
- abstract fun handleOnStartCommand(intent: Intent?, flags: Int, startId: Int): Int
-
- /**
- * This is an equivalent of [Service.onDestroy]. Developers should override this method instead.
- */
- abstract fun handleOnDestroyCommand()
-
- /**
- * Optional method that can be overridden by apps. Apps can implement their own policy for how
- * multiplexing for task notifications will behave.
- *
- * **The default notification policy is FIFO.**
- */
- open fun handleTaskNotification(id: Int, notification: Notification) {
- TODO()
- }
-
- final override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- return super.onStartCommand(intent, flags, startId)
- }
-
- final override fun onDestroy() {
- // TODO: notify UserInitiatedTaskManager to stop all running tasks
- super.onDestroy()
- }
-}
diff --git a/work/work-datatransfer/src/main/java/androidx/work/datatransfer/Constraints.kt b/work/work-datatransfer/src/main/java/androidx/work/datatransfer/Constraints.kt
deleted file mode 100644
index 763e14f..0000000
--- a/work/work-datatransfer/src/main/java/androidx/work/datatransfer/Constraints.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx.work.datatransfer
-
-import android.net.NetworkCapabilities
-import android.net.NetworkRequest
-
-/**
- * A specification of the requirements that need to be met before a [UserInitiatedTaskRequest] can
- * run. By default, UserInitiatedTaskRequest only require a network constraint to be specified. By
- * adding additional constraints, you can make sure that work only runs in certain situations - for
- * example, when you have an unmetered network and are charging.
- */
-class Constraints
-constructor(
- /**
- * The network request required for the work to run.
- *
- * **The default value assumes a requirement of any internet.**
- */
- private val requiredNetwork: NetworkRequest = getDefaultNetworkRequest()
-) {
- val networkRequest: NetworkRequest
- get() = requiredNetwork
-
- companion object {
- /** Copies an existing [Constraints] object. */
- @JvmStatic
- fun copyFrom(constraints: Constraints): Constraints {
- return Constraints(constraints.requiredNetwork)
- }
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other == null || javaClass != other.javaClass) return false
- val that = other as Constraints
- return requiredNetwork == that.requiredNetwork
- }
-
- override fun hashCode(): Int {
- return 31 * requiredNetwork.hashCode()
- }
-}
-
-fun getDefaultNetworkRequest(): NetworkRequest {
- return NetworkRequest.Builder()
- .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
- .build()
-}
diff --git a/work/work-datatransfer/src/main/java/androidx/work/datatransfer/TransferInfo.kt b/work/work-datatransfer/src/main/java/androidx/work/datatransfer/TransferInfo.kt
deleted file mode 100644
index 6769f2c..0000000
--- a/work/work-datatransfer/src/main/java/androidx/work/datatransfer/TransferInfo.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx.work.datatransfer
-
-/**
- * A container class holding byte transfer information related to a [UserInitiatedTaskRequest].
- * Specifically, the estimated number of upload or download bytes which are expected to be
- * transferred.
- */
-class TransferInfo(
- /** The estimated number of bytes to be uploaded, if applicable. */
- val estimatedUploadBytes: Long = 0L,
- /** The estimated number of bytes to be downloaded, if applicable. */
- val estimatedDownloadBytes: Long = 0L
-) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other == null || javaClass != other.javaClass) return false
- val that = other as TransferInfo
- return estimatedUploadBytes == that.estimatedUploadBytes &&
- estimatedDownloadBytes == that.estimatedDownloadBytes
- }
-
- override fun hashCode(): Int {
- var result = (estimatedUploadBytes xor (estimatedUploadBytes ushr 32)).toInt()
- result = 31 * result + (estimatedDownloadBytes xor (estimatedDownloadBytes ushr 32)).toInt()
- return result
- }
-}
diff --git a/work/work-datatransfer/src/main/java/androidx/work/datatransfer/UitForegroundInfo.kt b/work/work-datatransfer/src/main/java/androidx/work/datatransfer/UitForegroundInfo.kt
deleted file mode 100644
index b3c9f91..0000000
--- a/work/work-datatransfer/src/main/java/androidx/work/datatransfer/UitForegroundInfo.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx.work.datatransfer
-
-import android.app.Notification
-import android.content.pm.ServiceInfo
-import androidx.work.ForegroundInfo
-
-/**
- * A container class holding information related to the notifications for the
- * [UserInitiatedTaskRequest].
- */
-class UitForegroundInfo(
- /** The notification id of the notification to be associated with the foreground service. */
- val notificationId: Int,
- /** The notification object to be associated with the foreground service. */
- val notification: Notification,
- /**
- * The foreground service type for the foreground service associated with the task request.
- *
- * This is not required to be specified on API versions below 29.
- *
- * The default type here will be [ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE]. However, on API
- * versions 34 and above, a different type must be specified otherwise an
- * [InvalidForegroundServiceTypeException] will be thrown.
- */
- @Suppress("DEPRECATION") val fgsType: Int = ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE,
- /**
- * Indicates what should be done with the notification after the foreground service is finished.
- *
- * **By default, the notification will be removed (see
- * [TaskEndNotificationPolicy.NOTIFICATION_REMOVE])**
- */
- val taskEndNotificationPolicy: TaskEndNotificationPolicy =
- TaskEndNotificationPolicy.NOTIFICATION_REMOVE
-) {
- /** Internal container variable pointing to the [ForegroundInfo] object in WorkManager. */
- private val foregroundInfo: ForegroundInfo =
- ForegroundInfo(notificationId, notification, fgsType)
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other == null || javaClass != other.javaClass) return false
- val that = other as UitForegroundInfo
- return foregroundInfo == that.foregroundInfo &&
- taskEndNotificationPolicy == that.taskEndNotificationPolicy
- }
-
- override fun hashCode(): Int {
- var result = foregroundInfo.hashCode()
- result = 31 * result + taskEndNotificationPolicy.hashCode()
- return result
- }
-}
-
-enum class TaskEndNotificationPolicy {
- /**
- * This indicates that the notification will be removed when the task is finished.
- *
- * **This is the default behavior.**
- */
- NOTIFICATION_REMOVE,
- /**
- * This indicates that the notification will be detached from the foreground service, but not
- * removed so it can still be modified if needed.
- */
- NOTIFICATION_DETACH
-}
diff --git a/work/work-datatransfer/src/main/java/androidx/work/datatransfer/UserInitiatedTask.kt b/work/work-datatransfer/src/main/java/androidx/work/datatransfer/UserInitiatedTask.kt
deleted file mode 100644
index 9165321..0000000
--- a/work/work-datatransfer/src/main/java/androidx/work/datatransfer/UserInitiatedTask.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx.work.datatransfer
-
-import android.app.job.JobParameters
-import android.content.Context
-import java.util.concurrent.CancellationException
-
-/** A class that can perform work asynchronously in UserInitiatedTaskManager. */
-public abstract class UserInitiatedTask(
- /** A unique identifier for the task. */
- val name: String,
- /** The application context. */
- val appContext: Context
-) {
-
- /**
- * Override this method to start your actual data transfer work. This method is called on the
- * main thread by default.
- *
- * If the task is cancelled, the app will get a [CancellationException], with one of the
- * following messages:
- * - [JobParameters.STOP_REASON_CONSTRAINT_CONNECTIVITY]
- * - [JobParameters.STOP_REASON_DEVICE_STATE]
- * - [JobParameters.STOP_REASON_TIMEOUT]
- * - [JobParameters.STOP_REASON_USER]
- */
- abstract suspend fun performTask()
-
- /**
- * Override this method to provide the notification information associated with your work.
- *
- * On Android 14 and above, the notification will be delegated to the dedicated JobService.
- * While on Android 14- devices, the default policy to this API is FIFO: whoever calls this API
- * later will get posted as a foreground service notification here. The notifications from the
- * previous calls to this method would be posted as regular notifications (unless they have the
- * same notification ID).
- *
- * To change this behavior on Android 14-, override [AbstractUitService.handleTaskNotification]
- */
- abstract suspend fun createForegroundInfo(): UitForegroundInfo
-}
diff --git a/work/work-datatransfer/src/main/java/androidx/work/datatransfer/UserInitiatedTaskRequest.kt b/work/work-datatransfer/src/main/java/androidx/work/datatransfer/UserInitiatedTaskRequest.kt
deleted file mode 100644
index 8b46b3c..0000000
--- a/work/work-datatransfer/src/main/java/androidx/work/datatransfer/UserInitiatedTaskRequest.kt
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright 2023 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
- *
- * http://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 androidx.work.datatransfer
-
-import android.content.Context
-import java.util.UUID
-
-/**
- * The base class for specifying parameters for network based data transfer work that should be
- * enqueued in DataTransferTaskManager.
- */
-class UserInitiatedTaskRequest
-constructor(
- private val task: Class<out UserInitiatedTask>,
- /** [FallbackPolicy] indicating what the library should do on Android 14- devices. */
- private val fallbackPolicy: FallbackPolicy = FallbackPolicy.FALLBACK_NONE,
- /**
- * [Constraints] required for this task to run. The default value assumes a requirement of any
- * internet.
- */
- private val _constraints: Constraints = Constraints(),
- /**
- * Sets the appropriate estimated upload/download byte info of the data transfer request via the
- * [TransferInfo] object.
- */
- private val _transferInfo: TransferInfo? = null,
- /**
- * A list of tags associated to this work. You can query and cancel work by tags. Tags are
- * particularly useful for modules or libraries to find and operate on their own work.
- */
- private val _tags: MutableList<String> = mutableListOf()
-) {
- /** The unique identifier associated with this unit of work. */
- private val id: UUID = UUID.randomUUID()
- val stringId: String
- get() = id.toString()
-
- val constraints: Constraints
- get() = _constraints
-
- val transferInfo: TransferInfo?
- get() = _transferInfo
-
- val tags: List<String>
- get() = _tags
-
- /**
- * The foreground service which will be used as a fallback solution on Android 14- devices. This
- * is only used if [FallbackPolicy.FALLBACK_TO_FOREGROUND_SERVICE] is set.
- */
- var service: Class<out AbstractUitService>? = null
-
- /**
- * [ForegroundServiceOnTaskFinishPolicy] indicating what should occur when the task is finished.
- */
- var onTaskFinishPolicy: ForegroundServiceOnTaskFinishPolicy =
- ForegroundServiceOnTaskFinishPolicy.FOREGROUND_SERVICE_STOP_FOREGROUND
-
- init {
- // Update the list of tags to include the UserInitiatedTask class name if available
- _tags += task.name
- }
-
- /**
- * Set the [AbstractUitService] service to fallback to on Android 14- devices along with a
- * [ForegroundServiceOnTaskFinishPolicy] policy which defines what will happen when the task is
- * finished.
- *
- * This is only used if [FallbackPolicy.FALLBACK_TO_FOREGROUND_SERVICE] is set. If this method
- * is not called and [FallbackPolicy.FALLBACK_TO_FOREGROUND_SERVICE] is set, an exception will
- * be thrown when the task is enqueued.
- *
- * Upon scheduling the task request, the library will call [Context.startForegroundService] with
- * [ACTION_UIT_SCHEDULE] on the given service here. The app needs to call
- * [android.app.Service.startForeground] within a certain amount of time, otherwise it will
- * crash with a [android.app.ForegroundServiceDidNotStartInTimeException].
- */
- fun setForegroundService(
- service: Class<out AbstractUitService>,
- onTaskFinishPolicy: ForegroundServiceOnTaskFinishPolicy =
- ForegroundServiceOnTaskFinishPolicy.FOREGROUND_SERVICE_STOP_FOREGROUND
- ) {
- this.service = service
- this.onTaskFinishPolicy = onTaskFinishPolicy
- }
-
- internal fun getTaskState(): TaskState {
- return TaskState.TASK_STATE_INVALID // TODO: update impl
- }
-
- suspend fun enqueue(@Suppress("UNUSED_PARAMETER") context: Context) {
- if (
- this.fallbackPolicy == FallbackPolicy.FALLBACK_TO_FOREGROUND_SERVICE &&
- this.service == null
- ) {
- throw IllegalArgumentException(
- "FallbackPolicy.FALLBACK_TO_FOREGROUND_SERVICE is set," +
- " but a foreground service has not been set via setForegroundService()."
- )
- }
- // TODO: update impl
- }
-
- suspend fun cancel() {
- // TODO: update impl
- }
-
- companion object {
- const val ACTION_UIT_SCHEDULE =
- "androidx.work.datatransfer.UserInitiatedTaskRequest.SCHEDULE"
- }
-
- enum class FallbackPolicy {
- /**
- * Indicates to the library that it should not do anything on Android 14- devices. The
- * developer will perform the data transfer task on previous versions of Android with their
- * own logic.
- *
- * **This is the default policy.**
- */
- FALLBACK_NONE,
-
- /**
- * Indicates that the developer will provide an implementation of [AbstractUitService] which
- * will be used by the library to perform the data transfer work on Android 14- devices.
- *
- * _The foreground service fallback will act as a best effort to perform the data transfer
- * work on Android 14- devices._
- */
- FALLBACK_TO_FOREGROUND_SERVICE,
- }
-
- enum class ForegroundServiceOnTaskFinishPolicy {
- /**
- * This indicates that the foreground service should be stopped when the job is done. This
- * is the default behavior.
- */
- FOREGROUND_SERVICE_STOP_FOREGROUND,
-
- /**
- * This indicates that the foreground service should be left as is when the job is done and
- * the app will manage its lifecycle.
- */
- FOREGROUND_SERVICE_DETACH,
- }
-
- /** The internal definition of the task states. */
- internal enum class TaskState {
- /** Not a valid state. */
- TASK_STATE_INVALID,
-
- /**
- * The task has been scheduled but hasn't been put into execution, it may be waiting for the
- * constraints. Or, it used to be running, but the constraint are no longer met, so the task
- * was stopped.
- */
- TASK_STATE_SCHEDULED,
-
- /** The task is being executed. */
- TASK_STATE_EXECUTING,
-
- /** The task has finished. */
- TASK_STATE_FINISHED,
- }
-}
diff --git a/work/work-gcm/build.gradle b/work/work-gcm/build.gradle
index 4bc3d59..0f26ed1 100644
--- a/work/work-gcm/build.gradle
+++ b/work/work-gcm/build.gradle
@@ -38,7 +38,8 @@
}
dependencies {
- api project(":work:work-runtime")
+ api(libs.jspecify)
+ api(project(":work:work-runtime"))
implementation(libs.gcmNetworkManager)
annotationProcessor("androidx.room:room-compiler:2.5.0")
implementation("androidx.room:room-runtime:2.5.0")
@@ -60,6 +61,4 @@
inceptionYear = "2019"
description = "Android WorkManager GCMNetworkManager Support"
legacyDisableKotlinStrictApiMode = true
- // TODO: b/326456246
- optOutJSpecify = true
}
diff --git a/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmScheduler.java b/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmScheduler.java
index 71f595e..11545db 100644
--- a/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmScheduler.java
+++ b/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmScheduler.java
@@ -18,7 +18,6 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.work.Clock;
import androidx.work.Logger;
import androidx.work.impl.Scheduler;
@@ -29,6 +28,8 @@
import com.google.android.gms.gcm.GcmNetworkManager;
import com.google.android.gms.gcm.Task;
+import org.jspecify.annotations.NonNull;
+
/**
* The {@link androidx.work.WorkManager} scheduler which uses
* {@link com.google.android.gms.gcm.GcmNetworkManager}.
@@ -50,7 +51,7 @@
}
@Override
- public void schedule(@NonNull WorkSpec... workSpecs) {
+ public void schedule(WorkSpec @NonNull ... workSpecs) {
for (WorkSpec workSpec : workSpecs) {
Task task = mTaskConverter.convert(workSpec);
Logger.get().debug(TAG, "Scheduling " + workSpec + "with " + task);
diff --git a/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java b/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java
index fb6e1ae..c20fbae9 100644
--- a/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java
+++ b/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java
@@ -25,7 +25,6 @@
import android.os.Build;
import android.os.Bundle;
-import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.work.Clock;
import androidx.work.Constraints;
@@ -36,6 +35,7 @@
import com.google.android.gms.gcm.OneoffTask;
import com.google.android.gms.gcm.Task;
+import org.jspecify.annotations.NonNull;
/**
* Converts a {@link androidx.work.impl.model.WorkSpec} to a {@link Task}.
@@ -90,7 +90,7 @@
}
private static Task.Builder applyConstraints(
- @NonNull Task.Builder builder,
+ Task.@NonNull Builder builder,
@NonNull WorkSpec workSpec) {
// Apply defaults
diff --git a/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcher.java b/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcher.java
index faf8487..0dfd345 100644
--- a/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcher.java
+++ b/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcher.java
@@ -21,7 +21,6 @@
import android.os.PowerManager;
import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
import androidx.work.Logger;
import androidx.work.WorkInfo;
import androidx.work.impl.ExecutionListener;
@@ -41,6 +40,8 @@
import com.google.android.gms.gcm.GcmNetworkManager;
import com.google.android.gms.gcm.TaskParams;
+import org.jspecify.annotations.NonNull;
+
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -160,7 +161,7 @@
}
}
- private int reschedule(@NonNull final String workSpecId) {
+ private int reschedule(final @NonNull String workSpecId) {
final WorkDatabase workDatabase = mWorkManagerImpl.getWorkDatabase();
workDatabase.runInTransaction(new Runnable() {
@Override
diff --git a/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/WorkManagerGcmService.java b/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/WorkManagerGcmService.java
index bd99dab..0849695 100644
--- a/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/WorkManagerGcmService.java
+++ b/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/WorkManagerGcmService.java
@@ -18,7 +18,6 @@
import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
import androidx.work.Logger;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.utils.WorkTimer;
@@ -26,6 +25,8 @@
import com.google.android.gms.gcm.GcmTaskService;
import com.google.android.gms.gcm.TaskParams;
+import org.jspecify.annotations.NonNull;
+
/**
* The {@link GcmTaskService} responsible for handling requests for executing
* {@link androidx.work.WorkRequest}s.
diff --git a/work/work-multiprocess/build.gradle b/work/work-multiprocess/build.gradle
index 937a820..8643119 100644
--- a/work/work-multiprocess/build.gradle
+++ b/work/work-multiprocess/build.gradle
@@ -38,7 +38,8 @@
}
dependencies {
- api project(":work:work-runtime")
+ api(libs.jspecify)
+ api(project(":work:work-runtime"))
api(libs.kotlinStdlib)
api(libs.kotlinCoroutinesAndroid)
api(libs.guavaListenableFuture)
@@ -60,6 +61,4 @@
description = "Android WorkManager runtime library"
failOnDeprecationWarnings = false
legacyDisableKotlinStrictApiMode = true
- // TODO: b/326456246
- optOutJSpecify = true
}
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ListenableCallback.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ListenableCallback.java
index ed49d93..fd89df0 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ListenableCallback.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ListenableCallback.java
@@ -18,12 +18,13 @@
import android.os.RemoteException;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.Logger;
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+
import java.util.concurrent.Executor;
/**
@@ -53,8 +54,7 @@
/**
* Transforms the result of the {@link ListenableFuture} to a byte array.
*/
- @NonNull
- public abstract byte[] toByteArray(@NonNull I result);
+ public abstract byte @NonNull [] toByteArray(@NonNull I result);
/**
* Dispatches callbacks safely while handling {@link android.os.RemoteException}s.
@@ -90,7 +90,7 @@
*/
public static void reportSuccess(
@NonNull IWorkManagerImplCallback callback,
- @NonNull byte[] response) {
+ byte @NonNull [] response) {
try {
callback.onSuccess(response);
} catch (RemoteException exception) {
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImpl.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImpl.java
index 36eeb32..78185e1 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImpl.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImpl.java
@@ -22,7 +22,6 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.Configuration;
import androidx.work.ForegroundInfo;
@@ -41,6 +40,8 @@
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CancellationException;
@@ -90,8 +91,8 @@
@Override
public void startWork(
- @NonNull final byte[] request,
- @NonNull final IWorkManagerImplCallback callback) {
+ final byte @NonNull [] request,
+ final @NonNull IWorkManagerImplCallback callback) {
try {
ParcelableRemoteWorkRequest parcelableRemoteWorkRequest =
ParcelConverters.unmarshall(request, ParcelableRemoteWorkRequest.CREATOR);
@@ -144,8 +145,8 @@
@Override
public void interrupt(
- @NonNull final byte[] request,
- @NonNull final IWorkManagerImplCallback callback) {
+ final byte @NonNull [] request,
+ final @NonNull IWorkManagerImplCallback callback) {
try {
ParcelableInterruptRequest interruptRequest =
ParcelConverters.unmarshall(request, ParcelableInterruptRequest.CREATOR);
@@ -172,8 +173,8 @@
@Override
public void getForegroundInfoAsync(
- @NonNull final byte[] request,
- @NonNull final IWorkManagerImplCallback callback) {
+ final byte @NonNull [] request,
+ final @NonNull IWorkManagerImplCallback callback) {
try {
ParcelableRemoteWorkRequest parcelableRemoteWorkRequest =
ParcelConverters.unmarshall(request, ParcelableRemoteWorkRequest.CREATOR);
@@ -231,8 +232,7 @@
}
}
- @NonNull
- private ListenableFuture<ListenableWorker.Result> executeWorkRequest(
+ private @NonNull ListenableFuture<ListenableWorker.Result> executeWorkRequest(
@NonNull String workerClassName,
@NonNull WorkerParameters workerParameters) {
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImplClient.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImplClient.java
index 9884c9f..b75bdf5 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImplClient.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImplClient.java
@@ -25,8 +25,6 @@
import android.content.ServiceConnection;
import android.os.IBinder;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.work.Logger;
@@ -34,6 +32,9 @@
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.concurrent.Executor;
/***
@@ -66,8 +67,7 @@
* @return a {@link ListenableFuture} of {@link IListenableWorkerImpl} after a
* {@link ServiceConnection} is established.
*/
- @NonNull
- public ListenableFuture<IListenableWorkerImpl> getListenableWorkerImpl(
+ public @NonNull ListenableFuture<IListenableWorkerImpl> getListenableWorkerImpl(
@NonNull ComponentName component) {
synchronized (mLock) {
@@ -96,8 +96,7 @@
* Executes a method on an instance of {@link IListenableWorkerImpl} using the instance of
* {@link RemoteDispatcher}.
*/
- @NonNull
- public ListenableFuture<byte[]> execute(
+ public @NonNull ListenableFuture<byte[]> execute(
@NonNull ComponentName componentName,
@NonNull RemoteDispatcher<IListenableWorkerImpl> dispatcher) {
@@ -109,11 +108,10 @@
* Executes a method on an instance of {@link IListenableWorkerImpl} using the instance of
* {@link RemoteDispatcher}
*/
- @NonNull
@SuppressLint("LambdaLast")
- public ListenableFuture<byte[]> execute(
+ public @NonNull ListenableFuture<byte[]> execute(
@NonNull ListenableFuture<IListenableWorkerImpl> session,
- @NonNull final RemoteDispatcher<IListenableWorkerImpl> dispatcher) {
+ final @NonNull RemoteDispatcher<IListenableWorkerImpl> dispatcher) {
return RemoteExecuteKt.execute(mExecutor, session, dispatcher);
}
@@ -132,9 +130,8 @@
/**
* @return the {@link ServiceConnection} instance.
*/
- @Nullable
@VisibleForTesting
- public Connection getConnection() {
+ public @Nullable Connection getConnection() {
return mConnection;
}
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteDispatcher.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteDispatcher.java
index b61a1b7..6c0304e 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteDispatcher.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteDispatcher.java
@@ -18,9 +18,10 @@
import android.annotation.SuppressLint;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
/**
* @param <T> The remote interface subtype that usually implements {@link android.os.IBinder}.
*/
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteForegroundUpdater.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteForegroundUpdater.java
index bcafbb7..e901858 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteForegroundUpdater.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteForegroundUpdater.java
@@ -18,13 +18,14 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.ForegroundInfo;
import androidx.work.ForegroundUpdater;
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+
import java.util.UUID;
/**
@@ -34,9 +35,8 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class RemoteForegroundUpdater implements ForegroundUpdater {
- @NonNull
@Override
- public ListenableFuture<Void> setForegroundAsync(
+ public @NonNull ListenableFuture<Void> setForegroundAsync(
@NonNull Context context,
@NonNull UUID id,
@NonNull ForegroundInfo foregroundInfo) {
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteListenableWorker.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteListenableWorker.java
index b043313..2b6e1f7a 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteListenableWorker.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteListenableWorker.java
@@ -19,7 +19,6 @@
import android.content.ComponentName;
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.work.Data;
import androidx.work.ListenableWorker;
@@ -28,6 +27,7 @@
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
/**
* Is an implementation of a {@link ListenableWorker} that can bind to a remote process.
@@ -71,8 +71,7 @@
}
@Override
- @NonNull
- public final ListenableFuture<Result> startWork() {
+ public final @NonNull ListenableFuture<Result> startWork() {
String message = "startWork() shouldn't never be called on RemoteListenableWorker";
return getFailedFuture(message);
}
@@ -94,8 +93,7 @@
* @return A {@link ListenableFuture} with the {@code Result} of the computation. If you
* cancel this Future, WorkManager will treat this unit of work as a {@code Result#failure()}.
*/
- @NonNull
- public abstract ListenableFuture<Result> startRemoteWork();
+ public abstract @NonNull ListenableFuture<Result> startRemoteWork();
private static ListenableFuture<Result> getFailedFuture(@NonNull String message) {
return CallbackToFutureAdapter.getFuture((completer) -> {
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteProgressUpdater.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteProgressUpdater.java
index bdffda7..967a82a 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteProgressUpdater.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteProgressUpdater.java
@@ -18,13 +18,14 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.Data;
import androidx.work.ProgressUpdater;
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+
import java.util.UUID;
/**
@@ -33,9 +34,8 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class RemoteProgressUpdater implements ProgressUpdater {
- @NonNull
@Override
- public ListenableFuture<Void> updateProgress(
+ public @NonNull ListenableFuture<Void> updateProgress(
@NonNull Context context,
@NonNull UUID id,
@NonNull Data data) {
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkContinuationImpl.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkContinuationImpl.java
index 69aea74..b871768 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkContinuationImpl.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkContinuationImpl.java
@@ -18,13 +18,14 @@
import android.annotation.SuppressLint;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkContinuation;
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+
import java.util.ArrayList;
import java.util.List;
@@ -44,23 +45,20 @@
mContinuation = continuation;
}
- @NonNull
@Override
@SuppressLint("EnqueueWork")
- public RemoteWorkContinuation then(@NonNull List<OneTimeWorkRequest> work) {
+ public @NonNull RemoteWorkContinuation then(@NonNull List<OneTimeWorkRequest> work) {
return new RemoteWorkContinuationImpl(mClient, mContinuation.then(work));
}
- @NonNull
@Override
- public ListenableFuture<Void> enqueue() {
+ public @NonNull ListenableFuture<Void> enqueue() {
return mClient.enqueue(mContinuation);
}
- @NonNull
@Override
@SuppressLint("EnqueueWork")
- protected RemoteWorkContinuation combineInternal(
+ protected @NonNull RemoteWorkContinuation combineInternal(
@NonNull List<RemoteWorkContinuation> continuations) {
int size = continuations.size();
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerClient.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerClient.java
index 74fa0d7..8a397df 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerClient.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerClient.java
@@ -27,8 +27,6 @@
import android.os.IBinder;
import android.os.RemoteException;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.arch.core.util.Function;
@@ -58,6 +56,9 @@
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.Collections;
import java.util.List;
import java.util.UUID;
@@ -113,15 +114,13 @@
mRunnableScheduler = mWorkManager.getConfiguration().getRunnableScheduler();
}
- @NonNull
@Override
- public ListenableFuture<Void> enqueue(@NonNull WorkRequest request) {
+ public @NonNull ListenableFuture<Void> enqueue(@NonNull WorkRequest request) {
return enqueue(Collections.singletonList(request));
}
- @NonNull
@Override
- public ListenableFuture<Void> enqueue(@NonNull final List<WorkRequest> requests) {
+ public @NonNull ListenableFuture<Void> enqueue(final @NonNull List<WorkRequest> requests) {
ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
@Override
public void execute(
@@ -134,18 +133,16 @@
return map(result, sVoidMapper, mExecutor);
}
- @NonNull
@Override
- public ListenableFuture<Void> enqueueUniqueWork(
+ public @NonNull ListenableFuture<Void> enqueueUniqueWork(
@NonNull String uniqueWorkName,
@NonNull ExistingWorkPolicy existingWorkPolicy,
@NonNull List<OneTimeWorkRequest> work) {
return beginUniqueWork(uniqueWorkName, existingWorkPolicy, work).enqueue();
}
- @NonNull
@Override
- public ListenableFuture<Void> enqueueUniquePeriodicWork(
+ public @NonNull ListenableFuture<Void> enqueueUniquePeriodicWork(
@NonNull String uniqueWorkName,
@NonNull ExistingPeriodicWorkPolicy existingPeriodicWorkPolicy,
@NonNull PeriodicWorkRequest periodicWork) {
@@ -164,15 +161,13 @@
return enqueue(continuation);
}
- @NonNull
@Override
- public RemoteWorkContinuation beginWith(@NonNull List<OneTimeWorkRequest> work) {
+ public @NonNull RemoteWorkContinuation beginWith(@NonNull List<OneTimeWorkRequest> work) {
return new RemoteWorkContinuationImpl(this, mWorkManager.beginWith(work));
}
- @NonNull
@Override
- public RemoteWorkContinuation beginUniqueWork(
+ public @NonNull RemoteWorkContinuation beginUniqueWork(
@NonNull String uniqueWorkName,
@NonNull ExistingWorkPolicy existingWorkPolicy,
@NonNull List<OneTimeWorkRequest> work) {
@@ -180,9 +175,8 @@
mWorkManager.beginUniqueWork(uniqueWorkName, existingWorkPolicy, work));
}
- @NonNull
@Override
- public ListenableFuture<Void> enqueue(@NonNull final WorkContinuation continuation) {
+ public @NonNull ListenableFuture<Void> enqueue(final @NonNull WorkContinuation continuation) {
ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
@Override
public void execute(@NonNull IWorkManagerImpl iWorkManagerImpl,
@@ -196,9 +190,8 @@
return map(result, sVoidMapper, mExecutor);
}
- @NonNull
@Override
- public ListenableFuture<Void> cancelWorkById(@NonNull final UUID id) {
+ public @NonNull ListenableFuture<Void> cancelWorkById(final @NonNull UUID id) {
ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
@Override
public void execute(@NonNull IWorkManagerImpl iWorkManagerImpl,
@@ -209,9 +202,8 @@
return map(result, sVoidMapper, mExecutor);
}
- @NonNull
@Override
- public ListenableFuture<Void> cancelAllWorkByTag(@NonNull final String tag) {
+ public @NonNull ListenableFuture<Void> cancelAllWorkByTag(final @NonNull String tag) {
ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
@Override
public void execute(@NonNull IWorkManagerImpl iWorkManagerImpl,
@@ -222,9 +214,8 @@
return map(result, sVoidMapper, mExecutor);
}
- @NonNull
@Override
- public ListenableFuture<Void> cancelUniqueWork(@NonNull final String uniqueWorkName) {
+ public @NonNull ListenableFuture<Void> cancelUniqueWork(final @NonNull String uniqueWorkName) {
ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
@Override
public void execute(@NonNull IWorkManagerImpl iWorkManagerImpl,
@@ -235,9 +226,8 @@
return map(result, sVoidMapper, mExecutor);
}
- @NonNull
@Override
- public ListenableFuture<Void> cancelAllWork() {
+ public @NonNull ListenableFuture<Void> cancelAllWork() {
ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
@Override
public void execute(@NonNull IWorkManagerImpl iWorkManagerImpl,
@@ -248,9 +238,9 @@
return map(result, sVoidMapper, mExecutor);
}
- @NonNull
@Override
- public ListenableFuture<List<WorkInfo>> getWorkInfos(@NonNull final WorkQuery workQuery) {
+ public @NonNull ListenableFuture<List<WorkInfo>> getWorkInfos(
+ final @NonNull WorkQuery workQuery) {
ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
@Override
public void execute(
@@ -270,9 +260,9 @@
}, mExecutor);
}
- @NonNull
@Override
- public ListenableFuture<Void> setProgress(@NonNull final UUID id, @NonNull final Data data) {
+ public @NonNull ListenableFuture<Void> setProgress(final @NonNull UUID id,
+ final @NonNull Data data) {
ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
@Override
public void execute(
@@ -285,9 +275,8 @@
return map(result, sVoidMapper, mExecutor);
}
- @NonNull
@Override
- public ListenableFuture<Void> setForegroundAsync(
+ public @NonNull ListenableFuture<Void> setForegroundAsync(
@NonNull String id,
@NonNull ForegroundInfo foregroundInfo) {
ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
@@ -309,9 +298,8 @@
* @param dispatcher The {@link RemoteDispatcher} instance.
* @return The {@link ListenableFuture} instance.
*/
- @NonNull
- public ListenableFuture<byte[]> execute(
- @NonNull final RemoteDispatcher<IWorkManagerImpl> dispatcher) {
+ public @NonNull ListenableFuture<byte[]> execute(
+ final @NonNull RemoteDispatcher<IWorkManagerImpl> dispatcher) {
return execute(getSession(), dispatcher);
}
@@ -319,16 +307,14 @@
* Gets a handle to an instance of {@link IWorkManagerImpl} by binding to the
* {@link RemoteWorkManagerService} if necessary.
*/
- @NonNull
- public ListenableFuture<IWorkManagerImpl> getSession() {
+ public @NonNull ListenableFuture<IWorkManagerImpl> getSession() {
return getSession(newIntent(mContext));
}
/**
* @return The application {@link Context}.
*/
- @NonNull
- public Context getContext() {
+ public @NonNull Context getContext() {
return mContext;
}
@@ -342,32 +328,28 @@
/**
* @return The current {@link Session} in use by {@link RemoteWorkManagerClient}.
*/
- @Nullable
- public Session getCurrentSession() {
+ public @Nullable Session getCurrentSession() {
return mSession;
}
/**
* @return the {@link SessionTracker} instance.
*/
- @NonNull
- public SessionTracker getSessionTracker() {
+ public @NonNull SessionTracker getSessionTracker() {
return mSessionTracker;
}
/**
* @return The {@link Object} session lock.
*/
- @NonNull
- public Object getSessionLock() {
+ public @NonNull Object getSessionLock() {
return mLock;
}
/**
* @return The background {@link Executor} used by {@link RemoteWorkManagerClient}.
*/
- @NonNull
- public Executor getExecutor() {
+ public @NonNull Executor getExecutor() {
return mExecutor;
}
@@ -378,11 +360,10 @@
return mSessionIndex;
}
- @NonNull
@VisibleForTesting
- ListenableFuture<byte[]> execute(
- @NonNull final ListenableFuture<IWorkManagerImpl> session,
- @NonNull final RemoteDispatcher<IWorkManagerImpl> dispatcher) {
+ @NonNull ListenableFuture<byte[]> execute(
+ final @NonNull ListenableFuture<IWorkManagerImpl> session,
+ final @NonNull RemoteDispatcher<IWorkManagerImpl> dispatcher) {
session.addListener(() -> {
try {
session.get();
@@ -402,9 +383,8 @@
return future;
}
- @NonNull
@VisibleForTesting
- ListenableFuture<IWorkManagerImpl> getSession(@NonNull Intent intent) {
+ @NonNull ListenableFuture<IWorkManagerImpl> getSession(@NonNull Intent intent) {
synchronized (mLock) {
mSessionIndex += 1;
if (mSession == null) {
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerImpl.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerImpl.java
index 6ed8367..0ca6ab1 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerImpl.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerImpl.java
@@ -22,7 +22,6 @@
import android.content.Context;
import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.Operation;
import androidx.work.WorkInfo;
@@ -45,6 +44,8 @@
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executor;
@@ -69,7 +70,7 @@
@Override
@MainThread
public void enqueueWorkRequests(
- final @NonNull byte[] request,
+ final byte @NonNull [] request,
final @NonNull IWorkManagerImplCallback callback) {
try {
ParcelableWorkRequests parcelledRequests =
@@ -80,9 +81,9 @@
final ListenableCallback<Operation.State.SUCCESS> listenableCallback =
new ListenableCallback<Operation.State.SUCCESS>(executor, callback,
operation.getResult()) {
- @NonNull
@Override
- public byte[] toByteArray(@NonNull Operation.State.SUCCESS result) {
+ public byte @NonNull [] toByteArray(
+ Operation.State.@NonNull SUCCESS result) {
return sEMPTY;
}
};
@@ -94,7 +95,7 @@
@Override
public void enqueueContinuation(
- final @NonNull byte[] request,
+ final byte @NonNull [] request,
final @NonNull IWorkManagerImplCallback callback) {
try {
ParcelableWorkContinuationImpl parcelledRequest =
@@ -106,9 +107,9 @@
final ListenableCallback<Operation.State.SUCCESS> listenableCallback =
new ListenableCallback<Operation.State.SUCCESS>(executor, callback,
operation.getResult()) {
- @NonNull
@Override
- public byte[] toByteArray(@NonNull Operation.State.SUCCESS result) {
+ public byte @NonNull [] toByteArray(
+ Operation.State.@NonNull SUCCESS result) {
return sEMPTY;
}
};
@@ -126,9 +127,9 @@
final ListenableCallback<Operation.State.SUCCESS> listenableCallback =
new ListenableCallback<Operation.State.SUCCESS>(executor, callback,
operation.getResult()) {
- @NonNull
@Override
- public byte[] toByteArray(@NonNull Operation.State.SUCCESS result) {
+ public byte @NonNull [] toByteArray(
+ Operation.State.@NonNull SUCCESS result) {
return sEMPTY;
}
};
@@ -148,9 +149,9 @@
final ListenableCallback<Operation.State.SUCCESS> listenableCallback =
new ListenableCallback<Operation.State.SUCCESS>(executor, callback,
operation.getResult()) {
- @NonNull
@Override
- public byte[] toByteArray(@NonNull Operation.State.SUCCESS result) {
+ public byte @NonNull [] toByteArray(
+ Operation.State.@NonNull SUCCESS result) {
return sEMPTY;
}
};
@@ -170,9 +171,9 @@
final ListenableCallback<Operation.State.SUCCESS> listenableCallback =
new ListenableCallback<Operation.State.SUCCESS>(executor, callback,
operation.getResult()) {
- @NonNull
@Override
- public byte[] toByteArray(@NonNull Operation.State.SUCCESS result) {
+ public byte @NonNull [] toByteArray(
+ Operation.State.@NonNull SUCCESS result) {
return sEMPTY;
}
};
@@ -190,9 +191,9 @@
final ListenableCallback<Operation.State.SUCCESS> listenableCallback =
new ListenableCallback<Operation.State.SUCCESS>(executor, callback,
operation.getResult()) {
- @NonNull
@Override
- public byte[] toByteArray(@NonNull Operation.State.SUCCESS result) {
+ public byte @NonNull [] toByteArray(
+ Operation.State.@NonNull SUCCESS result) {
return sEMPTY;
}
};
@@ -203,7 +204,8 @@
}
@Override
- public void queryWorkInfo(@NonNull byte[] request, @NonNull IWorkManagerImplCallback callback) {
+ public void queryWorkInfo(byte @NonNull [] request,
+ @NonNull IWorkManagerImplCallback callback) {
try {
ParcelableWorkQuery parcelled =
ParcelConverters.unmarshall(request, ParcelableWorkQuery.CREATOR);
@@ -212,9 +214,8 @@
mWorkManager.getWorkInfos(parcelled.getWorkQuery());
final ListenableCallback<List<WorkInfo>> listenableCallback =
new ListenableCallback<List<WorkInfo>>(executor, callback, future) {
- @NonNull
@Override
- public byte[] toByteArray(@NonNull List<WorkInfo> result) {
+ public byte @NonNull [] toByteArray(@NonNull List<WorkInfo> result) {
ParcelableWorkInfos parcelables = new ParcelableWorkInfos(result);
return ParcelConverters.marshall(parcelables);
}
@@ -226,7 +227,7 @@
}
@Override
- public void setProgress(@NonNull byte[] request, @NonNull IWorkManagerImplCallback callback) {
+ public void setProgress(byte @NonNull [] request, @NonNull IWorkManagerImplCallback callback) {
try {
ParcelableUpdateRequest parcelled =
ParcelConverters.unmarshall(request, ParcelableUpdateRequest.CREATOR);
@@ -243,9 +244,8 @@
);
final ListenableCallback<Void> listenableCallback =
new ListenableCallback<Void>(executor, callback, future) {
- @NonNull
@Override
- public byte[] toByteArray(@NonNull Void result) {
+ public byte @NonNull [] toByteArray(@NonNull Void result) {
return sEMPTY;
}
};
@@ -257,7 +257,7 @@
@Override
public void setForegroundAsync(
- @NonNull byte[] request,
+ byte @NonNull [] request,
@NonNull IWorkManagerImplCallback callback) {
try {
ParcelableForegroundRequestInfo parcelled =
@@ -276,9 +276,8 @@
);
final ListenableCallback<Void> listenableCallback =
new ListenableCallback<Void>(executor, callback, future) {
- @NonNull
@Override
- public byte[] toByteArray(@NonNull Void result) {
+ public byte @NonNull [] toByteArray(@NonNull Void result) {
return sEMPTY;
}
};
@@ -289,7 +288,7 @@
}
@Override
- public void updateUniquePeriodicWorkRequest(@NonNull String name, @NonNull byte[] request,
+ public void updateUniquePeriodicWorkRequest(@NonNull String name, byte @NonNull [] request,
@NonNull IWorkManagerImplCallback callback) {
try {
ParcelableWorkRequest parcelableWorkRequest = ParcelConverters.unmarshall(request,
@@ -301,9 +300,9 @@
final ListenableCallback<Operation.State.SUCCESS> listenableCallback =
new ListenableCallback<Operation.State.SUCCESS>(executor, callback,
operation.getResult()) {
- @NonNull
@Override
- public byte[] toByteArray(@NonNull Operation.State.SUCCESS result) {
+ public byte @NonNull [] toByteArray(
+ Operation.State.@NonNull SUCCESS result) {
return sEMPTY;
}
};
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerInfo.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerInfo.java
index 2797baac..a61f399 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerInfo.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerInfo.java
@@ -18,7 +18,6 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.work.Configuration;
@@ -29,6 +28,8 @@
import androidx.work.impl.utils.taskexecutor.TaskExecutor;
import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor;
+import org.jspecify.annotations.NonNull;
+
/**
* Can keep track of WorkManager configuration and schedulers without having to fully
* initialize {@link androidx.work.WorkManager} in a remote process.
@@ -52,8 +53,7 @@
* @return an instance of {@link RemoteWorkManagerInfo} which tracks {@link WorkManager}
* configuration without having to initialize {@link WorkManager}.
*/
- @NonNull
- public static RemoteWorkManagerInfo getInstance(@NonNull Context context) {
+ public static @NonNull RemoteWorkManagerInfo getInstance(@NonNull Context context) {
if (sInstance == null) {
synchronized (sLock) {
if (sInstance == null) {
@@ -102,8 +102,7 @@
* @return The {@link Configuration} instance which can be used without having to initialize
* {@link WorkManager}.
*/
- @NonNull
- public Configuration getConfiguration() {
+ public @NonNull Configuration getConfiguration() {
return mConfiguration;
}
@@ -111,8 +110,7 @@
* @return The {@link TaskExecutor} instance that can be used without having to initialize
* {@link WorkManager}.
*/
- @NonNull
- public TaskExecutor getTaskExecutor() {
+ public @NonNull TaskExecutor getTaskExecutor() {
return mTaskExecutor;
}
@@ -120,8 +118,7 @@
* @return The {@link androidx.work.ProgressUpdater} instance that can be use without
* having to initialize {@link WorkManager}.
*/
- @NonNull
- public ProgressUpdater getProgressUpdater() {
+ public @NonNull ProgressUpdater getProgressUpdater() {
return mProgressUpdater;
}
@@ -129,8 +126,7 @@
* @return The {@link androidx.work.ForegroundUpdater} instance that can be use without
* having to initialize {@link WorkManager}.
*/
- @NonNull
- public ForegroundUpdater getForegroundUpdater() {
+ public @NonNull ForegroundUpdater getForegroundUpdater() {
return mForegroundUpdater;
}
}
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerService.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerService.java
index cd9263c..243dc5f 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerService.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerService.java
@@ -20,11 +20,12 @@
import android.content.Intent;
import android.os.IBinder;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.work.Logger;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* The {@link android.app.Service} which hosts an implementation of {@link RemoteWorkManager}.
*
@@ -40,9 +41,8 @@
mBinder = new RemoteWorkManagerImpl(this);
}
- @Nullable
@Override
- public IBinder onBind(@NonNull Intent intent) {
+ public @Nullable IBinder onBind(@NonNull Intent intent) {
Logger.get().info(TAG, "Binding to RemoteWorkManager");
return mBinder;
}
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkerService.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkerService.java
index 4fbf6a6..671c28a 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkerService.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkerService.java
@@ -20,10 +20,11 @@
import android.content.Intent;
import android.os.IBinder;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.work.Logger;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* The {@link Service} which hosts an implementation of a {@link androidx.work.ListenableWorker}.
*/
@@ -37,9 +38,8 @@
mBinder = new ListenableWorkerImpl(this);
}
- @Nullable
@Override
- public IBinder onBind(@NonNull Intent intent) {
+ public @Nullable IBinder onBind(@NonNull Intent intent) {
Logger.get().info(TAG, "Binding to RemoteWorkerService");
return mBinder;
}
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelConverters.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelConverters.java
index be233e0..fe1d1f1 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelConverters.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelConverters.java
@@ -19,9 +19,10 @@
import android.os.Parcel;
import android.os.Parcelable;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
/**
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -33,8 +34,7 @@
/**
* Marshalls a {@link Parcelable}.
*/
- @NonNull
- public static byte[] marshall(@NonNull Parcelable parcelable) {
+ public static byte @NonNull [] marshall(@NonNull Parcelable parcelable) {
Parcel parcel = Parcel.obtain();
try {
parcelable.writeToParcel(parcel, 0 /* flags */);
@@ -47,10 +47,9 @@
/**
* Unmarshalls a {@code byte[]} to the {@link T} given a {@link android.os.Parcelable.Creator}.
*/
- @NonNull
- public static <T> T unmarshall(
- @NonNull byte[] array,
- @NonNull Parcelable.Creator<T> creator) {
+ public static <T> @NonNull T unmarshall(
+ byte @NonNull [] array,
+ Parcelable.@NonNull Creator<T> creator) {
Parcel parcel = Parcel.obtain();
try {
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelUtils.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelUtils.java
index 949bb5ef..5d1c748 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelUtils.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelUtils.java
@@ -20,9 +20,10 @@
import android.os.Parcel;
import android.os.Parcelable;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
/**
*
*/
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableConstraints.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableConstraints.java
index 37eb9b1..fe359e8 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableConstraints.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableConstraints.java
@@ -31,13 +31,14 @@
import android.os.Parcel;
import android.os.Parcelable;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.Constraints;
import androidx.work.Constraints.ContentUriTrigger;
import androidx.work.NetworkType;
import androidx.work.impl.utils.NetworkRequest28;
+import org.jspecify.annotations.NonNull;
+
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -162,8 +163,7 @@
}
}
- @NonNull
- public Constraints getConstraints() {
+ public @NonNull Constraints getConstraints() {
return mConstraints;
}
}
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableForegroundRequestInfo.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableForegroundRequestInfo.java
index 5915a5b..9eea428 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableForegroundRequestInfo.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableForegroundRequestInfo.java
@@ -21,10 +21,11 @@
import android.os.Parcel;
import android.os.Parcelable;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.ForegroundInfo;
+import org.jspecify.annotations.NonNull;
+
/**
* ForegroundInfo but parcelable.
*
@@ -69,13 +70,11 @@
};
- @NonNull
- public ForegroundInfo getForegroundInfo() {
+ public @NonNull ForegroundInfo getForegroundInfo() {
return mForegroundInfo;
}
- @NonNull
- public String getId() {
+ public @NonNull String getId() {
return mId;
}
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRemoteWorkRequest.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRemoteWorkRequest.java
index 9609b54..4a54688 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRemoteWorkRequest.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRemoteWorkRequest.java
@@ -20,10 +20,11 @@
import android.os.Parcel;
import android.os.Parcelable;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
/**
* Everything you need to run a {@link androidx.work.multiprocess.RemoteListenableWorker}.
*
@@ -79,13 +80,11 @@
mParcelableWorkerParameters.writeToParcel(parcel, flags);
}
- @NonNull
- public String getWorkerClassName() {
+ public @NonNull String getWorkerClassName() {
return mWorkerClassName;
}
- @NonNull
- public ParcelableWorkerParameters getParcelableWorkerParameters() {
+ public @NonNull ParcelableWorkerParameters getParcelableWorkerParameters() {
return mParcelableWorkerParameters;
}
}
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableResult.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableResult.java
index c8340d5..a3bd73c 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableResult.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableResult.java
@@ -20,11 +20,12 @@
import android.os.Parcel;
import android.os.Parcelable;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.Data;
import androidx.work.ListenableWorker;
+import org.jspecify.annotations.NonNull;
+
/**
* {@link androidx.work.ListenableWorker.Result}, but parcelable.
*
@@ -34,7 +35,7 @@
public class ParcelableResult implements Parcelable {
private final ListenableWorker.Result mResult;
- public ParcelableResult(@NonNull ListenableWorker.Result result) {
+ public ParcelableResult(ListenableWorker.@NonNull Result result) {
mResult = result;
}
@@ -49,8 +50,7 @@
public static final Creator<ParcelableResult> CREATOR =
new Creator<ParcelableResult>() {
@Override
- @NonNull
- public ParcelableResult createFromParcel(Parcel in) {
+ public @NonNull ParcelableResult createFromParcel(Parcel in) {
return new ParcelableResult(in);
}
@@ -76,8 +76,7 @@
parcelableOutputData.writeToParcel(parcel, flags);
}
- @NonNull
- public ListenableWorker.Result getResult() {
+ public ListenableWorker.@NonNull Result getResult() {
return mResult;
}
@@ -94,8 +93,8 @@
}
}
- @NonNull
- private static ListenableWorker.Result intToResultType(int resultType, @NonNull Data data) {
+ private static ListenableWorker.@NonNull Result intToResultType(int resultType,
+ @NonNull Data data) {
ListenableWorker.Result result = null;
if (resultType == 1) {
result = ListenableWorker.Result.retry();
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRuntimeExtras.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRuntimeExtras.java
index 4180a34..3390d7f 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRuntimeExtras.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRuntimeExtras.java
@@ -26,11 +26,12 @@
import android.os.Parcel;
import android.os.Parcelable;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
import java.util.ArrayList;
import java.util.List;
@@ -43,7 +44,7 @@
public class ParcelableRuntimeExtras implements Parcelable {
private WorkerParameters.RuntimeExtras mRuntimeExtras;
- public ParcelableRuntimeExtras(@NonNull WorkerParameters.RuntimeExtras runtimeExtras) {
+ public ParcelableRuntimeExtras(WorkerParameters.@NonNull RuntimeExtras runtimeExtras) {
mRuntimeExtras = runtimeExtras;
}
@@ -89,8 +90,7 @@
public static final Creator<ParcelableRuntimeExtras> CREATOR =
new Creator<ParcelableRuntimeExtras>() {
@Override
- @NonNull
- public ParcelableRuntimeExtras createFromParcel(Parcel in) {
+ public @NonNull ParcelableRuntimeExtras createFromParcel(Parcel in) {
return new ParcelableRuntimeExtras(in);
}
@@ -144,8 +144,7 @@
}
}
- @NonNull
- public WorkerParameters.RuntimeExtras getRuntimeExtras() {
+ public WorkerParameters.@NonNull RuntimeExtras getRuntimeExtras() {
return mRuntimeExtras;
}
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableUpdateRequest.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableUpdateRequest.java
index f484a55..2f9fac7 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableUpdateRequest.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableUpdateRequest.java
@@ -20,10 +20,11 @@
import android.os.Parcel;
import android.os.Parcelable;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.Data;
+import org.jspecify.annotations.NonNull;
+
import java.util.UUID;
/**
@@ -63,13 +64,11 @@
}
};
- @NonNull
- public String getId() {
+ public @NonNull String getId() {
return mId;
}
- @NonNull
- public Data getData() {
+ public @NonNull Data getData() {
return mParcelableData.getData();
}
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkContinuationImpl.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkContinuationImpl.java
index 4881dd1..ce58af8 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkContinuationImpl.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkContinuationImpl.java
@@ -24,8 +24,6 @@
import android.os.Parcelable;
import android.text.TextUtils;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.work.ExistingWorkPolicy;
import androidx.work.WorkRequest;
@@ -33,6 +31,9 @@
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.WorkRequestHolder;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.List;
@@ -56,8 +57,7 @@
mInfo = info;
}
- @NonNull
- public WorkContinuationImplInfo getInfo() {
+ public @NonNull WorkContinuationImplInfo getInfo() {
return mInfo;
}
@@ -159,8 +159,8 @@
* @param workManager The {@link WorkManagerImpl} instance.
* @return The {@link WorkContinuationImpl} instance
*/
- @NonNull
- public WorkContinuationImpl toWorkContinuationImpl(@NonNull WorkManagerImpl workManager) {
+ public @NonNull WorkContinuationImpl toWorkContinuationImpl(
+ @NonNull WorkManagerImpl workManager) {
return mInfo.toWorkContinuationImpl(workManager);
}
@@ -199,23 +199,19 @@
mParents = parents;
}
- @Nullable
- public String getName() {
+ public @Nullable String getName() {
return mName;
}
- @NonNull
- public ExistingWorkPolicy getExistingWorkPolicy() {
+ public @NonNull ExistingWorkPolicy getExistingWorkPolicy() {
return mWorkPolicy;
}
- @NonNull
- public List<? extends WorkRequest> getWork() {
+ public @NonNull List<? extends WorkRequest> getWork() {
return mRequests;
}
- @Nullable
- public List<WorkContinuationImplInfo> getParentInfos() {
+ public @Nullable List<WorkContinuationImplInfo> getParentInfos() {
return mParents;
}
@@ -226,8 +222,8 @@
* @param workManager The {@link WorkManagerImpl} instance.
* @return The {@link WorkContinuationImpl} instance
*/
- @NonNull
- public WorkContinuationImpl toWorkContinuationImpl(@NonNull WorkManagerImpl workManager) {
+ public @NonNull WorkContinuationImpl toWorkContinuationImpl(
+ @NonNull WorkManagerImpl workManager) {
return new WorkContinuationImpl(
workManager,
getName(),
@@ -237,8 +233,7 @@
);
}
- @Nullable
- private static List<WorkContinuationImpl> parents(
+ private static @Nullable List<WorkContinuationImpl> parents(
@NonNull WorkManagerImpl workManager,
@Nullable List<WorkContinuationImplInfo> parentInfos) {
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkInfo.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkInfo.java
index 187ca90..5afadaf 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkInfo.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkInfo.java
@@ -23,11 +23,12 @@
import android.os.Parcel;
import android.os.Parcelable;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.Data;
import androidx.work.WorkInfo;
+import org.jspecify.annotations.NonNull;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
@@ -71,8 +72,7 @@
mWorkInfo = new WorkInfo(id, state, tags, output, progress, runAttemptCount, generation);
}
- @NonNull
- public WorkInfo getWorkInfo() {
+ public @NonNull WorkInfo getWorkInfo() {
return mWorkInfo;
}
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkInfos.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkInfos.java
index 16e4e6b..d76055c 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkInfos.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkInfos.java
@@ -20,10 +20,11 @@
import android.os.Parcel;
import android.os.Parcelable;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.WorkInfo;
+import org.jspecify.annotations.NonNull;
+
import java.util.ArrayList;
import java.util.List;
@@ -41,8 +42,7 @@
mInfos = infos;
}
- @NonNull
- public List<WorkInfo> getWorkInfos() {
+ public @NonNull List<WorkInfo> getWorkInfos() {
return mInfos;
}
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkQuery.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkQuery.java
index 42d1472..1b23ab1 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkQuery.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkQuery.java
@@ -23,11 +23,12 @@
import android.os.Parcel;
import android.os.Parcelable;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.WorkInfo;
import androidx.work.WorkQuery;
+import org.jspecify.annotations.NonNull;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -78,8 +79,7 @@
.build();
}
- @NonNull
- public WorkQuery getWorkQuery() {
+ public @NonNull WorkQuery getWorkQuery() {
return mWorkQuery;
}
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java
index 85c9a0c..b4d2128 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java
@@ -29,12 +29,13 @@
import android.os.Parcel;
import android.os.Parcelable;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.WorkRequest;
import androidx.work.impl.WorkRequestHolder;
import androidx.work.impl.model.WorkSpec;
+import org.jspecify.annotations.NonNull;
+
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -167,8 +168,7 @@
parcel.writeString(workSpec.getTraceTag());
}
- @NonNull
- public WorkRequest getWorkRequest() {
+ public @NonNull WorkRequest getWorkRequest() {
return mWorkRequest;
}
}
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequests.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequests.java
index 639fb3f4..8239cb4 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequests.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequests.java
@@ -20,10 +20,11 @@
import android.os.Parcel;
import android.os.Parcelable;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.WorkRequest;
+import org.jspecify.annotations.NonNull;
+
import java.util.ArrayList;
import java.util.List;
@@ -77,8 +78,7 @@
parcel.writeParcelableArray(parcelledArray, flags);
}
- @NonNull
- public List<WorkRequest> getRequests() {
+ public @NonNull List<WorkRequest> getRequests() {
return mRequests;
}
}
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkerParameters.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkerParameters.java
index 5aa72cc..7b2f14e 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkerParameters.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkerParameters.java
@@ -20,7 +20,6 @@
import android.os.Parcel;
import android.os.Parcelable;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.Configuration;
import androidx.work.Data;
@@ -34,6 +33,8 @@
import androidx.work.impl.utils.WorkProgressUpdater;
import androidx.work.impl.utils.taskexecutor.TaskExecutor;
+import org.jspecify.annotations.NonNull;
+
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -47,14 +48,10 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@SuppressLint("BanParcelableUsage")
public class ParcelableWorkerParameters implements Parcelable {
- @NonNull
- private final UUID mId;
- @NonNull
- private final Data mData;
- @NonNull
- private final Set<String> mTags;
- @NonNull
- private final WorkerParameters.RuntimeExtras mRuntimeExtras;
+ private final @NonNull UUID mId;
+ private final @NonNull Data mData;
+ private final @NonNull Set<String> mTags;
+ private final WorkerParameters.@NonNull RuntimeExtras mRuntimeExtras;
private final int mRunAttemptCount;
private final int mGeneration;
@@ -70,8 +67,7 @@
public static final Creator<ParcelableWorkerParameters> CREATOR =
new Creator<ParcelableWorkerParameters>() {
@Override
- @NonNull
- public ParcelableWorkerParameters createFromParcel(Parcel in) {
+ public @NonNull ParcelableWorkerParameters createFromParcel(Parcel in) {
return new ParcelableWorkerParameters(in);
}
@@ -124,13 +120,11 @@
parcel.writeInt(mGeneration);
}
- @NonNull
- public UUID getId() {
+ public @NonNull UUID getId() {
return mId;
}
- @NonNull
- public Data getData() {
+ public @NonNull Data getData() {
return mData;
}
@@ -138,8 +132,7 @@
return mRunAttemptCount;
}
- @NonNull
- public Set<String> getTags() {
+ public @NonNull Set<String> getTags() {
return mTags;
}
@@ -147,8 +140,7 @@
* Converts {@link ParcelableWorkerParameters} to an instance of {@link WorkerParameters}
* lazily.
*/
- @NonNull
- public WorkerParameters toWorkerParameters(@NonNull WorkManagerImpl workManager) {
+ public @NonNull WorkerParameters toWorkerParameters(@NonNull WorkManagerImpl workManager) {
Configuration configuration = workManager.getConfiguration();
WorkDatabase workDatabase = workManager.getWorkDatabase();
TaskExecutor taskExecutor = workManager.getWorkTaskExecutor();
@@ -171,8 +163,7 @@
* Converts {@link ParcelableWorkerParameters} to an instance of {@link WorkerParameters}
* lazily.
*/
- @NonNull
- public WorkerParameters toWorkerParameters(
+ public @NonNull WorkerParameters toWorkerParameters(
@NonNull Configuration configuration,
@NonNull TaskExecutor taskExecutor,
@NonNull ProgressUpdater progressUpdater,
diff --git a/work/work-runtime-ktx/build.gradle b/work/work-runtime-ktx/build.gradle
index 29a313f..25699af 100644
--- a/work/work-runtime-ktx/build.gradle
+++ b/work/work-runtime-ktx/build.gradle
@@ -30,7 +30,7 @@
}
dependencies {
- api project(":work:work-runtime")
+ api(project(":work:work-runtime"))
}
androidx {
diff --git a/work/work-runtime/build.gradle b/work/work-runtime/build.gradle
index 24bfba4..e27dbfc 100644
--- a/work/work-runtime/build.gradle
+++ b/work/work-runtime/build.gradle
@@ -63,6 +63,7 @@
}
dependencies {
+ api(libs.jspecify)
ksp("androidx.room:room-compiler:2.6.1")
api("androidx.core:core:1.12.0")
implementation("androidx.room:room-ktx:2.6.1")
@@ -108,6 +109,4 @@
description = "Android WorkManager runtime library"
failOnDeprecationWarnings = false
legacyDisableKotlinStrictApiMode = true
- // TODO: b/326456246
- optOutJSpecify = true
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/DefaultWorkerFactoryTest.java b/work/work-runtime/src/androidTest/java/androidx/work/DefaultWorkerFactoryTest.java
index cd18321..60c4d44 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/DefaultWorkerFactoryTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/DefaultWorkerFactoryTest.java
@@ -32,6 +32,8 @@
import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor;
import androidx.work.worker.TestWorker;
+import kotlinx.coroutines.Dispatchers;
+
import org.hamcrest.CoreMatchers;
import org.junit.Before;
import org.junit.Test;
@@ -39,8 +41,6 @@
import java.util.concurrent.Executor;
-import kotlinx.coroutines.Dispatchers;
-
@RunWith(AndroidJUnit4.class)
public class DefaultWorkerFactoryTest extends DatabaseTest {
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java b/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
index a836a59..ae99368 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
@@ -56,7 +56,6 @@
import android.net.Uri;
import android.os.Build;
-import androidx.annotation.NonNull;
import androidx.room.testing.MigrationTestHelper;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.test.core.app.ApplicationProvider;
@@ -83,6 +82,7 @@
import androidx.work.worker.TestWorker;
import org.hamcrest.Matchers;
+import org.jspecify.annotations.NonNull;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -682,8 +682,7 @@
}
// doesn't have COLUMN_RUN_IN_FOREGROUND
- @NonNull
- private ContentValues contentValuesPre8(String workSpecId) {
+ private @NonNull ContentValues contentValuesPre8(String workSpecId) {
ContentValues contentValues = new ContentValues();
contentValues.put("id", workSpecId);
contentValues.put("state", WorkTypeConverters.StateIds.ENQUEUED);
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkContinuationImplTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkContinuationImplTest.java
index d3e769ed..11fc6b5b 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkContinuationImplTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkContinuationImplTest.java
@@ -32,7 +32,6 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.arch.core.executor.ArchTaskExecutor;
import androidx.arch.core.executor.TaskExecutor;
import androidx.test.core.app.ApplicationProvider;
@@ -49,6 +48,7 @@
import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor;
import androidx.work.worker.TestWorker;
+import org.jspecify.annotations.NonNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -64,7 +64,6 @@
import java.util.UUID;
import java.util.concurrent.ExecutionException;
-
@RunWith(AndroidJUnit4.class)
@MediumTest
public class WorkContinuationImplTest extends WorkManagerTest {
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java
index 345eba8..e84386d 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java
@@ -28,8 +28,6 @@
import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.arch.core.executor.ArchTaskExecutor;
import androidx.lifecycle.Observer;
import androidx.lifecycle.testing.TestLifecycleOwner;
@@ -48,6 +46,8 @@
import androidx.work.impl.utils.taskexecutor.TaskExecutor;
import androidx.work.worker.RandomSleepTestWorker;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -199,7 +199,7 @@
}
@Override
- public void schedule(@NonNull WorkSpec... workSpecs) {
+ public void schedule(WorkSpec @NonNull ... workSpecs) {
synchronized (sLock) {
for (WorkSpec workSpec : workSpecs) {
assertThat(mScheduledWorkSpecIds.contains(workSpec.id), is(false));
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
index ad31a45..0326a9de 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
@@ -73,7 +73,6 @@
import android.provider.MediaStore;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.arch.core.executor.ArchTaskExecutor;
import androidx.arch.core.executor.TaskExecutor;
import androidx.lifecycle.LiveData;
@@ -126,6 +125,7 @@
import com.google.common.util.concurrent.Futures;
+import org.jspecify.annotations.NonNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -2297,8 +2297,8 @@
new Dependency(work.getStringId(), prerequisiteWork.getStringId()));
}
- @NonNull
- private static WorkInfo createWorkInfo(UUID id, WorkInfo.State state, List<String> tags) {
+ private static @NonNull WorkInfo createWorkInfo(UUID id, WorkInfo.State state,
+ List<String> tags) {
return new WorkInfo(
id, state, new HashSet<>(tags), Data.EMPTY, Data.EMPTY, 0, 0,
Constraints.NONE, 0, null,
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
index 10e4788..286067b 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
@@ -49,8 +49,6 @@
import android.net.Uri;
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -103,6 +101,8 @@
import kotlinx.coroutines.Dispatchers;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -121,7 +121,6 @@
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
-
@RunWith(AndroidJUnit4.class)
public class WorkerWrapperTest extends DatabaseTest {
@@ -1004,8 +1003,7 @@
assertBeginEndTraceSpans(periodicWork.getWorkSpec());
}
- @NonNull
- private FutureListener runWorker(PeriodicWorkRequest periodicWork, Worker worker) {
+ private @NonNull FutureListener runWorker(PeriodicWorkRequest periodicWork, Worker worker) {
WorkerWrapper workerWrapper =
createBuilder(periodicWork.getStringId()).withWorker(worker).build();
FutureListener listener = createAndAddFutureListener(workerWrapper);
@@ -1282,9 +1280,8 @@
.setInputData(new Data.Builder().putString("foo", "bar").build()).build();
insertWork(work);
WorkerFactory factory = new WorkerFactory() {
- @Nullable
@Override
- public ListenableWorker createWorker(@NonNull Context appContext,
+ public @Nullable ListenableWorker createWorker(@NonNull Context appContext,
@NonNull String workerClassName, @NonNull WorkerParameters workerParameters) {
throw new IllegalStateException("Thrown in WorkerFactory Exception");
}
@@ -1400,8 +1397,8 @@
);
}
- @Nullable
- private LatchWorker getLatchWorker(WorkRequest work, ExecutorService executorService) {
+ private @Nullable LatchWorker getLatchWorker(WorkRequest work,
+ ExecutorService executorService) {
return (LatchWorker) mConfiguration.getWorkerFactory().createWorkerWithDefaultFallback(
mContext.getApplicationContext(),
LatchWorker.class.getName(),
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/GreedySchedulerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/GreedySchedulerTest.java
index f695ffa..8fa4e48 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/GreedySchedulerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/GreedySchedulerTest.java
@@ -57,7 +57,6 @@
import java.util.concurrent.TimeUnit;
-
@RunWith(AndroidJUnit4.class)
public class GreedySchedulerTest extends WorkManagerTest {
private final Context mContext = ApplicationProvider.getApplicationContext();
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
index 11dbfcd..f5d0472 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
@@ -42,8 +42,6 @@
import android.os.Looper;
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
@@ -78,6 +76,8 @@
import androidx.work.worker.TestWorker;
import org.hamcrest.collection.IsIterableContainingInOrder;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -146,14 +146,12 @@
TaskExecutor instantTaskExecutor = new TaskExecutor() {
@Override
- @NonNull
- public Executor getMainThreadExecutor() {
+ public @NonNull Executor getMainThreadExecutor() {
return mMainThreadExecutor;
}
- @NonNull
@Override
- public SerialExecutor getSerialTaskExecutor() {
+ public @NonNull SerialExecutor getSerialTaskExecutor() {
return new SerialExecutorImpl(new SynchronousExecutor());
}
};
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/WorkTimerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/WorkTimerTest.java
index 040b3aa..50aef15 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/WorkTimerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/WorkTimerTest.java
@@ -22,13 +22,13 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.work.impl.DefaultRunnableScheduler;
import androidx.work.impl.model.WorkGenerationalId;
import androidx.work.impl.utils.WorkTimer;
+import org.jspecify.annotations.NonNull;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java
index e1dbc7b..d671339 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java
@@ -40,7 +40,6 @@
import android.os.Build;
import android.os.PersistableBundle;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.arch.core.executor.ArchTaskExecutor;
import androidx.arch.core.executor.TaskExecutor;
@@ -66,6 +65,7 @@
import androidx.work.worker.InfiniteTestWorker;
import androidx.work.worker.NeverResolvedWorker;
+import org.jspecify.annotations.NonNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/LiveDataUtilsTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/LiveDataUtilsTest.java
index c186329..a8971d0 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/LiveDataUtilsTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/LiveDataUtilsTest.java
@@ -20,7 +20,6 @@
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
-import androidx.annotation.Nullable;
import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
import androidx.arch.core.util.Function;
import androidx.lifecycle.LiveData;
@@ -32,6 +31,7 @@
import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor;
import androidx.work.impl.utils.taskexecutor.TaskExecutor;
+import org.jspecify.annotations.Nullable;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/taskexecutor/InstantWorkTaskExecutor.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/taskexecutor/InstantWorkTaskExecutor.java
index 0e11978..d577b5a 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/taskexecutor/InstantWorkTaskExecutor.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/taskexecutor/InstantWorkTaskExecutor.java
@@ -16,10 +16,11 @@
package androidx.work.impl.utils.taskexecutor;
-import androidx.annotation.NonNull;
import androidx.work.impl.utils.SerialExecutorImpl;
import androidx.work.impl.utils.SynchronousExecutor;
+import org.jspecify.annotations.NonNull;
+
import java.util.concurrent.Executor;
/**
@@ -30,9 +31,8 @@
private Executor mSynchronousExecutor = new SynchronousExecutor();
private SerialExecutorImpl mBackgroundExecutor = new SerialExecutorImpl(mSynchronousExecutor);
- @NonNull
@Override
- public Executor getMainThreadExecutor() {
+ public @NonNull Executor getMainThreadExecutor() {
return mSynchronousExecutor;
}
@@ -41,9 +41,8 @@
runnable.run();
}
- @NonNull
@Override
- public SerialExecutor getSerialTaskExecutor() {
+ public @NonNull SerialExecutor getSerialTaskExecutor() {
return mBackgroundExecutor;
}
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/worker/ChainedArgumentWorker.java b/work/work-runtime/src/androidTest/java/androidx/work/worker/ChainedArgumentWorker.java
index 3d3d84b..ea3964b 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/worker/ChainedArgumentWorker.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/worker/ChainedArgumentWorker.java
@@ -18,11 +18,12 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
/**
* A worker that passes its inputs as outputs.
*/
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/worker/EchoingWorker.java b/work/work-runtime/src/androidTest/java/androidx/work/worker/EchoingWorker.java
index b175651..c1d9d81 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/worker/EchoingWorker.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/worker/EchoingWorker.java
@@ -18,10 +18,11 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
public class EchoingWorker extends Worker {
public EchoingWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/worker/ExceptionWorker.java b/work/work-runtime/src/androidTest/java/androidx/work/worker/ExceptionWorker.java
index 34271b0..5f289f5 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/worker/ExceptionWorker.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/worker/ExceptionWorker.java
@@ -18,10 +18,11 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
public class ExceptionWorker extends Worker {
public ExceptionWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/worker/FailureWorker.java b/work/work-runtime/src/androidTest/java/androidx/work/worker/FailureWorker.java
index 393537b..0708733 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/worker/FailureWorker.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/worker/FailureWorker.java
@@ -19,10 +19,11 @@
import android.content.Context;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
/**
* Worker that fails.
*/
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/worker/InfiniteTestWorker.java b/work/work-runtime/src/androidTest/java/androidx/work/worker/InfiniteTestWorker.java
index d03b900..984a499 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/worker/InfiniteTestWorker.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/worker/InfiniteTestWorker.java
@@ -18,10 +18,11 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
/**
* Test Worker that loops until Thread is interrupted.
*/
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/worker/InterruptionAwareWorker.java b/work/work-runtime/src/androidTest/java/androidx/work/worker/InterruptionAwareWorker.java
index b1a16c6..ff30160 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/worker/InterruptionAwareWorker.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/worker/InterruptionAwareWorker.java
@@ -18,10 +18,11 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
import java.util.concurrent.CountDownLatch;
public class InterruptionAwareWorker extends Worker {
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/worker/LatchWorker.java b/work/work-runtime/src/androidTest/java/androidx/work/worker/LatchWorker.java
index 547a7f8..378e4b3 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/worker/LatchWorker.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/worker/LatchWorker.java
@@ -18,10 +18,11 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
import java.util.concurrent.CountDownLatch;
public class LatchWorker extends Worker {
@@ -34,9 +35,8 @@
super(context, workerParams);
}
- @NonNull
@Override
- public Result doWork() {
+ public @NonNull Result doWork() {
try {
mEntrySignal.countDown();
mLatch.await();
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/worker/RandomSleepTestWorker.java b/work/work-runtime/src/androidTest/java/androidx/work/worker/RandomSleepTestWorker.java
index fee507f..9a7ff9d 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/worker/RandomSleepTestWorker.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/worker/RandomSleepTestWorker.java
@@ -19,10 +19,11 @@
import android.content.Context;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
import java.util.Random;
/**
@@ -35,9 +36,8 @@
super(context, workerParams);
}
- @NonNull
@Override
- public Result doWork() {
+ public @NonNull Result doWork() {
int sleepDuration = new Random().nextInt(MAX_SLEEP_DURATION_MS);
try {
Log.d("RandomSleepTestWorker", "Sleeping : " + sleepDuration);
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/worker/RetryWorker.java b/work/work-runtime/src/androidTest/java/androidx/work/worker/RetryWorker.java
index e2c95e2..4693f59 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/worker/RetryWorker.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/worker/RetryWorker.java
@@ -19,10 +19,11 @@
import android.content.Context;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
/**
* Worker that requests retry continuously.
*/
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/worker/ReturnNullResultWorker.java b/work/work-runtime/src/androidTest/java/androidx/work/worker/ReturnNullResultWorker.java
index 809bebf..23ebc5c 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/worker/ReturnNullResultWorker.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/worker/ReturnNullResultWorker.java
@@ -19,10 +19,11 @@
import android.content.Context;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
/**
* A Worker that returns {@code null} for a {@link androidx.work.ListenableWorker.Result}.
*/
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/worker/SleepTestWorker.java b/work/work-runtime/src/androidTest/java/androidx/work/worker/SleepTestWorker.java
index 3522e88..cae387c 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/worker/SleepTestWorker.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/worker/SleepTestWorker.java
@@ -19,10 +19,11 @@
import android.content.Context;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
/**
* Worker that sleeps for 5 seconds before returning.
*/
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/worker/StopAwareWorker.java b/work/work-runtime/src/androidTest/java/androidx/work/worker/StopAwareWorker.java
index 941fb0d..9fb4e62 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/worker/StopAwareWorker.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/worker/StopAwareWorker.java
@@ -18,10 +18,11 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
/**
* A Worker that loops until it has been stopped.
*/
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/worker/TestWorker.java b/work/work-runtime/src/androidTest/java/androidx/work/worker/TestWorker.java
index 084fa74..a3358dd 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/worker/TestWorker.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/worker/TestWorker.java
@@ -19,10 +19,11 @@
import android.content.Context;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
/**
* Simple Test Worker
*/
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/worker/UsedWorker.java b/work/work-runtime/src/androidTest/java/androidx/work/worker/UsedWorker.java
index 9f2bd0f..cad90d7 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/worker/UsedWorker.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/worker/UsedWorker.java
@@ -18,10 +18,11 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
/**
* A {@link Worker} that starts off in the {@code used} state.
*/
@@ -32,9 +33,8 @@
setUsed();
}
- @NonNull
@Override
- public Result doWork() {
+ public @NonNull Result doWork() {
return Result.success();
}
}
diff --git a/work/work-runtime/src/main/java/androidx/work/ForegroundInfo.java b/work/work-runtime/src/main/java/androidx/work/ForegroundInfo.java
index b77c79e..76d95eb 100644
--- a/work/work-runtime/src/main/java/androidx/work/ForegroundInfo.java
+++ b/work/work-runtime/src/main/java/androidx/work/ForegroundInfo.java
@@ -18,7 +18,7 @@
import android.app.Notification;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
/**
* The information required when a {@link ListenableWorker} runs in the context of a foreground
@@ -84,8 +84,7 @@
/**
* @return The user visible {@link Notification}
*/
- @NonNull
- public Notification getNotification() {
+ public @NonNull Notification getNotification() {
return mNotification;
}
diff --git a/work/work-runtime/src/main/java/androidx/work/ForegroundUpdater.java b/work/work-runtime/src/main/java/androidx/work/ForegroundUpdater.java
index b721466..4bb2afd 100644
--- a/work/work-runtime/src/main/java/androidx/work/ForegroundUpdater.java
+++ b/work/work-runtime/src/main/java/androidx/work/ForegroundUpdater.java
@@ -18,10 +18,10 @@
import android.content.Context;
-import androidx.annotation.NonNull;
-
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+
import java.util.UUID;
/**
@@ -38,8 +38,7 @@
* {@link ListenableWorker} transitions to running in the context of a foreground
* {@link android.app.Service}.
*/
- @NonNull
- ListenableFuture<Void> setForegroundAsync(
+ @NonNull ListenableFuture<Void> setForegroundAsync(
@NonNull Context context,
@NonNull UUID id,
@NonNull ForegroundInfo foregroundInfo);
diff --git a/work/work-runtime/src/main/java/androidx/work/ListenableWorker.java b/work/work-runtime/src/main/java/androidx/work/ListenableWorker.java
index 4264b43..3d5d37b 100644
--- a/work/work-runtime/src/main/java/androidx/work/ListenableWorker.java
+++ b/work/work-runtime/src/main/java/androidx/work/ListenableWorker.java
@@ -24,8 +24,6 @@
import androidx.annotation.IntRange;
import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.concurrent.futures.CallbackToFutureAdapter;
@@ -33,6 +31,9 @@
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.List;
import java.util.Set;
import java.util.UUID;
@@ -200,8 +201,7 @@
* @return A {@code com.google.common.util.concurrent.ListenableFuture} which resolves
* after progress is persisted. Cancelling this future is a no-op.
*/
- @NonNull
- public ListenableFuture<Void> setProgressAsync(@NonNull Data data) {
+ public @NonNull ListenableFuture<Void> setProgressAsync(@NonNull Data data) {
return mWorkerParams.getProgressUpdater()
.updateProgress(getApplicationContext(), getId(), data);
}
@@ -229,8 +229,8 @@
* the {@link ListenableWorker} transitions to running in the context of a foreground
* {@link android.app.Service}.
*/
- @NonNull
- public final ListenableFuture<Void> setForegroundAsync(@NonNull ForegroundInfo foregroundInfo) {
+ public final @NonNull ListenableFuture<Void> setForegroundAsync(
+ @NonNull ForegroundInfo foregroundInfo) {
return mWorkerParams.getForegroundUpdater()
.setForegroundAsync(getApplicationContext(), getId(), foregroundInfo);
}
@@ -251,8 +251,7 @@
* {@link ForegroundInfo} instance if the WorkRequest is marked immediate. For more
* information look at {@link WorkRequest.Builder#setExpedited(OutOfQuotaPolicy)}.
*/
- @NonNull
- public ListenableFuture<ForegroundInfo> getForegroundInfoAsync() {
+ public @NonNull ListenableFuture<ForegroundInfo> getForegroundInfoAsync() {
return CallbackToFutureAdapter.getFuture((completer) -> {
String message =
"Expedited WorkRequests require a ListenableWorker to provide an implementation"
@@ -363,8 +362,7 @@
*
* @return An instance of {@link Result} indicating successful execution of work
*/
- @NonNull
- public static Result success() {
+ public static @NonNull Result success() {
return new Success();
}
@@ -377,8 +375,7 @@
* OneTimeWorkRequest that is dependent on this work
* @return An instance of {@link Result} indicating successful execution of work
*/
- @NonNull
- public static Result success(@NonNull Data outputData) {
+ public static @NonNull Result success(@NonNull Data outputData) {
return new Success(outputData);
}
@@ -389,8 +386,7 @@
*
* @return An instance of {@link Result} indicating that the work needs to be retried
*/
- @NonNull
- public static Result retry() {
+ public static @NonNull Result retry() {
return new Retry();
}
@@ -403,8 +399,7 @@
*
* @return An instance of {@link Result} indicating failure when executing work
*/
- @NonNull
- public static Result failure() {
+ public static @NonNull Result failure() {
return new Failure();
}
@@ -419,8 +414,7 @@
* failed
* @return An instance of {@link Result} indicating failure when executing work
*/
- @NonNull
- public static Result failure(@NonNull Data outputData) {
+ public static @NonNull Result failure(@NonNull Data outputData) {
return new Failure(outputData);
}
@@ -428,8 +422,7 @@
* @return The output {@link Data} which will be merged into the input {@link Data} of
* any {@link OneTimeWorkRequest} that is dependent on this work request.
*/
- @NonNull
- public abstract Data getOutputData();
+ public abstract @NonNull Data getOutputData();
/**
*/
@@ -482,9 +475,8 @@
return 31 * name.hashCode() + mOutputData.hashCode();
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
return "Success {" + "mOutputData=" + mOutputData + '}';
}
}
@@ -534,9 +526,8 @@
return 31 * name.hashCode() + mOutputData.hashCode();
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
return "Failure {" + "mOutputData=" + mOutputData + '}';
}
}
@@ -566,16 +557,14 @@
return name.hashCode();
}
- @NonNull
@Override
- public Data getOutputData() {
+ public @NonNull Data getOutputData() {
return Data.EMPTY;
}
- @NonNull
@Override
- public String toString() {
+ public @NonNull String toString() {
return "Retry";
}
}
diff --git a/work/work-runtime/src/main/java/androidx/work/Logger.java b/work/work-runtime/src/main/java/androidx/work/Logger.java
index 6c69513..2793e5d 100644
--- a/work/work-runtime/src/main/java/androidx/work/Logger.java
+++ b/work/work-runtime/src/main/java/androidx/work/Logger.java
@@ -18,9 +18,10 @@
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
/**
* The class that handles logging requests for {@link WorkManager}. Currently, this class is not
* accessible and has only one default implementation, {@link LogcatLogger}, that writes to logcat
@@ -55,8 +56,7 @@
* @param tag The {@link String} tag to use when logging
* @return The prefixed {@link String} tag to use when logging
*/
- @NonNull
- public static String tagWithPrefix(@NonNull String tag) {
+ public static @NonNull String tagWithPrefix(@NonNull String tag) {
int length = tag.length();
StringBuilder withPrefix = new StringBuilder(MAX_TAG_LENGTH);
withPrefix.append(TAG_PREFIX);
@@ -72,8 +72,7 @@
/**
* @return The current {@link Logger}.
*/
- @NonNull
- public static Logger get() {
+ public static @NonNull Logger get() {
// Logger may not be explicitly initialized by some tests which do not instantiate
// WorkManagerImpl directly.
//
diff --git a/work/work-runtime/src/main/java/androidx/work/Operation.java b/work/work-runtime/src/main/java/androidx/work/Operation.java
index 1eb01b7a..db11da5 100644
--- a/work/work-runtime/src/main/java/androidx/work/Operation.java
+++ b/work/work-runtime/src/main/java/androidx/work/Operation.java
@@ -16,7 +16,6 @@
package androidx.work;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
@@ -24,6 +23,8 @@
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+
/**
* An object that provides information about the execution of an asynchronous command being
* performed by {@link WorkManager}. Operations are generally tied to enqueue or cancel commands;
@@ -51,8 +52,7 @@
* @return A {@link LiveData} of the Operation {@link State}; you must use
* {@link LiveData#observe(LifecycleOwner, Observer)} to receive updates
*/
- @NonNull
- LiveData<State> getState();
+ @NonNull LiveData<State> getState();
/**
* Gets a {@link ListenableFuture} for the terminal state of the {@link Operation}. This will
@@ -66,8 +66,7 @@
* @return A {@link ListenableFuture} with information about {@link Operation}'s
* {@link State.SUCCESS} state.
*/
- @NonNull
- ListenableFuture<State.SUCCESS> getResult();
+ @NonNull ListenableFuture<State.SUCCESS> getResult();
/**
* The lifecycle state of an {@link Operation}.
@@ -91,8 +90,7 @@
}
@Override
- @NonNull
- public String toString() {
+ public @NonNull String toString() {
return "SUCCESS";
}
}
@@ -106,8 +104,7 @@
}
@Override
- @NonNull
- public String toString() {
+ public @NonNull String toString() {
return "IN_PROGRESS";
}
}
@@ -127,14 +124,12 @@
/**
* @return The {@link Throwable} which caused the {@link Operation} to fail.
*/
- @NonNull
- public Throwable getThrowable() {
+ public @NonNull Throwable getThrowable() {
return mThrowable;
}
@Override
- @NonNull
- public String toString() {
+ public @NonNull String toString() {
return "FAILURE (" + mThrowable.getMessage() + ")";
}
}
diff --git a/work/work-runtime/src/main/java/androidx/work/ProgressUpdater.java b/work/work-runtime/src/main/java/androidx/work/ProgressUpdater.java
index f2c22d6..40a851e 100644
--- a/work/work-runtime/src/main/java/androidx/work/ProgressUpdater.java
+++ b/work/work-runtime/src/main/java/androidx/work/ProgressUpdater.java
@@ -18,10 +18,10 @@
import android.content.Context;
-import androidx.annotation.NonNull;
-
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+
import java.util.UUID;
/**
@@ -38,8 +38,7 @@
* Cancelling this {@link ListenableFuture} does not cancel the writes to the database
* to update progress.
*/
- @NonNull
- ListenableFuture<Void> updateProgress(
+ @NonNull ListenableFuture<Void> updateProgress(
@NonNull Context context,
@NonNull UUID id,
@NonNull Data data);
diff --git a/work/work-runtime/src/main/java/androidx/work/RunnableScheduler.java b/work/work-runtime/src/main/java/androidx/work/RunnableScheduler.java
index d6b1379..a749449 100644
--- a/work/work-runtime/src/main/java/androidx/work/RunnableScheduler.java
+++ b/work/work-runtime/src/main/java/androidx/work/RunnableScheduler.java
@@ -17,7 +17,8 @@
package androidx.work;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
+
+import org.jspecify.annotations.NonNull;
/**
* Can be used to schedule {@link Runnable}s after a delay in milliseconds.
diff --git a/work/work-runtime/src/main/java/androidx/work/WorkContinuation.java b/work/work-runtime/src/main/java/androidx/work/WorkContinuation.java
index 94a939f..7413706 100644
--- a/work/work-runtime/src/main/java/androidx/work/WorkContinuation.java
+++ b/work/work-runtime/src/main/java/androidx/work/WorkContinuation.java
@@ -15,7 +15,6 @@
*/
package androidx.work;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
@@ -23,6 +22,8 @@
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+
import java.util.Collections;
import java.util.List;
diff --git a/work/work-runtime/src/main/java/androidx/work/WorkManagerInitializer.java b/work/work-runtime/src/main/java/androidx/work/WorkManagerInitializer.java
index 93acbdf..bc91f9a 100644
--- a/work/work-runtime/src/main/java/androidx/work/WorkManagerInitializer.java
+++ b/work/work-runtime/src/main/java/androidx/work/WorkManagerInitializer.java
@@ -18,9 +18,10 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.startup.Initializer;
+import org.jspecify.annotations.NonNull;
+
import java.util.Collections;
import java.util.List;
@@ -31,18 +32,16 @@
private static final String TAG = Logger.tagWithPrefix("WrkMgrInitializer");
- @NonNull
@Override
- public WorkManager create(@NonNull Context context) {
+ public @NonNull WorkManager create(@NonNull Context context) {
// Initialize WorkManager with the default configuration.
Logger.get().debug(TAG, "Initializing WorkManager with default configuration.");
WorkManager.initialize(context, new Configuration.Builder().build());
return WorkManager.getInstance(context);
}
- @NonNull
@Override
- public List<Class<? extends androidx.startup.Initializer<?>>> dependencies() {
+ public @NonNull List<Class<? extends androidx.startup.Initializer<?>>> dependencies() {
return Collections.emptyList();
}
}
diff --git a/work/work-runtime/src/main/java/androidx/work/WorkerParameters.java b/work/work-runtime/src/main/java/androidx/work/WorkerParameters.java
index fa8f5a2..f38b6d1 100644
--- a/work/work-runtime/src/main/java/androidx/work/WorkerParameters.java
+++ b/work/work-runtime/src/main/java/androidx/work/WorkerParameters.java
@@ -20,14 +20,15 @@
import android.net.Uri;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.work.impl.utils.taskexecutor.TaskExecutor;
import kotlin.coroutines.CoroutineContext;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
@@ -238,7 +239,6 @@
public @NonNull List<Uri> triggeredContentUris = Collections.emptyList();
@RequiresApi(28)
- @Nullable
- public Network network;
+ public @Nullable Network network;
}
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/DefaultRunnableScheduler.java b/work/work-runtime/src/main/java/androidx/work/impl/DefaultRunnableScheduler.java
index 2583e96..82079c3 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/DefaultRunnableScheduler.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/DefaultRunnableScheduler.java
@@ -19,11 +19,12 @@
import android.os.Handler;
import android.os.Looper;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.core.os.HandlerCompat;
import androidx.work.RunnableScheduler;
+import org.jspecify.annotations.NonNull;
+
/**
* The default implementation of the {@link androidx.work.RunnableScheduler} used by
* {@link androidx.work.impl.background.greedy.GreedyScheduler}.
@@ -38,8 +39,7 @@
mHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}
- @NonNull
- public Handler getHandler() {
+ public @NonNull Handler getHandler() {
return mHandler;
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/ExecutionListener.java b/work/work-runtime/src/main/java/androidx/work/impl/ExecutionListener.java
index f3eb4235..a67f925 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/ExecutionListener.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/ExecutionListener.java
@@ -16,11 +16,12 @@
package androidx.work.impl;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.Worker;
import androidx.work.impl.model.WorkGenerationalId;
+import org.jspecify.annotations.NonNull;
+
/**
* Listener that reports the result of a {@link Worker}'s execution.
*
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/Processor.java b/work/work-runtime/src/main/java/androidx/work/impl/Processor.java
index 95a2a95..ee69780 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/Processor.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/Processor.java
@@ -22,8 +22,6 @@
import android.content.Intent;
import android.os.PowerManager;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.content.ContextCompat;
import androidx.work.Configuration;
@@ -38,6 +36,9 @@
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@@ -54,8 +55,7 @@
private static final String TAG = Logger.tagWithPrefix("Processor");
private static final String FOREGROUND_WAKELOCK_TAG = "ProcessorForegroundLck";
- @Nullable
- private PowerManager.WakeLock mForegroundLock;
+ private PowerManager.@Nullable WakeLock mForegroundLock;
private Context mAppContext;
private Configuration mConfiguration;
@@ -108,7 +108,7 @@
@SuppressWarnings("ConstantConditions")
public boolean startWork(
@NonNull StartStopToken startStopToken,
- @Nullable WorkerParameters.RuntimeExtras runtimeExtras) {
+ WorkerParameters.@Nullable RuntimeExtras runtimeExtras) {
WorkGenerationalId id = startStopToken.getId();
String workSpecId = id.getWorkSpecId();
ArrayList<String> tags = new ArrayList<>();
@@ -352,8 +352,7 @@
}
}
- @Nullable
- private WorkerWrapper getWorkerWrapperUnsafe(@NonNull String workSpecId) {
+ private @Nullable WorkerWrapper getWorkerWrapperUnsafe(@NonNull String workSpecId) {
WorkerWrapper workerWrapper = mForegroundWorkMap.get(workSpecId);
if (workerWrapper == null) {
workerWrapper = mEnqueuedWorkMap.get(workSpecId);
@@ -366,8 +365,7 @@
*
* @param workSpecId id of running worker
*/
- @Nullable
- public WorkSpec getRunningWorkSpec(@NonNull String workSpecId) {
+ public @Nullable WorkSpec getRunningWorkSpec(@NonNull String workSpecId) {
synchronized (mLock) {
WorkerWrapper workerWrapper = getWorkerWrapperUnsafe(workSpecId);
if (workerWrapper != null) {
@@ -378,7 +376,7 @@
}
}
- private void runOnExecuted(@NonNull final WorkGenerationalId id, boolean needsReschedule) {
+ private void runOnExecuted(final @NonNull WorkGenerationalId id, boolean needsReschedule) {
mWorkTaskExecutor.getMainThreadExecutor().execute(
() -> {
synchronized (mLock) {
@@ -412,8 +410,7 @@
}
}
- @Nullable
- private WorkerWrapper cleanUpWorkerUnsafe(@NonNull String id) {
+ private @Nullable WorkerWrapper cleanUpWorkerUnsafe(@NonNull String id) {
WorkerWrapper wrapper = mForegroundWorkMap.remove(id);
boolean wasForeground = wrapper != null;
if (!wasForeground) {
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/Scheduler.java b/work/work-runtime/src/main/java/androidx/work/impl/Scheduler.java
index 399fbc0c..33b273f 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/Scheduler.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/Scheduler.java
@@ -15,10 +15,11 @@
*/
package androidx.work.impl;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.impl.model.WorkSpec;
+import org.jspecify.annotations.NonNull;
+
/**
* An interface for classes responsible for scheduling background work.
*
@@ -43,7 +44,7 @@
*
* @param workSpecs The array of {@link WorkSpec}s to schedule
*/
- void schedule(@NonNull WorkSpec... workSpecs);
+ void schedule(WorkSpec @NonNull ... workSpecs);
/**
* Cancel the work identified by the given {@link WorkSpec} id.
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/Schedulers.java b/work/work-runtime/src/main/java/androidx/work/impl/Schedulers.java
index b0786fe..1a24699 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/Schedulers.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/Schedulers.java
@@ -23,8 +23,6 @@
import android.content.Context;
import android.os.Build;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.work.Clock;
import androidx.work.Configuration;
@@ -36,6 +34,9 @@
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.model.WorkSpecDao;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.List;
import java.util.concurrent.Executor;
@@ -147,8 +148,7 @@
}
}
- @NonNull
- static Scheduler createBestAvailableBackgroundScheduler(@NonNull Context context,
+ static @NonNull Scheduler createBestAvailableBackgroundScheduler(@NonNull Context context,
@NonNull WorkDatabase workDatabase, Configuration configuration) {
Scheduler scheduler;
@@ -168,8 +168,8 @@
return scheduler;
}
- @Nullable
- private static Scheduler tryCreateGcmBasedScheduler(@NonNull Context context, Clock clock) {
+ private static @Nullable Scheduler tryCreateGcmBasedScheduler(@NonNull Context context,
+ Clock clock) {
try {
Class<?> klass = Class.forName(GCM_SCHEDULER);
Scheduler scheduler =
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkContinuationImpl.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkContinuationImpl.java
index 5a93ee6..22e7f4f 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkContinuationImpl.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkContinuationImpl.java
@@ -20,8 +20,6 @@
import android.text.TextUtils;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.lifecycle.LiveData;
import androidx.work.ArrayCreatingInputMerger;
@@ -40,6 +38,9 @@
import kotlin.Unit;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@@ -65,33 +66,27 @@
private boolean mEnqueued;
private Operation mOperation;
- @NonNull
- public WorkManagerImpl getWorkManagerImpl() {
+ public @NonNull WorkManagerImpl getWorkManagerImpl() {
return mWorkManagerImpl;
}
- @Nullable
- public String getName() {
+ public @Nullable String getName() {
return mName;
}
- @NonNull
- public ExistingWorkPolicy getExistingWorkPolicy() {
+ public @NonNull ExistingWorkPolicy getExistingWorkPolicy() {
return mExistingWorkPolicy;
}
- @NonNull
- public List<? extends WorkRequest> getWork() {
+ public @NonNull List<? extends WorkRequest> getWork() {
return mWork;
}
- @NonNull
- public List<String> getIds() {
+ public @NonNull List<String> getIds() {
return mIds;
}
- @NonNull
- public List<String> getAllIds() {
+ public @NonNull List<String> getAllIds() {
return mAllIds;
}
@@ -106,8 +101,7 @@
mEnqueued = true;
}
- @Nullable
- public List<WorkContinuationImpl> getParents() {
+ public @Nullable List<WorkContinuationImpl> getParents() {
return mParents;
}
@@ -180,9 +174,8 @@
return mWorkManagerImpl.getWorkInfosById(mAllIds);
}
- @NonNull
@Override
- public ListenableFuture<List<WorkInfo>> getWorkInfos() {
+ public @NonNull ListenableFuture<List<WorkInfo>> getWorkInfos() {
return StatusRunnable.forStringIds(mWorkManagerImpl.getWorkDatabase(),
mWorkManagerImpl.getWorkTaskExecutor(), mAllIds);
}
@@ -281,9 +274,9 @@
/**
* @return the {@link Set} of pre-requisites for a given {@link WorkContinuationImpl}.
*/
- @NonNull
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static Set<String> prerequisitesFor(@NonNull WorkContinuationImpl continuation) {
+ public static @NonNull Set<String> prerequisitesFor(
+ @NonNull WorkContinuationImpl continuation) {
Set<String> preRequisites = new HashSet<>();
List<WorkContinuationImpl> parents = continuation.getParents();
if (parents != null && !parents.isEmpty()) {
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
index e8153ad..f78fba2 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
@@ -37,8 +37,6 @@
import android.content.Intent;
import android.os.Build;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.arch.core.util.Function;
@@ -82,6 +80,9 @@
import kotlinx.coroutines.CoroutineScope;
import kotlinx.coroutines.flow.Flow;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.Collections;
import java.util.List;
import java.util.UUID;
@@ -265,8 +266,7 @@
* @return The application {@link Context} associated with this WorkManager.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public Context getApplicationContext() {
+ public @NonNull Context getApplicationContext() {
return mContext;
}
@@ -274,25 +274,22 @@
* @return The {@link WorkDatabase} instance associated with this WorkManager.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public WorkDatabase getWorkDatabase() {
+ public @NonNull WorkDatabase getWorkDatabase() {
return mWorkDatabase;
}
/**
* @return workmanager's CoroutineScope
*/
- @NonNull
- CoroutineScope getWorkManagerScope() {
+ @NonNull CoroutineScope getWorkManagerScope() {
return mWorkManagerScope;
}
/**
* @return The {@link Configuration} instance associated with this WorkManager.
*/
- @NonNull
@Override
- public Configuration getConfiguration() {
+ public @NonNull Configuration getConfiguration() {
return mConfiguration;
}
@@ -333,14 +330,12 @@
* @return the {@link Trackers} used by {@link WorkManager}
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public Trackers getTrackers() {
+ public @NonNull Trackers getTrackers() {
return mTrackers;
}
@Override
- @NonNull
- public Operation enqueue(
+ public @NonNull Operation enqueue(
@NonNull List<? extends WorkRequest> requests) {
// This error is not being propagated as part of the Operation, as we want the
@@ -373,9 +368,8 @@
return new WorkContinuationImpl(this, uniqueWorkName, existingWorkPolicy, requests);
}
- @NonNull
@Override
- public Operation enqueueUniqueWork(@NonNull String uniqueWorkName,
+ public @NonNull Operation enqueueUniqueWork(@NonNull String uniqueWorkName,
@NonNull ExistingWorkPolicy existingWorkPolicy,
@NonNull List<OneTimeWorkRequest> requests) {
return new WorkContinuationImpl(this, uniqueWorkName,
@@ -383,8 +377,7 @@
}
@Override
- @NonNull
- public Operation enqueueUniquePeriodicWork(
+ public @NonNull Operation enqueueUniquePeriodicWork(
@NonNull String uniqueWorkName,
@NonNull ExistingPeriodicWorkPolicy existingPeriodicWorkPolicy,
@NonNull PeriodicWorkRequest request) {
@@ -401,8 +394,7 @@
/**
* Creates a {@link WorkContinuation} for the given unique {@link PeriodicWorkRequest}.
*/
- @NonNull
- public WorkContinuationImpl createWorkContinuationForUniquePeriodicWork(
+ public @NonNull WorkContinuationImpl createWorkContinuationForUniquePeriodicWork(
@NonNull String uniqueWorkName,
@NonNull ExistingPeriodicWorkPolicy existingPeriodicWorkPolicy,
@NonNull PeriodicWorkRequest periodicWork) {
@@ -425,13 +417,12 @@
}
@Override
- public @NonNull Operation cancelAllWorkByTag(@NonNull final String tag) {
+ public @NonNull Operation cancelAllWorkByTag(final @NonNull String tag) {
return CancelWorkRunnable.forTag(tag, this);
}
@Override
- @NonNull
- public Operation cancelUniqueWork(@NonNull String uniqueWorkName) {
+ public @NonNull Operation cancelUniqueWork(@NonNull String uniqueWorkName) {
return CancelWorkRunnable.forName(uniqueWorkName, this);
}
@@ -440,9 +431,8 @@
return CancelWorkRunnable.forAll(this);
}
- @NonNull
@Override
- public PendingIntent createCancelPendingIntent(@NonNull UUID id) {
+ public @NonNull PendingIntent createCancelPendingIntent(@NonNull UUID id) {
Intent intent = createCancelWorkIntent(mContext, id.toString());
int flags = FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= 31) {
@@ -487,9 +477,8 @@
mWorkTaskExecutor);
}
- @NonNull
@Override
- public Flow<WorkInfo> getWorkInfoByIdFlow(@NonNull UUID id) {
+ public @NonNull Flow<WorkInfo> getWorkInfoByIdFlow(@NonNull UUID id) {
return getWorkStatusPojoFlowDataForIds(getWorkDatabase().workSpecDao(), id);
}
@@ -498,9 +487,8 @@
return StatusRunnable.forUUID(mWorkDatabase, mWorkTaskExecutor, id);
}
- @NonNull
@Override
- public Flow<List<WorkInfo>> getWorkInfosByTagFlow(@NonNull String tag) {
+ public @NonNull Flow<List<WorkInfo>> getWorkInfosByTagFlow(@NonNull String tag) {
WorkSpecDao workSpecDao = mWorkDatabase.workSpecDao();
return getWorkStatusPojoFlowForTag(workSpecDao,
mWorkTaskExecutor.getTaskCoroutineDispatcher(), tag);
@@ -523,8 +511,7 @@
}
@Override
- @NonNull
- public LiveData<List<WorkInfo>> getWorkInfosForUniqueWorkLiveData(
+ public @NonNull LiveData<List<WorkInfo>> getWorkInfosForUniqueWorkLiveData(
@NonNull String uniqueWorkName) {
WorkSpecDao workSpecDao = mWorkDatabase.workSpecDao();
LiveData<List<WorkSpec.WorkInfoPojo>> inputLiveData =
@@ -535,24 +522,22 @@
mWorkTaskExecutor);
}
- @NonNull
@Override
- public Flow<List<WorkInfo>> getWorkInfosForUniqueWorkFlow(@NonNull String uniqueWorkName) {
+ public @NonNull Flow<List<WorkInfo>> getWorkInfosForUniqueWorkFlow(
+ @NonNull String uniqueWorkName) {
WorkSpecDao workSpecDao = mWorkDatabase.workSpecDao();
return getWorkStatusPojoFlowForName(workSpecDao,
mWorkTaskExecutor.getTaskCoroutineDispatcher(), uniqueWorkName);
}
@Override
- @NonNull
- public ListenableFuture<List<WorkInfo>> getWorkInfosForUniqueWork(
+ public @NonNull ListenableFuture<List<WorkInfo>> getWorkInfosForUniqueWork(
@NonNull String uniqueWorkName) {
return StatusRunnable.forUniqueWork(mWorkDatabase, mWorkTaskExecutor, uniqueWorkName);
}
- @NonNull
@Override
- public LiveData<List<WorkInfo>> getWorkInfosLiveData(
+ public @NonNull LiveData<List<WorkInfo>> getWorkInfosLiveData(
@NonNull WorkQuery workQuery) {
RawWorkInfoDao rawWorkInfoDao = mWorkDatabase.rawWorkInfoDao();
LiveData<List<WorkSpec.WorkInfoPojo>> inputLiveData =
@@ -564,23 +549,20 @@
mWorkTaskExecutor);
}
- @NonNull
@Override
- public Flow<List<WorkInfo>> getWorkInfosFlow(@NonNull WorkQuery workQuery) {
+ public @NonNull Flow<List<WorkInfo>> getWorkInfosFlow(@NonNull WorkQuery workQuery) {
RawWorkInfoDao rawWorkInfoDao = mWorkDatabase.rawWorkInfoDao();
return getWorkInfoPojosFlow(rawWorkInfoDao, mWorkTaskExecutor.getTaskCoroutineDispatcher(),
RawQueries.toRawQuery(workQuery));
}
- @NonNull
@Override
- public ListenableFuture<List<WorkInfo>> getWorkInfos(@NonNull WorkQuery workQuery) {
+ public @NonNull ListenableFuture<List<WorkInfo>> getWorkInfos(@NonNull WorkQuery workQuery) {
return StatusRunnable.forWorkQuerySpec(mWorkDatabase, mWorkTaskExecutor, workQuery);
}
- @NonNull
@Override
- public ListenableFuture<UpdateResult> updateWork(@NonNull WorkRequest request) {
+ public @NonNull ListenableFuture<UpdateResult> updateWork(@NonNull WorkRequest request) {
return WorkerUpdater.updateWorkImpl(this, request);
}
@@ -597,9 +579,8 @@
/**
*
*/
- @Nullable
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public RemoteWorkManager getRemoteWorkManager() {
+ public @Nullable RemoteWorkManager getRemoteWorkManager() {
if (mRemoteWorkManager == null) {
synchronized (sLock) {
if (mRemoteWorkManager == null) {
@@ -675,7 +656,7 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public void setReschedulePendingResult(
- @NonNull BroadcastReceiver.PendingResult rescheduleReceiverResult) {
+ BroadcastReceiver.@NonNull PendingResult rescheduleReceiverResult) {
synchronized (sLock) {
// if we have two broadcast in the row, finish old one and use new one
if (mRescheduleReceiverResult != null) {
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkRequestHolder.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkRequestHolder.java
index b29ee1d4..c0a4026 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkRequestHolder.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkRequestHolder.java
@@ -16,11 +16,12 @@
package androidx.work.impl;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.WorkRequest;
import androidx.work.impl.model.WorkSpec;
+import org.jspecify.annotations.NonNull;
+
import java.util.Set;
import java.util.UUID;
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/DelayedWorkTracker.java b/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/DelayedWorkTracker.java
index 83e5ad3..cbc52d5 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/DelayedWorkTracker.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/DelayedWorkTracker.java
@@ -16,7 +16,6 @@
package androidx.work.impl.background.greedy;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.Clock;
import androidx.work.Logger;
@@ -24,10 +23,11 @@
import androidx.work.impl.Scheduler;
import androidx.work.impl.model.WorkSpec;
+import org.jspecify.annotations.NonNull;
+
import java.util.HashMap;
import java.util.Map;
-
/**
* Keeps track of {@link androidx.work.WorkRequest}s that have a timing component in a
* {@link GreedyScheduler}.
@@ -66,7 +66,7 @@
* @param workSpec The {@link WorkSpec} corresponding to the {@link androidx.work.WorkRequest}
* @param nextRunTime time when work should be executed
*/
- public void schedule(@NonNull final WorkSpec workSpec, long nextRunTime) {
+ public void schedule(final @NonNull WorkSpec workSpec, long nextRunTime) {
Runnable existing = mRunnables.remove(workSpec.id);
if (existing != null) {
mRunnableScheduler.cancel(existing);
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java b/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java
index 715338b..f46d32b7 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java
@@ -27,7 +27,6 @@
import android.content.Context;
import android.text.TextUtils;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.work.Configuration;
@@ -50,13 +49,15 @@
import androidx.work.impl.utils.ProcessUtils;
import androidx.work.impl.utils.taskexecutor.TaskExecutor;
+import kotlinx.coroutines.Job;
+
+import org.jspecify.annotations.NonNull;
+
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
-import kotlinx.coroutines.Job;
-
/**
* A greedy {@link Scheduler} that schedules unconstrained, non-timed work. It intentionally does
* not acquire any WakeLocks, instead trying to brute-force them as time allows before the process
@@ -124,7 +125,7 @@
}
@Override
- public void schedule(@NonNull WorkSpec... workSpecs) {
+ public void schedule(WorkSpec @NonNull ... workSpecs) {
if (mInDefaultProcess == null) {
checkDefaultProcess();
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/Alarms.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/Alarms.java
index 4f3c958..afa14b6 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/Alarms.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/Alarms.java
@@ -26,7 +26,6 @@
import android.content.Intent;
import android.os.Build;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.Logger;
import androidx.work.impl.WorkDatabase;
@@ -36,6 +35,8 @@
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.utils.IdGenerator;
+import org.jspecify.annotations.NonNull;
+
/**
* A common class for managing Alarms.
*
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/CommandHandler.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/CommandHandler.java
index 1e73418..1376030 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/CommandHandler.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/CommandHandler.java
@@ -20,8 +20,6 @@
import android.content.Intent;
import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.WorkerThread;
import androidx.work.Clock;
@@ -35,6 +33,9 @@
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.model.WorkSpecDao;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -362,7 +363,7 @@
}
@SuppressWarnings("deprecation")
- private static boolean hasKeys(@Nullable Bundle bundle, @NonNull String... keys) {
+ private static boolean hasKeys(@Nullable Bundle bundle, String @NonNull ... keys) {
if (bundle == null || bundle.isEmpty()) {
return false;
} else {
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintProxyUpdateReceiver.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintProxyUpdateReceiver.java
index cc15885..c181df6 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintProxyUpdateReceiver.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintProxyUpdateReceiver.java
@@ -21,8 +21,6 @@
import android.content.Context;
import android.content.Intent;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.work.Logger;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.background.systemalarm.ConstraintProxy.BatteryChargingProxy;
@@ -32,6 +30,8 @@
import androidx.work.impl.utils.PackageManagerHelper;
import androidx.work.impl.utils.taskexecutor.TaskExecutor;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
/**
* The {@link BroadcastReceiver} responsible for updating constraint proxies.
@@ -58,8 +58,7 @@
* @return an {@link Intent} with information about the constraint proxies which need to be
* enabled.
*/
- @NonNull
- public static Intent newConstraintProxyUpdateIntent(
+ public static @NonNull Intent newConstraintProxyUpdateIntent(
@NonNull Context context,
boolean batteryNotLowProxyEnabled,
boolean batteryChargingProxyEnabled,
@@ -80,7 +79,7 @@
}
@Override
- public void onReceive(@NonNull final Context context, @Nullable final Intent intent) {
+ public void onReceive(final @NonNull Context context, final @Nullable Intent intent) {
String action = intent != null ? intent.getAction() : null;
if (!ACTION.equals(action)) {
Logger.get().debug(TAG, "Ignoring unknown action " + action);
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintsCommandHandler.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintsCommandHandler.java
index e59758c..79e1821 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintsCommandHandler.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintsCommandHandler.java
@@ -21,7 +21,6 @@
import android.content.Context;
import android.content.Intent;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.WorkerThread;
import androidx.work.Clock;
@@ -30,6 +29,8 @@
import androidx.work.impl.constraints.trackers.Trackers;
import androidx.work.impl.model.WorkSpec;
+import org.jspecify.annotations.NonNull;
+
import java.util.ArrayList;
import java.util.List;
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/DelayMetCommandHandler.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/DelayMetCommandHandler.java
index 65151de..40be2e4 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/DelayMetCommandHandler.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/DelayMetCommandHandler.java
@@ -23,8 +23,6 @@
import android.content.Intent;
import android.os.PowerManager;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.WorkerThread;
import androidx.work.Logger;
@@ -38,11 +36,14 @@
import androidx.work.impl.utils.WakeLocks;
import androidx.work.impl.utils.WorkTimer;
-import java.util.concurrent.Executor;
-
import kotlinx.coroutines.CoroutineDispatcher;
import kotlinx.coroutines.Job;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+import java.util.concurrent.Executor;
+
/**
* This is a command handler which attempts to run a work spec given its id.
* Also handles constraints gracefully.
@@ -96,7 +97,7 @@
private final Executor mSerialExecutor;
private final Executor mMainThreadExecutor;
- @Nullable private PowerManager.WakeLock mWakeLock;
+ private PowerManager.@Nullable WakeLock mWakeLock;
private boolean mHasConstraints;
private final StartStopToken mToken;
private final CoroutineDispatcher mCoroutineDispatcher;
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcher.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcher.java
index 43dfe44..48df964 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcher.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcher.java
@@ -23,8 +23,6 @@
import android.text.TextUtils;
import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.work.Logger;
@@ -40,6 +38,9 @@
import androidx.work.impl.utils.taskexecutor.SerialExecutor;
import androidx.work.impl.utils.taskexecutor.TaskExecutor;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.List;
@@ -71,8 +72,7 @@
final List<Intent> mIntents;
Intent mCurrentIntent;
- @Nullable
- private CommandsCompletedListener mCompletedListener;
+ private @Nullable CommandsCompletedListener mCompletedListener;
private StartStopTokens mStartStopTokens;
private final WorkLauncher mWorkLauncher;
@@ -140,7 +140,7 @@
* @return <code>true</code> when the command was added to the command processor queue.
*/
@MainThread
- public boolean add(@NonNull final Intent intent, final int startId) {
+ public boolean add(final @NonNull Intent intent, final int startId) {
Logger.get().debug(TAG, "Adding command " + intent + " (" + startId + ")");
assertMainThread();
String action = intent.getAction();
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmScheduler.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmScheduler.java
index 39f9497..b61c285 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmScheduler.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmScheduler.java
@@ -21,12 +21,13 @@
import android.content.Context;
import android.content.Intent;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.Logger;
import androidx.work.impl.Scheduler;
import androidx.work.impl.model.WorkSpec;
+import org.jspecify.annotations.NonNull;
+
/**
* A {@link Scheduler} that schedules work using {@link android.app.AlarmManager}.
*
@@ -43,7 +44,7 @@
}
@Override
- public void schedule(@NonNull WorkSpec... workSpecs) {
+ public void schedule(WorkSpec @NonNull ... workSpecs) {
for (WorkSpec workSpec : workSpecs) {
scheduleWorkSpec(workSpec);
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
index 158f9fc..b90d3ac 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
@@ -18,7 +18,6 @@
import static androidx.work.impl.background.systemjob.SystemJobInfoConverterExtKt.setRequiredNetworkRequest;
-import android.annotation.SuppressLint;
import android.app.job.JobInfo;
import android.content.ComponentName;
import android.content.Context;
@@ -27,7 +26,6 @@
import android.os.Build;
import android.os.PersistableBundle;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.work.BackoffPolicy;
@@ -38,13 +36,14 @@
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.model.WorkSpec;
+import org.jspecify.annotations.NonNull;
+
/**
* Converts a {@link WorkSpec} into a JobInfo.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresApi(api = WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL)
-@SuppressLint("ClassVerificationFailure")
class SystemJobInfoConverter {
private static final String TAG = Logger.tagWithPrefix("SystemJobInfoConverter");
@@ -164,7 +163,7 @@
* @param networkType The {@link NetworkType} instance.
*/
static void setRequiredNetwork(
- @NonNull JobInfo.Builder builder,
+ JobInfo.@NonNull Builder builder,
@NonNull NetworkType networkType) {
if (Build.VERSION.SDK_INT >= 30 && networkType == NetworkType.TEMPORARILY_UNMETERED) {
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
index f3301ee..a184d32 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
@@ -35,8 +35,6 @@
import android.os.Build;
import android.os.PersistableBundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
@@ -52,6 +50,9 @@
import androidx.work.impl.model.WorkSpecDao;
import androidx.work.impl.utils.IdGenerator;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -100,7 +101,7 @@
}
@Override
- public void schedule(@NonNull WorkSpec... workSpecs) {
+ public void schedule(WorkSpec @NonNull ... workSpecs) {
IdGenerator idGenerator = new IdGenerator(mWorkDatabase);
for (WorkSpec workSpec : workSpecs) {
@@ -357,8 +358,7 @@
return needsReconciling;
}
- @Nullable
- static List<JobInfo> getPendingJobs(
+ static @Nullable List<JobInfo> getPendingJobs(
@NonNull Context context,
@NonNull JobScheduler jobScheduler) {
List<JobInfo> pendingJobs = getSafePendingJobs(jobScheduler);
@@ -383,8 +383,7 @@
*
* For reference: b/133556574, b/133556809, b/133556535
*/
- @Nullable
- private static List<Integer> getPendingJobIds(
+ private static @Nullable List<Integer> getPendingJobIds(
@NonNull Context context,
@NonNull JobScheduler jobScheduler,
@NonNull String workSpecId) {
@@ -407,8 +406,8 @@
return jobIds;
}
- @Nullable
- private static WorkGenerationalId getWorkGenerationalIdFromJobInfo(@NonNull JobInfo jobInfo) {
+ private static @Nullable WorkGenerationalId getWorkGenerationalIdFromJobInfo(
+ @NonNull JobInfo jobInfo) {
PersistableBundle extras = jobInfo.getExtras();
try {
if (extras != null && extras.containsKey(EXTRA_WORK_SPEC_ID)) {
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobService.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobService.java
index 009b245..283ab2f 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobService.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobService.java
@@ -47,8 +47,6 @@
import android.os.PersistableBundle;
import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.work.Logger;
@@ -63,6 +61,9 @@
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.model.WorkGenerationalId;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@@ -220,9 +221,8 @@
}
}
- @Nullable
@SuppressWarnings("ConstantConditions")
- private static WorkGenerationalId workGenerationalIdFromJobParameters(
+ private static @Nullable WorkGenerationalId workGenerationalIdFromJobParameters(
@NonNull JobParameters parameters
) {
try {
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/diagnostics/DiagnosticsReceiver.java b/work/work-runtime/src/main/java/androidx/work/impl/diagnostics/DiagnosticsReceiver.java
index b3f54e8..292f3a4 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/diagnostics/DiagnosticsReceiver.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/diagnostics/DiagnosticsReceiver.java
@@ -20,14 +20,15 @@
import android.content.Context;
import android.content.Intent;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.work.Logger;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import androidx.work.impl.workers.DiagnosticsWorker;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* The {@link android.content.BroadcastReceiver} which dumps out useful diagnostics information.
*
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/foreground/ForegroundProcessor.java b/work/work-runtime/src/main/java/androidx/work/impl/foreground/ForegroundProcessor.java
index 2958019..4c514f3 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/foreground/ForegroundProcessor.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/foreground/ForegroundProcessor.java
@@ -16,10 +16,11 @@
package androidx.work.impl.foreground;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.ForegroundInfo;
+import org.jspecify.annotations.NonNull;
+
/**
* An interface that provides {@link androidx.work.impl.WorkerWrapper} the hooks to move a
* {@link androidx.work.ListenableWorker}s execution to the foreground.
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java b/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java
index a327b14..19dedd2 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java
@@ -28,8 +28,6 @@
import android.text.TextUtils;
import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.work.ForegroundInfo;
@@ -45,14 +43,17 @@
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.utils.taskexecutor.TaskExecutor;
+import kotlinx.coroutines.Job;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
-import kotlinx.coroutines.Job;
-
/**
* Handles requests for executing {@link androidx.work.WorkRequest}s on behalf of
* {@link SystemForegroundService}.
@@ -100,8 +101,7 @@
@SuppressWarnings("WeakerAccess") // Synthetic access
final WorkConstraintsTracker mConstraintsTracker;
- @Nullable
- private Callback mCallback;
+ private @Nullable Callback mCallback;
SystemForegroundDispatcher(@NonNull Context context) {
mContext = context;
@@ -370,8 +370,7 @@
* foreground service
* @return The {@link Intent}
*/
- @NonNull
- public static Intent createStartForegroundIntent(
+ public static @NonNull Intent createStartForegroundIntent(
@NonNull Context context,
@NonNull WorkGenerationalId id,
@NonNull ForegroundInfo info) {
@@ -393,8 +392,7 @@
* foreground service
* @return The {@link Intent}
*/
- @NonNull
- public static Intent createCancelWorkIntent(
+ public static @NonNull Intent createCancelWorkIntent(
@NonNull Context context,
@NonNull String workSpecId) {
Intent intent = new Intent(context, SystemForegroundService.class);
@@ -414,8 +412,7 @@
* @param info The {@link ForegroundInfo}
* @return The {@link Intent}
*/
- @NonNull
- public static Intent createNotifyIntent(
+ public static @NonNull Intent createNotifyIntent(
@NonNull Context context,
@NonNull WorkGenerationalId id,
@NonNull ForegroundInfo info) {
@@ -435,8 +432,7 @@
* @param context The application {@link Context}
* @return The {@link Intent}
*/
- @NonNull
- public static Intent createStopForegroundIntent(@NonNull Context context) {
+ public static @NonNull Intent createStopForegroundIntent(@NonNull Context context) {
Intent intent = new Intent(context, SystemForegroundService.class);
intent.setAction(ACTION_STOP_FOREGROUND);
return intent;
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundService.java b/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundService.java
index b48701daf..819d686 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundService.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundService.java
@@ -28,14 +28,15 @@
import android.os.Build;
import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission;
import androidx.annotation.RestrictTo;
import androidx.lifecycle.LifecycleService;
import androidx.work.Logger;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -44,8 +45,7 @@
private static final String TAG = Logger.tagWithPrefix("SystemFgService");
- @Nullable
- private static SystemForegroundService sForegroundService = null;
+ private static @Nullable SystemForegroundService sForegroundService = null;
private boolean mIsShutdown;
@@ -133,7 +133,7 @@
public void startForeground(
final int notificationId,
final int notificationType,
- @NonNull final Notification notification) {
+ final @NonNull Notification notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Api31Impl.startForeground(SystemForegroundService.this, notificationId,
notification, notificationType);
@@ -148,7 +148,7 @@
@MainThread
@RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
@Override
- public void notify(final int notificationId, @NonNull final Notification notification) {
+ public void notify(final int notificationId, final @NonNull Notification notification) {
mNotificationManager.notify(notificationId, notification);
}
@@ -161,8 +161,7 @@
/**
* @return The current instance of {@link SystemForegroundService}.
*/
- @Nullable
- public static SystemForegroundService getInstance() {
+ public static @Nullable SystemForegroundService getInstance() {
return sForegroundService;
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueRunnable.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueRunnable.java
index 52eb1d3..b0a9d4d 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueRunnable.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueRunnable.java
@@ -30,7 +30,6 @@
import android.text.TextUtils;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.work.ExistingWorkPolicy;
@@ -47,6 +46,8 @@
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.model.WorkSpecDao;
+import org.jspecify.annotations.NonNull;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
index 2e7707d..e347899 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
@@ -25,7 +25,6 @@
import static androidx.work.WorkInfo.State.ENQUEUED;
import static androidx.work.impl.model.WorkSpec.SCHEDULE_NOT_REQUESTED_YET;
-import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.AlarmManager;
import android.app.ApplicationExitInfo;
@@ -45,8 +44,6 @@
import android.os.Build;
import android.text.TextUtils;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.core.os.UserManagerCompat;
@@ -63,6 +60,9 @@
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.model.WorkSpecDao;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -193,7 +193,6 @@
* @return {@code true} If the application was force stopped.
*/
@VisibleForTesting
- @SuppressLint("ClassVerificationFailure")
public boolean isForceStopped() {
// Alarms get cancelled when an app is force-stopped starting at Eclair MR1.
// Cancelling of Jobs on force-stop was introduced in N-MR1 (SDK 25).
@@ -385,7 +384,6 @@
return intent;
}
- @SuppressLint("ClassVerificationFailure")
static void setAlarm(Context context) {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
// Using FLAG_UPDATE_CURRENT, because we only ever want once instance of this alarm.
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/LiveDataUtils.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/LiveDataUtils.java
index 225607e..6821a84 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/LiveDataUtils.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/LiveDataUtils.java
@@ -19,8 +19,6 @@
import android.annotation.SuppressLint;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.arch.core.util.Function;
import androidx.lifecycle.LiveData;
@@ -28,6 +26,9 @@
import androidx.lifecycle.Observer;
import androidx.work.impl.utils.taskexecutor.TaskExecutor;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
/**
* Utility methods for {@link LiveData}.
*
@@ -50,11 +51,10 @@
* @return A new {@link LiveData} of type {@code Out}
*/
@SuppressLint("LambdaLast")
- @NonNull
- public static <In, Out> LiveData<Out> dedupedMappedLiveDataFor(
+ public static <In, Out> @NonNull LiveData<Out> dedupedMappedLiveDataFor(
@NonNull LiveData<In> inputLiveData,
- @NonNull final Function<In, Out> mappingMethod,
- @NonNull final TaskExecutor workTaskExecutor) {
+ final @NonNull Function<In, Out> mappingMethod,
+ final @NonNull TaskExecutor workTaskExecutor) {
final Object lock = new Object();
final MediatorLiveData<Out> outputLiveData = new MediatorLiveData<>();
@@ -64,7 +64,7 @@
Out mCurrentOutput = null;
@Override
- public void onChanged(@Nullable final In input) {
+ public void onChanged(final @Nullable In input) {
workTaskExecutor.executeOnTaskThread(new Runnable() {
@Override
public void run() {
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/PackageManagerHelper.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/PackageManagerHelper.java
index 3708813..b9ab5a2 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/PackageManagerHelper.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/PackageManagerHelper.java
@@ -19,9 +19,10 @@
import android.content.Context;
import android.content.pm.PackageManager;
-import androidx.annotation.NonNull;
import androidx.work.Logger;
+import org.jspecify.annotations.NonNull;
+
/**
* Helper class for common {@link PackageManager} functions
*/
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/PreferenceUtils.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/PreferenceUtils.java
index a981bc4..08d8ff0 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/PreferenceUtils.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/PreferenceUtils.java
@@ -18,11 +18,9 @@
import static android.content.Context.MODE_PRIVATE;
-
import android.content.Context;
import android.content.SharedPreferences;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Transformations;
@@ -30,6 +28,8 @@
import androidx.work.impl.WorkDatabase;
import androidx.work.impl.model.Preference;
+import org.jspecify.annotations.NonNull;
+
/**
* Preference Utils for WorkManager.
*
@@ -71,8 +71,7 @@
* @return A {@link LiveData} of the last time (in milliseconds) a {@code cancelAll} method was
* called
*/
- @NonNull
- public LiveData<Long> getLastCancelAllTimeMillisLiveData() {
+ public @NonNull LiveData<Long> getLastCancelAllTimeMillisLiveData() {
LiveData<Long> observableValue =
mWorkDatabase.preferenceDao().getObservableLongValue(KEY_LAST_CANCEL_ALL_TIME_MS);
return Transformations.map(observableValue, (Long value) -> value != null ? value : 0L);
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/SerialExecutorImpl.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/SerialExecutorImpl.java
index 4339438..1bd475e 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/SerialExecutorImpl.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/SerialExecutorImpl.java
@@ -17,10 +17,11 @@
package androidx.work.impl.utils;
import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.work.impl.utils.taskexecutor.SerialExecutor;
+import org.jspecify.annotations.NonNull;
+
import java.util.ArrayDeque;
import java.util.concurrent.Executor;
@@ -71,9 +72,8 @@
}
}
- @NonNull
@VisibleForTesting
- public Executor getDelegatedExecutor() {
+ public @NonNull Executor getDelegatedExecutor() {
return mExecutor;
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/SynchronousExecutor.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/SynchronousExecutor.java
index 95edf78..39b1637 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/SynchronousExecutor.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/SynchronousExecutor.java
@@ -16,9 +16,10 @@
package androidx.work.impl.utils;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import org.jspecify.annotations.NonNull;
+
import java.util.concurrent.Executor;
/**
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/WorkForegroundUpdater.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/WorkForegroundUpdater.java
index 8639934..314127b 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/WorkForegroundUpdater.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/WorkForegroundUpdater.java
@@ -16,15 +16,14 @@
package androidx.work.impl.utils;
+import static androidx.work.ListenableFutureKt.executeAsync;
import static androidx.work.impl.foreground.SystemForegroundDispatcher.createNotifyIntent;
import static androidx.work.impl.model.WorkSpecKt.generationalId;
-import static androidx.work.ListenableFutureKt.executeAsync;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.ForegroundInfo;
import androidx.work.ForegroundUpdater;
@@ -37,8 +36,9 @@
import com.google.common.util.concurrent.ListenableFuture;
-import java.util.UUID;
+import org.jspecify.annotations.NonNull;
+import java.util.UUID;
/**
* Transitions a {@link androidx.work.ListenableWorker} to run in the context of a foreground
@@ -70,12 +70,11 @@
mWorkSpecDao = workDatabase.workSpecDao();
}
- @NonNull
@Override
- public ListenableFuture<Void> setForegroundAsync(
- @NonNull final Context context,
- @NonNull final UUID id,
- @NonNull final ForegroundInfo foregroundInfo) {
+ public @NonNull ListenableFuture<Void> setForegroundAsync(
+ final @NonNull Context context,
+ final @NonNull UUID id,
+ final @NonNull ForegroundInfo foregroundInfo) {
return executeAsync(mTaskExecutor.getSerialTaskExecutor(), "setForegroundAsync",
() -> {
String workSpecId = id.toString();
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/WorkProgressUpdater.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/WorkProgressUpdater.java
index f1d12ba5..21ef153 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/WorkProgressUpdater.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/WorkProgressUpdater.java
@@ -20,7 +20,6 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.Data;
import androidx.work.Logger;
@@ -34,6 +33,8 @@
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+
import java.util.UUID;
/**
@@ -61,12 +62,11 @@
mTaskExecutor = taskExecutor;
}
- @NonNull
@Override
- public ListenableFuture<Void> updateProgress(
- @NonNull final Context context,
- @NonNull final UUID id,
- @NonNull final Data data) {
+ public @NonNull ListenableFuture<Void> updateProgress(
+ final @NonNull Context context,
+ final @NonNull UUID id,
+ final @NonNull Data data) {
return executeAsync(mTaskExecutor.getSerialTaskExecutor(), "updateProgress", () -> {
String workSpecId = id.toString();
Logger.get().debug(TAG, "Updating progress for " + id + " (" + data + ")");
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/WorkTimer.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/WorkTimer.java
index bbd9da2..e61f19a 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/WorkTimer.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/WorkTimer.java
@@ -16,7 +16,6 @@
package androidx.work.impl.utils;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.work.Logger;
@@ -24,6 +23,8 @@
import androidx.work.WorkRequest;
import androidx.work.impl.model.WorkGenerationalId;
+import org.jspecify.annotations.NonNull;
+
import java.util.HashMap;
import java.util.Map;
@@ -61,7 +62,7 @@
* {@code processingTimeMillis}
*/
@SuppressWarnings("FutureReturnValueIgnored")
- public void startTimer(@NonNull final WorkGenerationalId id,
+ public void startTimer(final @NonNull WorkGenerationalId id,
long processingTimeMillis,
@NonNull TimeLimitExceededListener listener) {
@@ -81,7 +82,7 @@
*
* @param id The {@link androidx.work.impl.model.WorkSpec} id
*/
- public void stopTimer(@NonNull final WorkGenerationalId id) {
+ public void stopTimer(final @NonNull WorkGenerationalId id) {
synchronized (mLock) {
WorkTimerRunnable removed = mTimerMap.remove(id);
if (removed != null) {
@@ -92,16 +93,14 @@
}
@VisibleForTesting
- @NonNull
- public Map<WorkGenerationalId, WorkTimerRunnable> getTimerMap() {
+ public @NonNull Map<WorkGenerationalId, WorkTimerRunnable> getTimerMap() {
synchronized (mLock) {
return mTimerMap;
}
}
@VisibleForTesting
- @NonNull
- public Map<WorkGenerationalId, TimeLimitExceededListener> getListeners() {
+ public @NonNull Map<WorkGenerationalId, TimeLimitExceededListener> getListeners() {
synchronized (mLock) {
return mListeners;
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/futures/AbstractFuture.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/futures/AbstractFuture.java
index 5a8e7d5..49e1763 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/futures/AbstractFuture.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/futures/AbstractFuture.java
@@ -18,12 +18,13 @@
import static java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.Locale;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
@@ -122,10 +123,8 @@
private static final class Waiter {
static final Waiter TOMBSTONE = new Waiter(false /* ignored param */);
- @Nullable
- volatile Thread thread;
- @Nullable
- volatile Waiter next;
+ volatile @Nullable Thread thread;
+ volatile @Nullable Waiter next;
/**
* Constructor for the TOMBSTONE, avoids use of ATOMIC_HELPER in case this class is loaded
@@ -205,8 +204,7 @@
final Executor executor;
// writes to next are made visible by subsequent CAS's on the listeners field
- @Nullable
- Listener next;
+ @Nullable Listener next;
Listener(Runnable task, Executor executor) {
this.task = task;
@@ -251,8 +249,7 @@
}
final boolean wasInterrupted;
- @Nullable
- final Throwable cause;
+ final @Nullable Throwable cause;
Cancellation(boolean wasInterrupted, @Nullable Throwable cause) {
this.wasInterrupted = wasInterrupted;
@@ -300,19 +297,16 @@
* argument.
* </ul>
*/
- @Nullable
@SuppressWarnings("WeakerAccess") // Avoiding synthetic accessor.
- volatile Object value;
+ volatile @Nullable Object value;
/** All listeners. */
- @Nullable
@SuppressWarnings("WeakerAccess") // Avoiding synthetic accessor.
- volatile Listener listeners;
+ volatile @Nullable Listener listeners;
/** All waiting threads. */
- @Nullable
@SuppressWarnings("WeakerAccess") // Avoiding synthetic accessor.
- volatile Waiter waiters;
+ volatile @Nullable Waiter waiters;
/** Constructor for use by subclasses. */
protected AbstractFuture() {
@@ -1002,8 +996,7 @@
* @return null if an explanation cannot be provided because the future is done.
* @since 23.0
*/
- @Nullable
- protected String pendingToString() {
+ protected @Nullable String pendingToString() {
Object localValue = value;
if (localValue instanceof SetFuture) {
return "setFuture=[" + userObjectToString(((SetFuture) localValue).future) + "]";
@@ -1186,8 +1179,7 @@
}
@SuppressWarnings("WeakerAccess") // Avoiding synthetic accessor.
- @NonNull
- static <T> T checkNotNull(@Nullable T reference) {
+ static <T> @NonNull T checkNotNull(@Nullable T reference) {
if (reference == null) {
throw new NullPointerException();
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/futures/SettableFuture.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/futures/SettableFuture.java
index 2043ef1..ef94345 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/futures/SettableFuture.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/futures/SettableFuture.java
@@ -16,11 +16,12 @@
package androidx.work.impl.utils.futures;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.Nullable;
+
/**
* Cloned from concurrent-futures package to avoid AndroidX namespace issues since there is no
* supportlib 28.* equivalent of this class.
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java
index e61932a..60a71dc 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java
@@ -16,15 +16,16 @@
package androidx.work.impl.utils.taskexecutor;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.Configuration;
-import java.util.concurrent.Executor;
-
import kotlinx.coroutines.CoroutineDispatcher;
import kotlinx.coroutines.ExecutorsKt;
+import org.jspecify.annotations.NonNull;
+
+import java.util.concurrent.Executor;
+
/**
* Interface for executing common tasks in WorkManager.
*
@@ -35,8 +36,7 @@
/**
* @return The {@link Executor} for main thread task processing
*/
- @NonNull
- Executor getMainThreadExecutor();
+ @NonNull Executor getMainThreadExecutor();
/**
* @param runnable {@link Runnable} to execute on a thread pool used
@@ -53,11 +53,9 @@
*
* @return The {@link Executor} for internal book-keeping
*/
- @NonNull
- SerialExecutor getSerialTaskExecutor();
+ @NonNull SerialExecutor getSerialTaskExecutor();
- @NonNull
- default CoroutineDispatcher getTaskCoroutineDispatcher() {
+ default @NonNull CoroutineDispatcher getTaskCoroutineDispatcher() {
return ExecutorsKt.from(getSerialTaskExecutor());
}
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/WorkManagerTaskExecutor.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/WorkManagerTaskExecutor.java
index 9c4f92c..1866e90 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/WorkManagerTaskExecutor.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/WorkManagerTaskExecutor.java
@@ -19,15 +19,16 @@
import android.os.Handler;
import android.os.Looper;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.impl.utils.SerialExecutorImpl;
-import java.util.concurrent.Executor;
-
import kotlinx.coroutines.CoroutineDispatcher;
import kotlinx.coroutines.ExecutorsKt;
+import org.jspecify.annotations.NonNull;
+
+import java.util.concurrent.Executor;
+
/**
* Default Task Executor for executing common tasks in WorkManager
*/
@@ -54,20 +55,17 @@
};
@Override
- @NonNull
- public Executor getMainThreadExecutor() {
+ public @NonNull Executor getMainThreadExecutor() {
return mMainThreadExecutor;
}
@Override
- @NonNull
- public SerialExecutorImpl getSerialTaskExecutor() {
+ public @NonNull SerialExecutorImpl getSerialTaskExecutor() {
return mBackgroundExecutor;
}
- @NonNull
@Override
- public CoroutineDispatcher getTaskCoroutineDispatcher() {
+ public @NonNull CoroutineDispatcher getTaskCoroutineDispatcher() {
return mTaskDispatcher;
}
}
diff --git a/work/work-runtime/src/main/java/androidx/work/multiprocess/RemoteWorkContinuation.java b/work/work-runtime/src/main/java/androidx/work/multiprocess/RemoteWorkContinuation.java
index 8df1b19..a17fdc9 100644
--- a/work/work-runtime/src/main/java/androidx/work/multiprocess/RemoteWorkContinuation.java
+++ b/work/work-runtime/src/main/java/androidx/work/multiprocess/RemoteWorkContinuation.java
@@ -16,12 +16,13 @@
package androidx.work.multiprocess;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.OneTimeWorkRequest;
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+
import java.util.Collections;
import java.util.List;
@@ -58,8 +59,7 @@
* @return A {@link RemoteWorkContinuation} that allows for further chaining of dependent
* {@link OneTimeWorkRequest}s
*/
- @NonNull
- public abstract RemoteWorkContinuation then(@NonNull List<OneTimeWorkRequest> work);
+ public abstract @NonNull RemoteWorkContinuation then(@NonNull List<OneTimeWorkRequest> work);
/**
* Enqueues the instance of {@link RemoteWorkContinuation} on the background thread.
@@ -67,8 +67,7 @@
* @return An {@link ListenableFuture} that can be used to determine when the enqueue
* has completed
*/
- @NonNull
- public abstract ListenableFuture<Void> enqueue();
+ public abstract @NonNull ListenableFuture<Void> enqueue();
/**
* Combines multiple {@link RemoteWorkContinuation}s as prerequisites for a new
@@ -78,8 +77,7 @@
* prerequisites for the return value
* @return A {@link RemoteWorkContinuation} that allows further chaining
*/
- @NonNull
- public static RemoteWorkContinuation combine(
+ public static @NonNull RemoteWorkContinuation combine(
@NonNull List<RemoteWorkContinuation> continuations) {
return continuations.get(0).combineInternal(continuations);
@@ -88,7 +86,6 @@
/**
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- protected abstract RemoteWorkContinuation combineInternal(
+ protected abstract @NonNull RemoteWorkContinuation combineInternal(
@NonNull List<RemoteWorkContinuation> continuations);
}
diff --git a/work/work-runtime/src/main/java/androidx/work/multiprocess/RemoteWorkManager.java b/work/work-runtime/src/main/java/androidx/work/multiprocess/RemoteWorkManager.java
index 9d5e698..e48c9bd 100644
--- a/work/work-runtime/src/main/java/androidx/work/multiprocess/RemoteWorkManager.java
+++ b/work/work-runtime/src/main/java/androidx/work/multiprocess/RemoteWorkManager.java
@@ -18,7 +18,6 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.Data;
import androidx.work.ExistingPeriodicWorkPolicy;
@@ -36,6 +35,8 @@
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+
import java.util.Collections;
import java.util.List;
import java.util.UUID;
@@ -59,8 +60,7 @@
* @return A {@link ListenableFuture} that can be used to determine when the enqueue has
* completed
*/
- @NonNull
- public abstract ListenableFuture<Void> enqueue(@NonNull WorkRequest request);
+ public abstract @NonNull ListenableFuture<Void> enqueue(@NonNull WorkRequest request);
/**
* Enqueues one or more items for background processing.
@@ -69,8 +69,7 @@
* @return A {@link ListenableFuture} that can be used to determine when the enqueue has
* completed
*/
- @NonNull
- public abstract ListenableFuture<Void> enqueue(@NonNull List<WorkRequest> requests);
+ public abstract @NonNull ListenableFuture<Void> enqueue(@NonNull List<WorkRequest> requests);
/**
* This method allows you to enqueue {@code work} requests to a uniquely-named
@@ -91,8 +90,7 @@
* @return A {@link ListenableFuture} that can be used to determine when the enqueue has
* completed
*/
- @NonNull
- public final ListenableFuture<Void> enqueueUniqueWork(
+ public final @NonNull ListenableFuture<Void> enqueueUniqueWork(
@NonNull String uniqueWorkName,
@NonNull ExistingWorkPolicy existingWorkPolicy,
@NonNull OneTimeWorkRequest work) {
@@ -121,8 +119,7 @@
* @return A {@link ListenableFuture} that can be used to determine when the enqueue has
* completed
*/
- @NonNull
- public abstract ListenableFuture<Void> enqueueUniqueWork(
+ public abstract @NonNull ListenableFuture<Void> enqueueUniqueWork(
@NonNull String uniqueWorkName,
@NonNull ExistingWorkPolicy existingWorkPolicy,
@NonNull List<OneTimeWorkRequest> work);
@@ -145,8 +142,7 @@
* @return An {@link ListenableFuture} that can be used to determine when the enqueue has
* completed
*/
- @NonNull
- public abstract ListenableFuture<Void> enqueueUniquePeriodicWork(
+ public abstract @NonNull ListenableFuture<Void> enqueueUniquePeriodicWork(
@NonNull String uniqueWorkName,
@NonNull ExistingPeriodicWorkPolicy existingPeriodicWorkPolicy,
@NonNull PeriodicWorkRequest periodicWork);
@@ -162,8 +158,7 @@
* @return A {@link RemoteWorkContinuation} that allows for further chaining of dependent
* {@link OneTimeWorkRequest}
*/
- @NonNull
- public final RemoteWorkContinuation beginWith(@NonNull OneTimeWorkRequest work) {
+ public final @NonNull RemoteWorkContinuation beginWith(@NonNull OneTimeWorkRequest work) {
return beginWith(Collections.singletonList(work));
}
@@ -178,8 +173,8 @@
* @return A {@link RemoteWorkContinuation} that allows for further chaining of dependent
* {@link OneTimeWorkRequest}
*/
- @NonNull
- public abstract RemoteWorkContinuation beginWith(@NonNull List<OneTimeWorkRequest> work);
+ public abstract @NonNull RemoteWorkContinuation beginWith(
+ @NonNull List<OneTimeWorkRequest> work);
/**
* This method allows you to begin unique chains of work for situations where you only want one
@@ -208,8 +203,7 @@
* leaf nodes labelled with {@code uniqueWorkName}.
* @return A {@link RemoteWorkContinuation} that allows further chaining
*/
- @NonNull
- public final RemoteWorkContinuation beginUniqueWork(
+ public final @NonNull RemoteWorkContinuation beginUniqueWork(
@NonNull String uniqueWorkName,
@NonNull ExistingWorkPolicy existingWorkPolicy,
@NonNull OneTimeWorkRequest work) {
@@ -243,8 +237,7 @@
* as a child of all leaf nodes labelled with {@code uniqueWorkName}.
* @return A {@link RemoteWorkContinuation} that allows further chaining
*/
- @NonNull
- public abstract RemoteWorkContinuation beginUniqueWork(
+ public abstract @NonNull RemoteWorkContinuation beginUniqueWork(
@NonNull String uniqueWorkName,
@NonNull ExistingWorkPolicy existingWorkPolicy,
@NonNull List<OneTimeWorkRequest> work);
@@ -255,9 +248,8 @@
* @return A {@link ListenableFuture} that can be used to determine when the enqueue has
* completed
*/
- @NonNull
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public abstract ListenableFuture<Void> enqueue(@NonNull WorkContinuation continuation);
+ public abstract @NonNull ListenableFuture<Void> enqueue(@NonNull WorkContinuation continuation);
/**
* Cancels work with the given id if it isn't finished. Note that cancellation is a best-effort
@@ -268,8 +260,7 @@
* @return A {@link ListenableFuture} that can be used to determine when the cancelWorkById has
* completed
*/
- @NonNull
- public abstract ListenableFuture<Void> cancelWorkById(@NonNull UUID id);
+ public abstract @NonNull ListenableFuture<Void> cancelWorkById(@NonNull UUID id);
/**
* Cancels all unfinished work with the given tag. Note that cancellation is a best-effort
@@ -280,8 +271,7 @@
* @return An {@link ListenableFuture} that can be used to determine when the
* cancelAllWorkByTag has completed
*/
- @NonNull
- public abstract ListenableFuture<Void> cancelAllWorkByTag(@NonNull String tag);
+ public abstract @NonNull ListenableFuture<Void> cancelAllWorkByTag(@NonNull String tag);
/**
* Cancels all unfinished work in the work chain with the given name. Note that cancellation is
@@ -292,8 +282,8 @@
* @return A {@link ListenableFuture} that can be used to determine when the cancelUniqueWork
* has completed
*/
- @NonNull
- public abstract ListenableFuture<Void> cancelUniqueWork(@NonNull String uniqueWorkName);
+ public abstract @NonNull ListenableFuture<Void> cancelUniqueWork(
+ @NonNull String uniqueWorkName);
/**
* Cancels all unfinished work. <b>Use this method with extreme caution!</b> By invoking it,
@@ -306,8 +296,7 @@
* @return A {@link ListenableFuture} that can be used to determine when the cancelAllWork has
* completed
*/
- @NonNull
- public abstract ListenableFuture<Void> cancelAllWork();
+ public abstract @NonNull ListenableFuture<Void> cancelAllWork();
/**
* Gets the {@link ListenableFuture} of the {@link List} of {@link WorkInfo} for all work
@@ -317,8 +306,8 @@
* @return A {@link ListenableFuture} of the {@link List} of {@link WorkInfo} for work
* referenced by this {@link WorkQuery}.
*/
- @NonNull
- public abstract ListenableFuture<List<WorkInfo>> getWorkInfos(@NonNull WorkQuery workQuery);
+ public abstract @NonNull ListenableFuture<List<WorkInfo>> getWorkInfos(
+ @NonNull WorkQuery workQuery);
/**
* Updates progress information for a {@link ListenableWorker}.
@@ -328,9 +317,9 @@
* @return A {@link ListenableFuture} that can be used to determine when the setProgress
* has completed.
*/
- @NonNull
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public abstract ListenableFuture<Void> setProgress(@NonNull UUID id, @NonNull Data data);
+ public abstract @NonNull ListenableFuture<Void> setProgress(@NonNull UUID id,
+ @NonNull Data data);
/**
* Delegates the call to {@link ListenableWorker#setForegroundAsync(ForegroundInfo)} to the
@@ -341,9 +330,8 @@
* @return A {@link ListenableFuture} that can be used to determine when the setForeground
* has completed.
*/
- @NonNull
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public abstract ListenableFuture<Void> setForegroundAsync(
+ public abstract @NonNull ListenableFuture<Void> setForegroundAsync(
@NonNull String id,
@NonNull ForegroundInfo foregroundInfo);
@@ -354,8 +342,7 @@
* @param context The application context.
* @return The instance of {@link RemoteWorkManager}.
*/
- @NonNull
- public static RemoteWorkManager getInstance(@NonNull Context context) {
+ public static @NonNull RemoteWorkManager getInstance(@NonNull Context context) {
WorkManagerImpl workManager = WorkManagerImpl.getInstance(context);
RemoteWorkManager remoteWorkManager = workManager.getRemoteWorkManager();
if (remoteWorkManager == null) {
diff --git a/work/work-runtime/src/test/java/androidx/work/DataTest.java b/work/work-runtime/src/test/java/androidx/work/DataTest.java
index ff8736d..5322c17 100644
--- a/work/work-runtime/src/test/java/androidx/work/DataTest.java
+++ b/work/work-runtime/src/test/java/androidx/work/DataTest.java
@@ -23,8 +23,7 @@
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
-import androidx.annotation.NonNull;
-
+import org.jspecify.annotations.NonNull;
import org.junit.Test;
import java.util.HashMap;
@@ -239,8 +238,7 @@
assertThat(caughtIllegalArgumentException, is(true));
}
- @NonNull
- private Data createData() {
+ private @NonNull Data createData() {
Map<String, Object> map = new HashMap<>();
map.put("boolean", true);
map.put("byte", (byte) 1);
diff --git a/work/work-rxjava2/build.gradle b/work/work-rxjava2/build.gradle
index 2753a93..bfe3012 100644
--- a/work/work-rxjava2/build.gradle
+++ b/work/work-rxjava2/build.gradle
@@ -30,6 +30,7 @@
}
dependencies {
+ api(libs.jspecify)
api(project(":work:work-runtime"))
api(libs.rxjava2)
implementation("androidx.concurrent:concurrent-futures:1.1.0")
@@ -46,8 +47,6 @@
description = "Android WorkManager RxJava2 interoperatibility library"
failOnDeprecationWarnings = false
legacyDisableKotlinStrictApiMode = true
- // TODO: b/326456246
- optOutJSpecify = true
}
android {
diff --git a/work/work-rxjava2/src/main/java/androidx/work/RxWorker.java b/work/work-rxjava2/src/main/java/androidx/work/RxWorker.java
index 7c44317..3d3500a 100644
--- a/work/work-rxjava2/src/main/java/androidx/work/RxWorker.java
+++ b/work/work-rxjava2/src/main/java/androidx/work/RxWorker.java
@@ -21,7 +21,6 @@
import android.content.Context;
import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
import androidx.work.impl.utils.SynchronousExecutor;
import com.google.common.util.concurrent.ListenableFuture;
@@ -33,6 +32,8 @@
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
+import org.jspecify.annotations.NonNull;
+
import java.util.concurrent.Executor;
/**
@@ -63,9 +64,8 @@
super(appContext, workerParams);
}
- @NonNull
@Override
- public ListenableFuture<Result> startWork() {
+ public @NonNull ListenableFuture<Result> startWork() {
return convert(createWork());
}
@@ -120,9 +120,8 @@
* <p>
* Use {@link #setCompletableProgress(Data)} instead.
*/
- @NonNull
@Deprecated
- public final Single<Void> setProgress(@NonNull Data data) {
+ public final @NonNull Single<Void> setProgress(@NonNull Data data) {
return Single.fromFuture(setProgressAsync(data));
}
@@ -133,14 +132,12 @@
* @param data The progress {@link Data}
* @return The {@link Completable}
*/
- @NonNull
- public final Completable setCompletableProgress(@NonNull Data data) {
+ public final @NonNull Completable setCompletableProgress(@NonNull Data data) {
return Completable.fromFuture(setProgressAsync(data));
}
- @NonNull
@Override
- public ListenableFuture<ForegroundInfo> getForegroundInfoAsync() {
+ public @NonNull ListenableFuture<ForegroundInfo> getForegroundInfoAsync() {
return convert(getForegroundInfo());
}
@@ -161,8 +158,7 @@
* is marked immediate. For more information look at
* {@link WorkRequest.Builder#setExpedited(OutOfQuotaPolicy)}.
*/
- @NonNull
- public Single<ForegroundInfo> getForegroundInfo() {
+ public @NonNull Single<ForegroundInfo> getForegroundInfo() {
String message =
"Expedited WorkRequests require a RxWorker to provide an implementation for"
+ " `getForegroundInfo()`";
@@ -191,8 +187,7 @@
* @return A {@link Completable} which resolves after the {@link RxWorker}
* transitions to running in the context of a foreground {@link android.app.Service}.
*/
- @NonNull
- public final Completable setForeground(@NonNull ForegroundInfo foregroundInfo) {
+ public final @NonNull Completable setForeground(@NonNull ForegroundInfo foregroundInfo) {
return Completable.fromFuture(setForegroundAsync(foregroundInfo));
}
diff --git a/work/work-rxjava3/build.gradle b/work/work-rxjava3/build.gradle
index e592631..dec427d 100644
--- a/work/work-rxjava3/build.gradle
+++ b/work/work-rxjava3/build.gradle
@@ -30,6 +30,7 @@
}
dependencies {
+ api(libs.jspecify)
api(project(":work:work-runtime"))
api(libs.rxjava3)
implementation("androidx.concurrent:concurrent-futures:1.1.0")
@@ -45,8 +46,6 @@
inceptionYear = "2020"
description = "Android WorkManager RxJava3 interoperatibility library"
legacyDisableKotlinStrictApiMode = true
- // TODO: b/326456246
- optOutJSpecify = true
}
android {
diff --git a/work/work-rxjava3/src/main/java/androidx/work/rxjava3/RxWorker.java b/work/work-rxjava3/src/main/java/androidx/work/rxjava3/RxWorker.java
index c5e8574..b631459 100644
--- a/work/work-rxjava3/src/main/java/androidx/work/rxjava3/RxWorker.java
+++ b/work/work-rxjava3/src/main/java/androidx/work/rxjava3/RxWorker.java
@@ -21,7 +21,6 @@
import android.content.Context;
import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
import androidx.work.Configuration;
import androidx.work.Data;
import androidx.work.ForegroundInfo;
@@ -42,6 +41,8 @@
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
+import org.jspecify.annotations.NonNull;
+
import java.util.concurrent.Executor;
/**
@@ -72,9 +73,8 @@
super(appContext, workerParams);
}
- @NonNull
@Override
- public final ListenableFuture<Result> startWork() {
+ public final @NonNull ListenableFuture<Result> startWork() {
return convert(createWork());
}
@@ -123,14 +123,12 @@
* @param data The progress {@link Data}
* @return The {@link Completable}
*/
- @NonNull
- public final Completable setCompletableProgress(@NonNull Data data) {
+ public final @NonNull Completable setCompletableProgress(@NonNull Data data) {
return Completable.fromFuture(setProgressAsync(data));
}
- @NonNull
@Override
- public ListenableFuture<ForegroundInfo> getForegroundInfoAsync() {
+ public @NonNull ListenableFuture<ForegroundInfo> getForegroundInfoAsync() {
return convert(getForegroundInfo());
}
@@ -151,8 +149,7 @@
* is marked immediate. For more information look at
* {@link WorkRequest.Builder#setExpedited(OutOfQuotaPolicy)}.
*/
- @NonNull
- public Single<ForegroundInfo> getForegroundInfo() {
+ public @NonNull Single<ForegroundInfo> getForegroundInfo() {
String message =
"Expedited WorkRequests require a RxWorker to provide an implementation for"
+ " `getForegroundInfo()`";
@@ -181,8 +178,7 @@
* @return A {@link Completable} which resolves after the {@link RxWorker}
* transitions to running in the context of a foreground {@link android.app.Service}.
*/
- @NonNull
- public final Completable setForeground(@NonNull ForegroundInfo foregroundInfo) {
+ public final @NonNull Completable setForeground(@NonNull ForegroundInfo foregroundInfo) {
return Completable.fromFuture(setForegroundAsync(foregroundInfo));
}
diff --git a/work/work-testing/build.gradle b/work/work-testing/build.gradle
index 446e53b..7e946d1 100644
--- a/work/work-testing/build.gradle
+++ b/work/work-testing/build.gradle
@@ -30,6 +30,7 @@
}
dependencies {
+ api(libs.jspecify)
api(project(":work:work-runtime"))
implementation("androidx.lifecycle:lifecycle-livedata-core:2.6.2")
@@ -59,8 +60,6 @@
inceptionYear = "2018"
description = "Android WorkManager testing library"
legacyDisableKotlinStrictApiMode = true
- // TODO: b/326456246
- optOutJSpecify = true
}
android {
diff --git a/work/work-testing/src/androidTest/java/androidx/work/testing/workers/CountingTestWorker.java b/work/work-testing/src/androidTest/java/androidx/work/testing/workers/CountingTestWorker.java
index b07246d..d274251 100644
--- a/work/work-testing/src/androidTest/java/androidx/work/testing/workers/CountingTestWorker.java
+++ b/work/work-testing/src/androidTest/java/androidx/work/testing/workers/CountingTestWorker.java
@@ -18,10 +18,11 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -35,9 +36,8 @@
super(context, workerParams);
}
- @NonNull
@Override
- public Result doWork() {
+ public @NonNull Result doWork() {
COUNT.incrementAndGet();
return Result.success();
}
diff --git a/work/work-testing/src/androidTest/java/androidx/work/testing/workers/TestListenableWorker.java b/work/work-testing/src/androidTest/java/androidx/work/testing/workers/TestListenableWorker.java
index c47d444..5ef48c3 100644
--- a/work/work-testing/src/androidTest/java/androidx/work/testing/workers/TestListenableWorker.java
+++ b/work/work-testing/src/androidTest/java/androidx/work/testing/workers/TestListenableWorker.java
@@ -18,13 +18,14 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.work.ListenableWorker;
import androidx.work.WorkerParameters;
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+
public class TestListenableWorker extends ListenableWorker {
public TestListenableWorker(
@NonNull Context context,
@@ -32,9 +33,8 @@
super(context, workerParameters);
}
- @NonNull
@Override
- public ListenableFuture<Result> startWork() {
+ public @NonNull ListenableFuture<Result> startWork() {
return CallbackToFutureAdapter.getFuture(completer -> {
completer.set(Result.success());
return "successfully completed future";
diff --git a/work/work-testing/src/androidTest/java/androidx/work/testing/workers/TestWorker.java b/work/work-testing/src/androidTest/java/androidx/work/testing/workers/TestWorker.java
index 2423096..6f56ac0 100644
--- a/work/work-testing/src/androidTest/java/androidx/work/testing/workers/TestWorker.java
+++ b/work/work-testing/src/androidTest/java/androidx/work/testing/workers/TestWorker.java
@@ -19,11 +19,12 @@
import android.content.Context;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.work.Logger;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import org.jspecify.annotations.NonNull;
+
/**
* A test {@link Worker} that prints a log and returns a successful result.
*/
diff --git a/work/work-testing/src/main/java/androidx/work/testing/InstantWorkTaskExecutor.java b/work/work-testing/src/main/java/androidx/work/testing/InstantWorkTaskExecutor.java
index 27f7548..d44f819 100644
--- a/work/work-testing/src/main/java/androidx/work/testing/InstantWorkTaskExecutor.java
+++ b/work/work-testing/src/main/java/androidx/work/testing/InstantWorkTaskExecutor.java
@@ -16,12 +16,13 @@
package androidx.work.testing;
-import androidx.annotation.NonNull;
import androidx.work.impl.utils.SerialExecutorImpl;
import androidx.work.impl.utils.SynchronousExecutor;
import androidx.work.impl.utils.taskexecutor.SerialExecutor;
import androidx.work.impl.utils.taskexecutor.TaskExecutor;
+import org.jspecify.annotations.NonNull;
+
import java.util.concurrent.Executor;
/**
@@ -32,20 +33,17 @@
private Executor mSynchronousExecutor = new SynchronousExecutor();
private SerialExecutorImpl mSerialExecutor = new SerialExecutorImpl(mSynchronousExecutor);
- @NonNull
- Executor getSynchronousExecutor() {
+ @NonNull Executor getSynchronousExecutor() {
return mSynchronousExecutor;
}
- @NonNull
@Override
- public Executor getMainThreadExecutor() {
+ public @NonNull Executor getMainThreadExecutor() {
return mSynchronousExecutor;
}
- @NonNull
@Override
- public SerialExecutor getSerialTaskExecutor() {
+ public @NonNull SerialExecutor getSerialTaskExecutor() {
return mSerialExecutor;
}
}
diff --git a/work/work-testing/src/main/java/androidx/work/testing/SynchronousExecutor.java b/work/work-testing/src/main/java/androidx/work/testing/SynchronousExecutor.java
index 4d6612c..1718c13 100644
--- a/work/work-testing/src/main/java/androidx/work/testing/SynchronousExecutor.java
+++ b/work/work-testing/src/main/java/androidx/work/testing/SynchronousExecutor.java
@@ -16,7 +16,7 @@
package androidx.work.testing;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
import java.util.concurrent.Executor;
diff --git a/work/work-testing/src/main/java/androidx/work/testing/TestDriver.java b/work/work-testing/src/main/java/androidx/work/testing/TestDriver.java
index 7295f11c..b395fdb 100644
--- a/work/work-testing/src/main/java/androidx/work/testing/TestDriver.java
+++ b/work/work-testing/src/main/java/androidx/work/testing/TestDriver.java
@@ -16,7 +16,7 @@
package androidx.work.testing;
-import androidx.annotation.NonNull;
+import org.jspecify.annotations.NonNull;
import java.util.UUID;
diff --git a/work/work-testing/src/main/java/androidx/work/testing/TestForegroundUpdater.java b/work/work-testing/src/main/java/androidx/work/testing/TestForegroundUpdater.java
index 2835e98..79b0c26 100644
--- a/work/work-testing/src/main/java/androidx/work/testing/TestForegroundUpdater.java
+++ b/work/work-testing/src/main/java/androidx/work/testing/TestForegroundUpdater.java
@@ -20,7 +20,6 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.ForegroundInfo;
import androidx.work.ForegroundUpdater;
@@ -28,6 +27,8 @@
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+
import java.util.UUID;
/**
@@ -38,9 +39,8 @@
private static final String TAG = Logger.tagWithPrefix("TestForegroundUpdater");
- @NonNull
@Override
- public ListenableFuture<Void> setForegroundAsync(
+ public @NonNull ListenableFuture<Void> setForegroundAsync(
@NonNull Context context,
@NonNull UUID id,
@NonNull ForegroundInfo foregroundInfo) {
diff --git a/work/work-testing/src/main/java/androidx/work/testing/TestListenableWorkerBuilder.java b/work/work-testing/src/main/java/androidx/work/testing/TestListenableWorkerBuilder.java
index c06abc9..e9c235d 100644
--- a/work/work-testing/src/main/java/androidx/work/testing/TestListenableWorkerBuilder.java
+++ b/work/work-testing/src/main/java/androidx/work/testing/TestListenableWorkerBuilder.java
@@ -22,7 +22,6 @@
import android.net.Uri;
import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.work.Data;
@@ -36,14 +35,16 @@
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.utils.taskexecutor.TaskExecutor;
+import kotlinx.coroutines.Dispatchers;
+
+import org.jspecify.annotations.NonNull;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executor;
-import kotlinx.coroutines.Dispatchers;
-
/**
* Builds instances of {@link androidx.work.ListenableWorker} which can be used for testing.
*
@@ -86,8 +87,7 @@
/**
* @return The application {@link Context}.
*/
- @NonNull
- Context getApplicationContext() {
+ @NonNull Context getApplicationContext() {
return mContext;
}
@@ -101,8 +101,7 @@
/**
* @return The {@link String} name of the unit of work.
*/
- @NonNull
- String getWorkerName() {
+ @NonNull String getWorkerName() {
return mWorkerName;
}
@@ -110,8 +109,7 @@
* @return The {@link androidx.work.WorkerParameters.RuntimeExtras} associated with this unit
* of work.
*/
- @NonNull
- WorkerParameters.RuntimeExtras getRuntimeExtras() {
+ WorkerParameters.@NonNull RuntimeExtras getRuntimeExtras() {
return mRuntimeExtras;
}
@@ -119,34 +117,30 @@
* @return The {@link TaskExecutor} associated with this unit of work.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- TaskExecutor getTaskExecutor() {
+ @NonNull TaskExecutor getTaskExecutor() {
return mTaskExecutor;
}
/**
* @return The {@link TaskExecutor} associated with this unit of work.
*/
- @NonNull
- Executor getExecutor() {
+ @NonNull Executor getExecutor() {
return mExecutor;
}
/**
* @return The {@link ProgressUpdater} associated with this unit of work.
*/
- @NonNull
@SuppressWarnings({"KotlinPropertyAccess", "WeakerAccess"})
- ProgressUpdater getProgressUpdater() {
+ @NonNull ProgressUpdater getProgressUpdater() {
return mProgressUpdater;
}
/**
* @return The {@link ForegroundUpdater} associated with this unit of work.
*/
- @NonNull
@SuppressLint("KotlinPropertyAccess")
- ForegroundUpdater getForegroundUpdater() {
+ @NonNull ForegroundUpdater getForegroundUpdater() {
return mForegroundUpdater;
}
@@ -156,8 +150,7 @@
* @param id The {@link UUID}
* @return The current {@link TestListenableWorkerBuilder}
*/
- @NonNull
- public TestListenableWorkerBuilder<W> setId(@NonNull UUID id) {
+ public @NonNull TestListenableWorkerBuilder<W> setId(@NonNull UUID id) {
mId = id;
return this;
}
@@ -168,8 +161,7 @@
* @param inputData key/value pairs that will be provided to the worker
* @return The current {@link TestListenableWorkerBuilder}
*/
- @NonNull
- public TestListenableWorkerBuilder<W> setInputData(@NonNull Data inputData) {
+ public @NonNull TestListenableWorkerBuilder<W> setInputData(@NonNull Data inputData) {
mInputData = inputData;
return this;
}
@@ -180,8 +172,7 @@
* @param tags The {@link List} of tags to be used
* @return The current {@link TestListenableWorkerBuilder}
*/
- @NonNull
- public TestListenableWorkerBuilder<W> setTags(@NonNull List<String> tags) {
+ public @NonNull TestListenableWorkerBuilder<W> setTags(@NonNull List<String> tags) {
mTags = tags;
return this;
}
@@ -192,8 +183,7 @@
* @param runAttemptCount The initial run attempt count
* @return The current {@link TestListenableWorkerBuilder}
*/
- @NonNull
- public TestListenableWorkerBuilder<W> setRunAttemptCount(int runAttemptCount) {
+ public @NonNull TestListenableWorkerBuilder<W> setRunAttemptCount(int runAttemptCount) {
mRunAttemptCount = runAttemptCount;
return this;
}
@@ -205,8 +195,8 @@
* @return The current {@link TestListenableWorkerBuilder}
*/
@RequiresApi(24)
- @NonNull
- public TestListenableWorkerBuilder<W> setTriggeredContentUris(@NonNull List<Uri> contentUris) {
+ public @NonNull TestListenableWorkerBuilder<W> setTriggeredContentUris(
+ @NonNull List<Uri> contentUris) {
mRuntimeExtras.triggeredContentUris = contentUris;
return this;
}
@@ -218,8 +208,7 @@
* @return The current {@link TestListenableWorkerBuilder}
*/
@RequiresApi(24)
- @NonNull
- public TestListenableWorkerBuilder<W> setTriggeredContentAuthorities(
+ public @NonNull TestListenableWorkerBuilder<W> setTriggeredContentAuthorities(
@NonNull List<String> authorities) {
mRuntimeExtras.triggeredContentAuthorities = authorities;
return this;
@@ -232,8 +221,7 @@
* @return The current {@link TestListenableWorkerBuilder}
*/
@RequiresApi(28)
- @NonNull
- public TestListenableWorkerBuilder<W> setNetwork(@NonNull Network network) {
+ public @NonNull TestListenableWorkerBuilder<W> setNetwork(@NonNull Network network) {
mRuntimeExtras.network = network;
return this;
}
@@ -246,8 +234,8 @@
* {@link androidx.work.ListenableWorker}
* @return The current {@link TestListenableWorkerBuilder}
*/
- @NonNull
- public TestListenableWorkerBuilder<W> setWorkerFactory(@NonNull WorkerFactory workerFactory) {
+ public @NonNull TestListenableWorkerBuilder<W> setWorkerFactory(
+ @NonNull WorkerFactory workerFactory) {
mWorkerFactory = workerFactory;
return this;
}
@@ -259,8 +247,8 @@
* @param updater The {@link ProgressUpdater} which can handle progress updates.
* @return The current {@link TestListenableWorkerBuilder}
*/
- @NonNull
- public TestListenableWorkerBuilder<W> setProgressUpdater(@NonNull ProgressUpdater updater) {
+ public @NonNull TestListenableWorkerBuilder<W> setProgressUpdater(
+ @NonNull ProgressUpdater updater) {
mProgressUpdater = updater;
return this;
}
@@ -272,8 +260,7 @@
* @param updater The {@link ForegroundUpdater} which can handle notification updates.
* @return The current {@link TestListenableWorkerBuilder}
*/
- @NonNull
- public TestListenableWorkerBuilder<W> setForegroundUpdater(
+ public @NonNull TestListenableWorkerBuilder<W> setForegroundUpdater(
@NonNull ForegroundUpdater updater) {
mForegroundUpdater = updater;
return this;
@@ -285,8 +272,7 @@
* @param executor The {@link Executor}
* @return The current {@link TestListenableWorkerBuilder}
*/
- @NonNull
- TestListenableWorkerBuilder<W> setExecutor(@NonNull Executor executor) {
+ @NonNull TestListenableWorkerBuilder<W> setExecutor(@NonNull Executor executor) {
mExecutor = executor;
return this;
}
@@ -299,8 +285,7 @@
* @param generation generation
* @return The current {@link TestListenableWorkerBuilder}
*/
- @NonNull
- TestListenableWorkerBuilder<W> setGeneration(@IntRange(from = 0) int generation) {
+ @NonNull TestListenableWorkerBuilder<W> setGeneration(@IntRange(from = 0) int generation) {
mGeneration = generation;
return this;
}
@@ -310,9 +295,8 @@
*
* @return the instance of a {@link ListenableWorker}.
*/
- @NonNull
@SuppressWarnings("unchecked")
- public W build() {
+ public @NonNull W build() {
WorkerParameters parameters =
new WorkerParameters(
mId,
@@ -354,9 +338,8 @@
* @param workRequest The {@link WorkRequest}
* @return The new instance of a {@link ListenableWorker}
*/
- @NonNull
@SuppressWarnings("unchecked")
- public static TestListenableWorkerBuilder<? extends ListenableWorker> from(
+ public static @NonNull TestListenableWorkerBuilder<? extends ListenableWorker> from(
@NonNull Context context,
@NonNull WorkRequest workRequest) {
WorkSpec workSpec = workRequest.getWorkSpec();
@@ -381,8 +364,7 @@
* @param workerClass The subtype of {@link ListenableWorker} being built
* @return The new instance of a {@link ListenableWorker}
*/
- @NonNull
- public static <W extends ListenableWorker> TestListenableWorkerBuilder<W> from(
+ public static <W extends ListenableWorker> @NonNull TestListenableWorkerBuilder<W> from(
@NonNull Context context,
@NonNull Class<W> workerClass) {
return new TestListenableWorkerBuilder<>(context, workerClass);
diff --git a/work/work-testing/src/main/java/androidx/work/testing/TestProgressUpdater.java b/work/work-testing/src/main/java/androidx/work/testing/TestProgressUpdater.java
index 161bfad..ed521af 100644
--- a/work/work-testing/src/main/java/androidx/work/testing/TestProgressUpdater.java
+++ b/work/work-testing/src/main/java/androidx/work/testing/TestProgressUpdater.java
@@ -20,7 +20,6 @@
import android.content.Context;
-import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.Data;
import androidx.work.Logger;
@@ -28,6 +27,8 @@
import com.google.common.util.concurrent.ListenableFuture;
+import org.jspecify.annotations.NonNull;
+
import java.util.UUID;
/**
@@ -37,9 +38,8 @@
public class TestProgressUpdater implements ProgressUpdater {
private static final String TAG = Logger.tagWithPrefix("TestProgressUpdater");
- @NonNull
@Override
- public ListenableFuture<Void> updateProgress(
+ public @NonNull ListenableFuture<Void> updateProgress(
@NonNull Context context,
@NonNull UUID id,
@NonNull Data data) {
diff --git a/work/work-testing/src/main/java/androidx/work/testing/TestWorkerBuilder.java b/work/work-testing/src/main/java/androidx/work/testing/TestWorkerBuilder.java
index cdb50af..75f899b 100644
--- a/work/work-testing/src/main/java/androidx/work/testing/TestWorkerBuilder.java
+++ b/work/work-testing/src/main/java/androidx/work/testing/TestWorkerBuilder.java
@@ -18,12 +18,13 @@
import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.work.WorkRequest;
import androidx.work.Worker;
import androidx.work.impl.model.WorkSpec;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
@@ -54,9 +55,8 @@
* @param executor The {@link Executor}
* @return The new instance of a {@link TestWorkerBuilder}
*/
- @NonNull
@SuppressWarnings("unchecked")
- public static TestWorkerBuilder<? extends Worker> from(
+ public static @NonNull TestWorkerBuilder<? extends Worker> from(
@NonNull Context context,
@NonNull WorkRequest workRequest,
@NonNull Executor executor) {
@@ -91,8 +91,7 @@
* @param executor The {@link Executor}
* @return The new instance of a {@link TestWorkerBuilder}
*/
- @NonNull
- public static <W extends Worker> TestWorkerBuilder<W> from(
+ public static <W extends Worker> @NonNull TestWorkerBuilder<W> from(
@NonNull Context context,
@NonNull Class<W> workerClass,
@NonNull Executor executor) {
diff --git a/work/work-testing/src/main/java/androidx/work/testing/WorkManagerTestInitHelper.java b/work/work-testing/src/main/java/androidx/work/testing/WorkManagerTestInitHelper.java
index 4bd0a08..e44663c 100644
--- a/work/work-testing/src/main/java/androidx/work/testing/WorkManagerTestInitHelper.java
+++ b/work/work-testing/src/main/java/androidx/work/testing/WorkManagerTestInitHelper.java
@@ -21,15 +21,15 @@
import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.work.Configuration;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.utils.SerialExecutorImpl;
import androidx.work.impl.utils.taskexecutor.SerialExecutor;
-import java.util.UUID;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import java.util.UUID;
/**
* Helps initialize {@link androidx.work.WorkManager} for testing.
diff --git a/xr/OWNERS b/xr/OWNERS
new file mode 100644
index 0000000..d8768a3
--- /dev/null
+++ b/xr/OWNERS
@@ -0,0 +1,5 @@
+# Bug component: 1524104
[email protected]
[email protected]
[email protected]
[email protected]
diff --git a/camera/camera-effects-still-portrait/api/current.txt b/xr/arcore/arcore/api/current.txt
similarity index 100%
rename from camera/camera-effects-still-portrait/api/current.txt
rename to xr/arcore/arcore/api/current.txt
diff --git a/camera/camera-effects-still-portrait/api/res-current.txt b/xr/arcore/arcore/api/res-current.txt
similarity index 100%
copy from camera/camera-effects-still-portrait/api/res-current.txt
copy to xr/arcore/arcore/api/res-current.txt
diff --git a/xr/arcore/arcore/api/restricted_current.txt b/xr/arcore/arcore/api/restricted_current.txt
new file mode 100644
index 0000000..fcdf076
--- /dev/null
+++ b/xr/arcore/arcore/api/restricted_current.txt
@@ -0,0 +1,148 @@
+// Signature format: 4.0
+package androidx.xr.arcore {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class Anchor {
+ ctor public Anchor(androidx.xr.runtime.internal.Anchor runtimeAnchor);
+ method public static androidx.xr.arcore.AnchorCreateResult create(androidx.xr.runtime.Session session, androidx.xr.runtime.math.Pose pose);
+ method public void detach();
+ method public static java.util.List<java.util.UUID> getPersistedAnchorUuids(androidx.xr.runtime.Session session);
+ method public androidx.xr.runtime.internal.Anchor getRuntimeAnchor();
+ method public kotlinx.coroutines.flow.StateFlow<androidx.xr.arcore.Anchor.State> getState();
+ method public static androidx.xr.arcore.AnchorCreateResult load(androidx.xr.runtime.Session session, java.util.UUID uuid);
+ method public static androidx.xr.arcore.Anchor loadFromNativePointer(androidx.xr.runtime.Session session, long nativePointer);
+ method public suspend Object? persist(kotlin.coroutines.Continuation<? super java.util.UUID>);
+ method public static void unpersist(androidx.xr.runtime.Session session, java.util.UUID uuid);
+ method public suspend Object? update(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public final androidx.xr.runtime.internal.Anchor runtimeAnchor;
+ property public final kotlinx.coroutines.flow.StateFlow<androidx.xr.arcore.Anchor.State> state;
+ field public static final androidx.xr.arcore.Anchor.Companion Companion;
+ }
+
+ public static final class Anchor.Companion {
+ method public androidx.xr.arcore.AnchorCreateResult create(androidx.xr.runtime.Session session, androidx.xr.runtime.math.Pose pose);
+ method public java.util.List<java.util.UUID> getPersistedAnchorUuids(androidx.xr.runtime.Session session);
+ method public androidx.xr.arcore.AnchorCreateResult load(androidx.xr.runtime.Session session, java.util.UUID uuid);
+ method public androidx.xr.arcore.Anchor loadFromNativePointer(androidx.xr.runtime.Session session, long nativePointer);
+ method public void unpersist(androidx.xr.runtime.Session session, java.util.UUID uuid);
+ }
+
+ public static final class Anchor.State {
+ ctor public Anchor.State(androidx.xr.runtime.internal.TrackingState trackingState, androidx.xr.runtime.math.Pose pose);
+ method public androidx.xr.runtime.math.Pose getPose();
+ method public androidx.xr.runtime.internal.TrackingState getTrackingState();
+ property public final androidx.xr.runtime.math.Pose pose;
+ property public final androidx.xr.runtime.internal.TrackingState trackingState;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class AnchorCreateResourcesExhausted extends androidx.xr.arcore.AnchorCreateResult {
+ ctor public AnchorCreateResourcesExhausted();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract sealed class AnchorCreateResult {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class AnchorCreateSuccess extends androidx.xr.arcore.AnchorCreateResult {
+ ctor public AnchorCreateSuccess(androidx.xr.arcore.Anchor anchor);
+ method public androidx.xr.arcore.Anchor getAnchor();
+ property public final androidx.xr.arcore.Anchor anchor;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class HitResult {
+ method public androidx.xr.arcore.Anchor createAnchor();
+ method public float getDistance();
+ method public androidx.xr.runtime.math.Pose getHitPose();
+ method public androidx.xr.arcore.Trackable<androidx.xr.arcore.Trackable.State> getTrackable();
+ property public final float distance;
+ property public final androidx.xr.runtime.math.Pose hitPose;
+ property public final androidx.xr.arcore.Trackable<androidx.xr.arcore.Trackable.State> trackable;
+ }
+
+ public final class Interaction {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static java.util.List<androidx.xr.arcore.HitResult> hitTest(androidx.xr.runtime.Session session, androidx.xr.runtime.math.Ray ray);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class PerceptionState {
+ method public kotlin.time.ComparableTimeMark getTimeMark();
+ method public java.util.Collection<androidx.xr.arcore.Trackable<androidx.xr.arcore.Trackable.State>> getTrackables();
+ property public final kotlin.time.ComparableTimeMark timeMark;
+ property public final java.util.Collection<androidx.xr.arcore.Trackable<androidx.xr.arcore.Trackable.State>> trackables;
+ }
+
+ public final class PerceptionStateExtenderKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.arcore.PerceptionState? getPerceptionState(androidx.xr.runtime.CoreState);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class Plane implements androidx.xr.arcore.Trackable<androidx.xr.arcore.Plane.State> {
+ method public androidx.xr.arcore.Anchor createAnchor(androidx.xr.runtime.math.Pose pose);
+ method public kotlinx.coroutines.flow.StateFlow<androidx.xr.arcore.Plane.State> getState();
+ method public androidx.xr.arcore.Plane.Type getType();
+ method public static kotlinx.coroutines.flow.StateFlow<java.util.Collection<androidx.xr.arcore.Plane>> subscribe(androidx.xr.runtime.Session session);
+ method public suspend Object? update(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public kotlinx.coroutines.flow.StateFlow<androidx.xr.arcore.Plane.State> state;
+ property public final androidx.xr.arcore.Plane.Type type;
+ field public static final androidx.xr.arcore.Plane.Companion Companion;
+ }
+
+ public static final class Plane.Companion {
+ method public kotlinx.coroutines.flow.StateFlow<java.util.Collection<androidx.xr.arcore.Plane>> subscribe(androidx.xr.runtime.Session session);
+ }
+
+ public static final class Plane.Label {
+ field public static final androidx.xr.arcore.Plane.Label Ceiling;
+ field public static final androidx.xr.arcore.Plane.Label.Companion Companion;
+ field public static final androidx.xr.arcore.Plane.Label Floor;
+ field public static final androidx.xr.arcore.Plane.Label Table;
+ field public static final androidx.xr.arcore.Plane.Label Unknown;
+ field public static final androidx.xr.arcore.Plane.Label Wall;
+ }
+
+ public static final class Plane.Label.Companion {
+ property public final androidx.xr.arcore.Plane.Label Ceiling;
+ property public final androidx.xr.arcore.Plane.Label Floor;
+ property public final androidx.xr.arcore.Plane.Label Table;
+ property public final androidx.xr.arcore.Plane.Label Unknown;
+ property public final androidx.xr.arcore.Plane.Label Wall;
+ }
+
+ public static final class Plane.State implements androidx.xr.arcore.Trackable.State {
+ ctor public Plane.State(androidx.xr.runtime.internal.TrackingState trackingState, androidx.xr.arcore.Plane.Label label, androidx.xr.runtime.math.Pose centerPose, androidx.xr.runtime.math.Vector2 extents, androidx.xr.arcore.Plane? subsumedBy, java.util.List<androidx.xr.runtime.math.Vector2> vertices);
+ method public androidx.xr.runtime.math.Pose getCenterPose();
+ method public androidx.xr.runtime.math.Vector2 getExtents();
+ method public androidx.xr.arcore.Plane.Label getLabel();
+ method public androidx.xr.arcore.Plane? getSubsumedBy();
+ method public androidx.xr.runtime.internal.TrackingState getTrackingState();
+ method public java.util.List<androidx.xr.runtime.math.Vector2> getVertices();
+ property public final androidx.xr.runtime.math.Pose centerPose;
+ property public final androidx.xr.runtime.math.Vector2 extents;
+ property public final androidx.xr.arcore.Plane.Label label;
+ property public final androidx.xr.arcore.Plane? subsumedBy;
+ property public androidx.xr.runtime.internal.TrackingState trackingState;
+ property public final java.util.List<androidx.xr.runtime.math.Vector2> vertices;
+ }
+
+ public static final class Plane.Type {
+ field public static final androidx.xr.arcore.Plane.Type.Companion Companion;
+ field public static final androidx.xr.arcore.Plane.Type HorizontalDownwardFacing;
+ field public static final androidx.xr.arcore.Plane.Type HorizontalUpwardFacing;
+ field public static final androidx.xr.arcore.Plane.Type Vertical;
+ }
+
+ public static final class Plane.Type.Companion {
+ property public final androidx.xr.arcore.Plane.Type HorizontalDownwardFacing;
+ property public final androidx.xr.arcore.Plane.Type HorizontalUpwardFacing;
+ property public final androidx.xr.arcore.Plane.Type Vertical;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Trackable<State> {
+ method public androidx.xr.arcore.Anchor createAnchor(androidx.xr.runtime.math.Pose pose);
+ method public kotlinx.coroutines.flow.StateFlow<androidx.xr.arcore.Trackable.State> getState();
+ property public abstract kotlinx.coroutines.flow.StateFlow<androidx.xr.arcore.Trackable.State> state;
+ }
+
+ public static interface Trackable.State {
+ method public androidx.xr.runtime.internal.TrackingState getTrackingState();
+ property public abstract androidx.xr.runtime.internal.TrackingState trackingState;
+ }
+
+}
+
diff --git a/xr/arcore/arcore/build.gradle b/xr/arcore/arcore/build.gradle
new file mode 100644
index 0000000..336d97e
--- /dev/null
+++ b/xr/arcore/arcore/build.gradle
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+
+import androidx.build.KotlinTarget
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ api(libs.kotlinCoroutinesCore)
+ api(project(":xr:runtime:runtime"))
+
+ implementation("androidx.annotation:annotation:1.8.1")
+ implementation(project(":xr:runtime:runtime-openxr"))
+
+ testImplementation(libs.kotlinCoroutinesTest)
+ testImplementation(libs.junit)
+ testImplementation(libs.robolectric)
+ testImplementation(libs.testExtJunit)
+ testImplementation(libs.testRunner)
+ testImplementation(libs.truth)
+ testImplementation(project(":kruth:kruth"))
+ testImplementation(project(":xr:runtime:runtime-testing"))
+ testImplementation("androidx.appcompat:appcompat:1.2.0")
+ testImplementation(libs.testRules)
+}
+
+android {
+ namespace = "androidx.xr.arcore"
+ sourceSets.main.resources.srcDirs += "src/main/resources"
+ testOptions.unitTests.includeAndroidResources = true
+}
+
+androidx {
+ name = "ARCore"
+ type = LibraryType.PUBLISHED_LIBRARY
+ inceptionYear = "2024"
+ description = "Provides augmented reality (AR) functionality."
+ kotlinTarget = KotlinTarget.KOTLIN_1_9
+ // TODO: b/326456246
+ optOutJSpecify = true
+}
diff --git a/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/Anchor.kt b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/Anchor.kt
new file mode 100644
index 0000000..1c8411b
--- /dev/null
+++ b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/Anchor.kt
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.arcore
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.Session
+import androidx.xr.runtime.internal.Anchor as RuntimeAnchor
+import androidx.xr.runtime.math.Pose
+import java.util.UUID
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * An anchor describes a fixed location and orientation in the real world. To stay at a fixed
+ * location in physical space, the numerical description of this position may update as ARCore for
+ * XR updates its understanding of the physical world.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class Anchor
+internal constructor(
+ public val runtimeAnchor: RuntimeAnchor,
+ private val xrResourceManager: XrResourcesManager,
+) : Updatable {
+ public companion object {
+ /**
+ * Creates an [Anchor] at the given [pose].
+ *
+ * @param session the [Session] that is used to create the anchor.
+ * @param pose the [Pose] that describes the location and orientation of the anchor.
+ * @return the result of the operation. Can be [AnchorCreateSuccess] that contains the
+ * created [Anchor], or [AnchorCreateResourcesExhausted] if the resources allocated for
+ * anchors have been exhausted.
+ */
+ @JvmStatic
+ public fun create(session: Session, pose: Pose): AnchorCreateResult {
+ val perceptionStateExtender = getPerceptionStateExtender(session)
+ val runtimeAnchor = session.runtime.perceptionManager.createAnchor(pose)
+ return generateCreateResult(runtimeAnchor, perceptionStateExtender.xrResourcesManager)
+ }
+
+ /**
+ * Retrieves all the [UUID] instances from [Anchor] objects that have been persisted by
+ * [persist] that are still present in the local storage.
+ */
+ @JvmStatic
+ public fun getPersistedAnchorUuids(session: Session): List<UUID> {
+ return session.runtime.perceptionManager.getPersistedAnchorUuids()
+ }
+
+ /**
+ * Loads an [Anchor] from local storage, using the given [uuid]. The anchor will attempt to
+ * be in the same physical location as the anchor that was previously persisted. The [uuid]
+ * should be the return value of a previous call to [persist].
+ */
+ @JvmStatic
+ public fun load(session: Session, uuid: UUID): AnchorCreateResult {
+ val perceptionStateExtender = getPerceptionStateExtender(session)
+ val runtimeAnchor = session.runtime.perceptionManager.loadAnchor(uuid)
+ return generateCreateResult(runtimeAnchor, perceptionStateExtender.xrResourcesManager)
+ }
+
+ /** Loads an [Anchor] of the given native pointer. */
+ // TODO(b/373711152) : Remove this method once the Jetpack XR Runtime API migration is done.
+ @JvmStatic
+ public fun loadFromNativePointer(session: Session, nativePointer: Long): Anchor {
+ val perceptionStateExtender = getPerceptionStateExtender(session)
+ val runtimeAnchor =
+ session.runtime.perceptionManager.loadAnchorFromNativePointer(nativePointer)
+ return Anchor(runtimeAnchor, perceptionStateExtender.xrResourcesManager)
+ }
+
+ /** Deletes a persisted Anchor denoted by [uuid] from local storage. */
+ @JvmStatic
+ public fun unpersist(session: Session, uuid: UUID) {
+ session.runtime.perceptionManager.unpersistAnchor(uuid)
+ }
+
+ private fun getPerceptionStateExtender(session: Session): PerceptionStateExtender {
+ val perceptionStateExtender: PerceptionStateExtender? =
+ session.stateExtenders.filterIsInstance<PerceptionStateExtender>().first()
+ check(perceptionStateExtender != null) { "PerceptionStateExtender is not available." }
+ return perceptionStateExtender
+ }
+
+ private fun generateCreateResult(
+ runtimeAnchor: RuntimeAnchor,
+ xrResourceManager: XrResourcesManager,
+ ): AnchorCreateResult {
+ val anchor = Anchor(runtimeAnchor, xrResourceManager)
+ xrResourceManager.addUpdatable(anchor)
+ return AnchorCreateSuccess(anchor)
+ }
+ }
+
+ // TODO(b/372049781): This constructor is only used for testing. Remove it once cl/683360061 is
+ // submitted.
+ public constructor(runtimeAnchor: RuntimeAnchor) : this(runtimeAnchor, XrResourcesManager())
+
+ /**
+ * The representation of the current state of an [Anchor].
+ *
+ * @property trackingState the current [TrackingState] of the anchor.
+ * @property pose the location of the anchor in the world coordinate space.
+ */
+ public class State(public val trackingState: TrackingState, public val pose: Pose) {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is State) return false
+ return pose == other.pose && trackingState == other.trackingState
+ }
+
+ override fun hashCode(): Int {
+ var result = pose.hashCode()
+ result = 31 * result + trackingState.hashCode()
+ return result
+ }
+ }
+
+ private val _state: MutableStateFlow<State> =
+ MutableStateFlow<State>(State(runtimeAnchor.trackingState, runtimeAnchor.pose))
+ /** The current [State] of this anchor. */
+ public val state: StateFlow<State> = _state.asStateFlow()
+
+ private var persistContinuation: Continuation<UUID>? = null
+
+ /**
+ * Stores this anchor in the application's local storage so that it can be shared across
+ * sessions.
+ *
+ * @return the [UUID] that uniquely identifies this anchor.
+ */
+ public suspend fun persist(): UUID {
+ runtimeAnchor.persist()
+ // Suspend the coroutine until the anchor is persisted.
+ return suspendCancellableCoroutine { persistContinuation = it }
+ }
+
+ /** Detaches this anchor. This anchor will no longer be updated or tracked. */
+ public fun detach() {
+ runtimeAnchor.detach()
+ xrResourceManager.removeUpdatable(this)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Anchor) return false
+ return runtimeAnchor == other.runtimeAnchor
+ }
+
+ override fun hashCode(): Int = runtimeAnchor.hashCode()
+
+ override suspend fun update() {
+ _state.emit(State(runtimeAnchor.trackingState, runtimeAnchor.pose))
+ if (persistContinuation == null) {
+ return
+ }
+ when (runtimeAnchor.persistenceState) {
+ RuntimeAnchor.PersistenceState.Pending -> {
+ // Do nothing while we wait for the anchor to be persisted.
+ }
+ RuntimeAnchor.PersistenceState.Persisted -> {
+ persistContinuation?.resume(runtimeAnchor.uuid!!)
+ persistContinuation = null
+ }
+ RuntimeAnchor.PersistenceState.NotPersisted -> {
+ persistContinuation?.resumeWithException(
+ RuntimeException("Anchor was not persisted.")
+ )
+ persistContinuation = null
+ }
+ }
+ }
+}
diff --git a/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/AnchorResults.kt b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/AnchorResults.kt
new file mode 100644
index 0000000..2ff9370
--- /dev/null
+++ b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/AnchorResults.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.arcore
+
+import androidx.annotation.RestrictTo
+
+/** Result of a [Anchor.create] call. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public sealed class AnchorCreateResult
+
+/**
+ * Result of a successful [Anchor.create] call.
+ *
+ * @property anchor the [Anchor] that was created.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class AnchorCreateSuccess(public val anchor: Anchor) : AnchorCreateResult()
+
+/**
+ * Result of an unsuccessful [Anchor.create] call. The resources allocated for anchors has been
+ * exhausted.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class AnchorCreateResourcesExhausted() : AnchorCreateResult()
diff --git a/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/HitResult.kt b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/HitResult.kt
new file mode 100644
index 0000000..43dd2b8
--- /dev/null
+++ b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/HitResult.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.arcore
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.math.Pose
+
+/**
+ * Defines an intersection between a ray and estimated real-world geometry.
+ *
+ * Can be obtained from [Interaction.hitTest].
+ *
+ * @property distance the distance from the camera to the hit location, in meters.
+ * @property hitPose the [Pose] of the intersection between a ray and the [Trackable].
+ * @property trackable the [Trackable] that was hit.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class HitResult
+internal constructor(
+ public val distance: Float,
+ public val hitPose: Pose,
+ public val trackable: Trackable<Trackable.State>,
+) {
+ public fun createAnchor(): Anchor {
+ return trackable.createAnchor(hitPose)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is HitResult) return false
+
+ if (distance != other.distance) return false
+ if (hitPose != other.hitPose) return false
+ if (trackable != other.trackable) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = distance.hashCode()
+ result = 31 * result + hitPose.hashCode()
+ result = 31 * result + trackable.hashCode()
+ return result
+ }
+}
diff --git a/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/Interaction.kt b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/Interaction.kt
new file mode 100644
index 0000000..0a656f6
--- /dev/null
+++ b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/Interaction.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+@file:JvmName("Interaction")
+
+package androidx.xr.arcore
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.Session
+import androidx.xr.runtime.math.Ray
+
+/**
+ * Performs a hit-test using the given [ray].
+ *
+ * A hit-test is a method of calculating the intersection of a ray with objects tracked by the
+ * session. Conducting a hit-test results in a list of hit objects, in other words, a hit-test does
+ * not stop at the first object hit.
+ *
+ * @return A list of [HitResult] objects, sorted by distance from the origin of the ray. The nearest
+ * hit is at the beginning of the list.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun hitTest(session: Session, ray: Ray): List<HitResult> {
+ val perceptionStateExtender =
+ session.stateExtenders.filterIsInstance<PerceptionStateExtender>().first()
+ val perceptionManager = perceptionStateExtender.perceptionManager
+ val trackableMap = perceptionStateExtender.xrResourcesManager.trackablesMap
+ return perceptionManager.hitTest(ray).map {
+ val trackable =
+ requireNotNull(trackableMap[it.trackable]) {
+ "No Active Trackable found for the given hit result."
+ }
+ HitResult(it.distance, it.hitPose, trackable)
+ }
+}
diff --git a/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/PerceptionState.kt b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/PerceptionState.kt
new file mode 100644
index 0000000..ae0dc58
--- /dev/null
+++ b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/PerceptionState.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.arcore
+
+import androidx.annotation.RestrictTo
+import kotlin.time.ComparableTimeMark
+
+/**
+ * Represents the state of ARCore for Jetpack XR at an specific point in time.
+ *
+ * Can be obtained from [CoreState.perceptionState].
+ *
+ * @property timeMark the time at which the state was computed.
+ * @property trackables the trackables that are currently being tracked.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class PerceptionState
+internal constructor(
+ public val timeMark: ComparableTimeMark,
+ public val trackables: Collection<Trackable<Trackable.State>>,
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is PerceptionState) return false
+ if (timeMark != other.timeMark) return false
+ if (trackables != other.trackables) return false
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = timeMark.hashCode()
+ result = 31 * result + trackables.hashCode()
+ return result
+ }
+}
diff --git a/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/PerceptionStateExtender.kt b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/PerceptionStateExtender.kt
new file mode 100644
index 0000000..58d82a2
--- /dev/null
+++ b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/PerceptionStateExtender.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.arcore
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.CoreState
+import androidx.xr.runtime.StateExtender
+import androidx.xr.runtime.internal.PerceptionManager
+import androidx.xr.runtime.internal.Runtime
+import kotlin.time.ComparableTimeMark
+
+/** [StateExtender] in charge of extending [CoreState] with [PerceptionState]. */
+internal class PerceptionStateExtender : StateExtender {
+
+ internal companion object {
+ internal const val MAX_PERCEPTION_STATE_EXTENSION_SIZE = 100
+
+ internal val perceptionStateMap = mutableMapOf<ComparableTimeMark, PerceptionState>()
+
+ private val timeMarkQueue = ArrayDeque<ComparableTimeMark>()
+ }
+
+ internal lateinit var perceptionManager: PerceptionManager
+
+ internal val xrResourcesManager = XrResourcesManager()
+
+ override fun initialize(runtime: Runtime) {
+ perceptionManager = runtime.perceptionManager
+ }
+
+ override suspend fun extend(coreState: CoreState) {
+ check(this::perceptionManager.isInitialized) {
+ "PerceptionStateExtender is not initialized."
+ }
+
+ xrResourcesManager.syncTrackables(perceptionManager.trackables)
+ xrResourcesManager.update()
+ updatePerceptionStateMap(coreState)
+ }
+
+ internal fun close() {
+ perceptionStateMap.clear()
+ timeMarkQueue.clear()
+ xrResourcesManager.clear()
+ }
+
+ private fun updatePerceptionStateMap(coreState: CoreState) {
+ perceptionStateMap.put(
+ coreState.timeMark,
+ PerceptionState(coreState.timeMark, xrResourcesManager.trackablesMap.values),
+ )
+ timeMarkQueue.add(coreState.timeMark)
+
+ if (timeMarkQueue.size > MAX_PERCEPTION_STATE_EXTENSION_SIZE) {
+ val timeMark = timeMarkQueue.removeFirst()
+ perceptionStateMap.remove(timeMark)
+ }
+ }
+}
+
+/** The state of the perception system. */
+@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public val CoreState.perceptionState: PerceptionState?
+ get() = PerceptionStateExtender.perceptionStateMap[this.timeMark]
diff --git a/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/Plane.kt b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/Plane.kt
new file mode 100644
index 0000000..5cb27ec
--- /dev/null
+++ b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/Plane.kt
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.arcore
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.Session
+import androidx.xr.runtime.internal.Plane as RuntimePlane
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Vector2
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.transform
+
+/** Describes the system's current best knowledge of a real-world planar surface. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class Plane
+internal constructor(
+ internal val runtimePlane: RuntimePlane,
+ private val xrResourceManager: XrResourcesManager,
+) : Trackable<Plane.State>, Updatable {
+
+ public companion object {
+ /** Emits the planes that are currently being tracked in the [session]. */
+ @JvmStatic
+ public fun subscribe(session: Session): StateFlow<Collection<Plane>> =
+ session.state
+ .transform { state ->
+ state.perceptionState?.let { perceptionState ->
+ emit(perceptionState.trackables.filterIsInstance<Plane>())
+ }
+ }
+ .stateIn(
+ session.coroutineScope,
+ SharingStarted.Eagerly,
+ session.state.value.perceptionState?.trackables?.filterIsInstance<Plane>()
+ ?: emptyList(),
+ )
+ }
+
+ /**
+ * The representation of the current state of a [Plane].
+ *
+ * @property trackingState whether this plane is being tracked or not.
+ * @property label The [Label] associated with the plane.
+ * @property centerPose The pose of the center of the detected plane.
+ * @property extents The dimensions of the detected plane.
+ * @property subsumedBy If this plane has been subsumed, returns the plane this plane was merged
+ * into.
+ * @property vertices The 2D vertices of a convex polygon approximating the detected plane.
+ */
+ public class State(
+ public override val trackingState: TrackingState,
+ public val label: Label,
+ public val centerPose: Pose,
+ public val extents: Vector2,
+ public val subsumedBy: Plane?,
+ public val vertices: List<Vector2>,
+ ) : Trackable.State {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is State) return false
+ return trackingState == other.trackingState &&
+ label == other.label &&
+ centerPose == other.centerPose &&
+ extents == other.extents &&
+ subsumedBy == other.subsumedBy &&
+ vertices == other.vertices
+ }
+
+ override fun hashCode(): Int {
+ var result = trackingState.hashCode()
+ result = 31 * result + label.hashCode()
+ result = 31 * result + centerPose.hashCode()
+ result = 31 * result + extents.hashCode()
+ result = 31 * result + subsumedBy.hashCode()
+ result = 31 * result + vertices.hashCode()
+ return result
+ }
+ }
+
+ /** A simple summary of the normal vector of a [Plane]. */
+ public class Type private constructor(private val value: Int) {
+ public companion object {
+ /** A horizontal plane facing upward (e.g. floor or tabletop). */
+ @JvmField public val HorizontalUpwardFacing: Type = Type(0)
+
+ /** A horizontal plane facing downward (e.g. a ceiling). */
+ @JvmField public val HorizontalDownwardFacing: Type = Type(1)
+
+ /** A vertical plane (e.g. a wall). */
+ @JvmField public val Vertical: Type = Type(2)
+ }
+
+ public override fun toString(): String =
+ when (this) {
+ HorizontalUpwardFacing -> "HorizontalUpwardFacing"
+ HorizontalDownwardFacing -> "HorizontalDownwardFacing"
+ Vertical -> "Vertical"
+ else -> "Unknown"
+ }
+ }
+
+ /** A semantic description of a [Plane]. */
+ public class Label private constructor(private val value: Int) {
+ public companion object {
+ /** The plane represents an unknown type. */
+ @JvmField public val Unknown: Label = Label(0)
+
+ /** The plane represents a wall. */
+ @JvmField public val Wall: Label = Label(1)
+
+ /** The plane represents a floor. */
+ @JvmField public val Floor: Label = Label(2)
+
+ /** The plane represents a ceiling. */
+ @JvmField public val Ceiling: Label = Label(3)
+
+ /** The plane represents a table. */
+ @JvmField public val Table: Label = Label(4)
+ }
+
+ public override fun toString(): String =
+ when (this) {
+ Wall -> "Wall"
+ Floor -> "Floor"
+ Ceiling -> "Ceiling"
+ Table -> "Table"
+ else -> "Unknown"
+ }
+ }
+
+ private val _state =
+ MutableStateFlow(
+ State(
+ runtimePlane.trackingState,
+ labelFromRuntimeType(),
+ runtimePlane.centerPose,
+ runtimePlane.extents,
+ subsumedByFromRuntimePlane(),
+ runtimePlane.vertices,
+ )
+ )
+ /** The current state of the [Plane]. */
+ public override val state: StateFlow<Plane.State> = _state.asStateFlow()
+
+ /** The [Type] of the [Plane]. */
+ public val type: Type
+ get() = typeFromRuntimeType()
+
+ override fun createAnchor(pose: Pose): Anchor {
+ val runtimeAnchor = runtimePlane.createAnchor(pose)
+ val anchor = Anchor(runtimeAnchor, xrResourceManager)
+ xrResourceManager.addUpdatable(anchor)
+ return anchor
+ }
+
+ override suspend fun update() {
+ _state.emit(
+ State(
+ trackingState = runtimePlane.trackingState,
+ label = labelFromRuntimeType(),
+ centerPose = runtimePlane.centerPose,
+ extents = runtimePlane.extents,
+ subsumedBy = subsumedByFromRuntimePlane(),
+ vertices = runtimePlane.vertices,
+ )
+ )
+ }
+
+ private fun typeFromRuntimeType(): Type =
+ when (runtimePlane.type) {
+ RuntimePlane.Type.HorizontalUpwardFacing -> Type.HorizontalUpwardFacing
+ RuntimePlane.Type.HorizontalDownwardFacing -> Type.HorizontalDownwardFacing
+ RuntimePlane.Type.Vertical -> Type.Vertical
+ else -> Type.HorizontalUpwardFacing
+ }
+
+ private fun labelFromRuntimeType(): Label =
+ when (runtimePlane.label) {
+ RuntimePlane.Label.Unknown -> Label.Unknown
+ RuntimePlane.Label.Wall -> Label.Wall
+ RuntimePlane.Label.Floor -> Label.Floor
+ RuntimePlane.Label.Ceiling -> Label.Ceiling
+ RuntimePlane.Label.Table -> Label.Table
+ else -> Label.Unknown
+ }
+
+ private fun subsumedByFromRuntimePlane(): Plane? =
+ runtimePlane.subsumedBy?.let { xrResourceManager.trackablesMap[it] as Plane? }
+}
diff --git a/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/Trackable.kt b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/Trackable.kt
new file mode 100644
index 0000000..3cc159d
--- /dev/null
+++ b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/Trackable.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.arcore
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.math.Pose
+import kotlinx.coroutines.flow.StateFlow
+
+/** An object that ARCore for Jetpack XR can track and that [Anchors] can be attached to. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface Trackable<out State> {
+
+ /** The subset of data that is common to the state of all [Trackable] instances. */
+ public interface State {
+ /** Whether this trackable is being tracked or not. */
+ public val trackingState: TrackingState
+ }
+
+ /** Emits the current state of this trackable. */
+ public val state: StateFlow<Trackable.State>
+
+ /**
+ * Creates an [Anchor] that is attached to this trackable, using the given initial [pose] in the
+ * world coordinate space.
+ */
+ public fun createAnchor(pose: Pose): Anchor
+}
diff --git a/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/TrackingState.kt b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/TrackingState.kt
new file mode 100644
index 0000000..9bd7cde
--- /dev/null
+++ b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/TrackingState.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.arcore
+
+public typealias TrackingState = androidx.xr.runtime.internal.TrackingState
diff --git a/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/Updatable.kt b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/Updatable.kt
new file mode 100644
index 0000000..8db387a
--- /dev/null
+++ b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/Updatable.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.arcore
+
+/** An interface for objects that need to be updated every frame. */
+internal interface Updatable {
+
+ /** Updates the state of the [Updatable]. */
+ suspend fun update()
+}
diff --git a/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/XrResourcesManager.kt b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/XrResourcesManager.kt
new file mode 100644
index 0000000..1eabee9
--- /dev/null
+++ b/xr/arcore/arcore/src/main/kotlin/androidx/xr/arcore/XrResourcesManager.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.arcore
+
+import android.annotation.SuppressLint
+import androidx.xr.runtime.internal.Plane as RuntimePlane
+import androidx.xr.runtime.internal.Trackable as RuntimeTrackable
+import java.util.concurrent.CopyOnWriteArrayList
+import kotlinx.coroutines.flow.update
+
+/** Manages all XR resources that are used by the ARCore for XR API. */
+internal class XrResourcesManager {
+
+ /** List of [Updatable]s that are updated every frame. */
+ private val _updatables = CopyOnWriteArrayList<Updatable>()
+ val updatables: List<Updatable> = _updatables
+
+ /** Map of runtime trackable pointer to [Trackable]. */
+ @SuppressLint("BanConcurrentHashMap")
+ private val _trackablesMap =
+ java.util.concurrent.ConcurrentHashMap<RuntimeTrackable, Trackable<Trackable.State>>()
+ val trackablesMap: Map<RuntimeTrackable, Trackable<Trackable.State>> = _trackablesMap
+
+ internal fun addUpdatable(updatable: Updatable) {
+ _updatables.add(updatable)
+ }
+
+ internal fun removeUpdatable(updatable: Updatable) {
+ _updatables.remove(updatable)
+ }
+
+ internal suspend fun update() {
+ for (updatable in updatables) {
+ updatable.update()
+ }
+ }
+
+ internal fun syncTrackables(runtimeTrackables: Collection<RuntimeTrackable>) {
+ val toRemoveTrackables = _trackablesMap.keys - runtimeTrackables
+ val toAddTrackables = runtimeTrackables - _trackablesMap.keys
+
+ for (runtimeTrackable in toRemoveTrackables) {
+ removeUpdatable(_trackablesMap[runtimeTrackable]!! as Updatable)
+ _trackablesMap.remove(runtimeTrackable)
+ }
+
+ for (runtimeTrackable in toAddTrackables) {
+ val trackable = createTrackable(runtimeTrackable)
+ _trackablesMap[runtimeTrackable] = trackable
+ addUpdatable(trackable as Updatable)
+ }
+ }
+
+ internal fun clear() {
+ _updatables.clear()
+ _trackablesMap.clear()
+ }
+
+ private fun createTrackable(runtimeTrackable: RuntimeTrackable): Trackable<Trackable.State> {
+ if (_trackablesMap.containsKey(runtimeTrackable)) {
+ return _trackablesMap[runtimeTrackable]!!
+ }
+
+ val trackable =
+ when (runtimeTrackable) {
+ is RuntimePlane -> Plane(runtimeTrackable, this)
+ else ->
+ throw IllegalArgumentException(
+ "Unsupported trackable type: ${runtimeTrackable.javaClass}"
+ )
+ }
+ _trackablesMap[runtimeTrackable] = trackable
+ return trackable
+ }
+}
diff --git a/xr/arcore/arcore/src/main/resources/META-INF/services/androidx.xr.runtime.StateExtender b/xr/arcore/arcore/src/main/resources/META-INF/services/androidx.xr.runtime.StateExtender
new file mode 100644
index 0000000..47bee1d
--- /dev/null
+++ b/xr/arcore/arcore/src/main/resources/META-INF/services/androidx.xr.runtime.StateExtender
@@ -0,0 +1,15 @@
+# Copyright 2024 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
+#
+# http://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.
+#
+androidx.xr.arcore.PerceptionStateExtender
\ No newline at end of file
diff --git a/xr/arcore/arcore/src/test/AndroidManifest.xml b/xr/arcore/arcore/src/test/AndroidManifest.xml
new file mode 100644
index 0000000..666fbed
--- /dev/null
+++ b/xr/arcore/arcore/src/test/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2024 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
+
+ http://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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <application>
+ <activity android:name="android.app.Activity" />
+ </application>
+</manifest>
diff --git a/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/AnchorTest.kt b/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/AnchorTest.kt
new file mode 100644
index 0000000..ab6b085
--- /dev/null
+++ b/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/AnchorTest.kt
@@ -0,0 +1,253 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.arcore
+
+import android.app.Activity
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.rule.GrantPermissionRule
+import androidx.xr.runtime.Session
+import androidx.xr.runtime.SessionCreateSuccess
+import androidx.xr.runtime.internal.Anchor as RuntimeAnchor
+import androidx.xr.runtime.internal.TrackingState
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Vector3
+import androidx.xr.runtime.testing.FakePerceptionManager
+import androidx.xr.runtime.testing.FakeRuntimeAnchor
+import androidx.xr.runtime.testing.FakeRuntimePlane
+import com.google.common.truth.Truth.assertThat
+import java.util.UUID
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AnchorTest {
+
+ private lateinit var xrResourcesManager: XrResourcesManager
+
+ @get:Rule
+ val grantPermissionRule = GrantPermissionRule.grant("android.permission.SCENE_UNDERSTANDING")
+
+ @Before
+ fun setUp() {
+ xrResourcesManager = XrResourcesManager()
+ }
+
+ @After
+ fun tearDown() {
+ xrResourcesManager.clear()
+ }
+
+ @Test
+ fun detach_removeAnchorFromActiveAnchorManager() {
+ val runtimeAnchor = FakeRuntimeAnchor(Pose())
+ val underTest = Anchor(runtimeAnchor, xrResourcesManager)
+ xrResourcesManager.addUpdatable(underTest)
+ check(xrResourcesManager.updatables.contains(underTest))
+ check(xrResourcesManager.updatables.size == 1)
+
+ underTest.detach()
+
+ assertThat(xrResourcesManager.updatables).isEmpty()
+ }
+
+ @Test
+ fun update_trackingStateMatchesRuntimeTrackingState() = runBlocking {
+ val runtimeAnchor = FakeRuntimeAnchor(Pose())
+ runtimeAnchor.trackingState = TrackingState.Paused
+ val underTest = Anchor(runtimeAnchor, xrResourcesManager)
+ check(underTest.state.value.trackingState.equals(TrackingState.Paused))
+ runtimeAnchor.trackingState = TrackingState.Tracking
+
+ underTest.update()
+
+ assertThat(underTest.state.value.trackingState).isEqualTo(TrackingState.Tracking)
+ }
+
+ @Test
+ fun update_poseMatchesRuntimePose() = runBlocking {
+ val runtimeAnchor = FakeRuntimeAnchor(Pose())
+ val underTest = Anchor(runtimeAnchor, xrResourcesManager)
+ check(
+ underTest.state.value.pose.equals(
+ Pose(Vector3(0f, 0f, 0f), Quaternion(0f, 0f, 0f, 1.0f))
+ )
+ )
+ val newPose = Pose(Vector3(1.0f, 2.0f, 3.0f), Quaternion(1.0f, 2.0f, 3.0f, 4.0f))
+ runtimeAnchor.pose = newPose
+
+ underTest.update()
+
+ assertThat(underTest.state.value.pose).isEqualTo(newPose)
+ }
+
+ @Test
+ fun persist_runtimeAnchorIsPersisted() = runTest {
+ val runtimeAnchor = FakeRuntimeAnchor(Pose())
+ val underTest = Anchor(runtimeAnchor, xrResourcesManager)
+ check(runtimeAnchor.persistenceState == RuntimeAnchor.PersistenceState.NotPersisted)
+
+ var uuid: UUID? = null
+ val persistJob = launch { uuid = underTest.persist() }
+ val updateJob = launch { underTest.update() }
+ updateJob.join()
+ persistJob.join()
+
+ assertThat(uuid).isNotNull()
+ assertThat(runtimeAnchor.persistenceState)
+ .isEqualTo(RuntimeAnchor.PersistenceState.Persisted)
+ }
+
+ @Test
+ fun getPersistedAnchorUuids_previouslyPersistedAnchor_returnsPersistedAnchorUuid() = runTest {
+ val session = createTestSession()
+ val runtimeAnchor =
+ FakeRuntimeAnchor(Pose(), session.runtime.perceptionManager as FakePerceptionManager)
+ val underTest = Anchor(runtimeAnchor, xrResourcesManager)
+ var uuid: UUID? = null
+ val persistJob = launch { uuid = underTest.persist() }
+ val updateJob = launch { underTest.update() }
+ updateJob.join()
+ persistJob.join()
+
+ assertThat(Anchor.getPersistedAnchorUuids(session)).containsExactly(uuid)
+ }
+
+ @Test
+ @Ignore("Flaky test, see b/380269912")
+ fun getPersistedAnchorUuids_noPreviouslyPersistedAnchors_returnsEmptyList() = runTest {
+ val session = createTestSession()
+
+ assertThat(Anchor.getPersistedAnchorUuids(session)).isEmpty()
+ }
+
+ @Test
+ fun load_previouslyPersistedAnchor_returnsAnchorCreateSuccess() = runTest {
+ val session = createTestSession()
+ val runtimeAnchor =
+ FakeRuntimeAnchor(Pose(), session.runtime.perceptionManager as FakePerceptionManager)
+ val underTest = Anchor(runtimeAnchor, xrResourcesManager)
+ var uuid: UUID? = null
+ val persistJob = launch { uuid = underTest.persist() }
+ val updateJob = launch { underTest.update() }
+ updateJob.join()
+ persistJob.join()
+
+ assertThat(Anchor.load(session, uuid!!)).isInstanceOf(AnchorCreateSuccess::class.java)
+ }
+
+ @Test
+ fun loadFromNativePointer_returnsAnchorCreateSuccess() = runTest {
+ val session = createTestSession()
+
+ assertThat(Anchor.loadFromNativePointer(session, 123L)).isNotNull()
+ }
+
+ @Test
+ fun unpersist_removesAnchorFromStorage() = runTest {
+ val session = createTestSession()
+ val runtimeAnchor =
+ FakeRuntimeAnchor(Pose(), session.runtime.perceptionManager as FakePerceptionManager)
+ val underTest = Anchor(runtimeAnchor, xrResourcesManager)
+ var uuid: UUID? = null
+ val persistJob = launch { uuid = underTest.persist() }
+ val updateJob = launch { underTest.update() }
+ updateJob.join()
+ persistJob.join()
+
+ Anchor.unpersist(session, uuid!!)
+
+ assertThat(Anchor.getPersistedAnchorUuids(session)).doesNotContain(uuid)
+ }
+
+ @Test
+ fun detach_removesRuntimeAnchor() {
+ val runtimeAnchor = FakeRuntimePlane().createAnchor(Pose()) as FakeRuntimeAnchor
+ check(runtimeAnchor.isAttached)
+ val underTest = Anchor(runtimeAnchor, xrResourcesManager)
+
+ underTest.detach()
+
+ assertThat(runtimeAnchor.isAttached).isFalse()
+ }
+
+ @Test
+ fun equals_sameObject_returnsTrue() {
+ val underTest = Anchor(FakeRuntimeAnchor(Pose()), xrResourcesManager)
+
+ assertThat(underTest.equals(underTest)).isTrue()
+ }
+
+ @Test
+ fun equals_differentObjectsSameValues_returnsTrue() {
+ val runtimeAnchor = FakeRuntimeAnchor(Pose())
+ val underTest1 = Anchor(runtimeAnchor, xrResourcesManager)
+ val underTest2 = Anchor(runtimeAnchor, xrResourcesManager)
+
+ assertThat(underTest1.equals(underTest2)).isTrue()
+ }
+
+ @Test
+ fun equals_differentObjectsDifferentValues_returnsFalse() {
+ val underTest1 =
+ Anchor(FakeRuntimeAnchor(Pose(Vector3.Up, Quaternion.Identity)), xrResourcesManager)
+ val underTest2 =
+ Anchor(FakeRuntimeAnchor(Pose(Vector3.Down, Quaternion.Identity)), xrResourcesManager)
+
+ assertThat(underTest1.equals(underTest2)).isFalse()
+ }
+
+ @Test
+ fun hashCode_differentObjectsSameValues_returnsSameHashCode() {
+ val runtimeAnchor = FakeRuntimeAnchor(Pose())
+ val underTest1 = Anchor(runtimeAnchor, xrResourcesManager)
+ val underTest2 = Anchor(runtimeAnchor, xrResourcesManager)
+
+ assertThat(underTest1.hashCode()).isEqualTo(underTest2.hashCode())
+ }
+
+ @Test
+ fun hashCode_differentObjectsDifferentValues_returnsDifferentHashCodes() {
+ val underTest1 =
+ Anchor(FakeRuntimeAnchor(Pose(Vector3.Up, Quaternion.Identity)), xrResourcesManager)
+ val underTest2 =
+ Anchor(FakeRuntimeAnchor(Pose(Vector3.Down, Quaternion.Identity)), xrResourcesManager)
+
+ assertThat(underTest1.hashCode()).isNotEqualTo(underTest2.hashCode())
+ }
+
+ private fun createTestSession(): Session {
+ var session: Session? = null
+ ActivityScenario.launch(Activity::class.java).use {
+ it.onActivity { activity ->
+ session =
+ (Session.create(activity, StandardTestDispatcher()) as SessionCreateSuccess)
+ .session
+ }
+ }
+ return checkNotNull(session) { "Session must not be null." }
+ }
+}
diff --git a/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/HitResultTest.kt b/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/HitResultTest.kt
new file mode 100644
index 0000000..93be668
--- /dev/null
+++ b/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/HitResultTest.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.arcore
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.runtime.math.Pose
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class HitResultTest {
+
+ class TestTrackable : Trackable<Trackable.State> {
+ override fun createAnchor(pose: Pose): Anchor {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override val state: StateFlow<Trackable.State> =
+ MutableStateFlow(
+ object : Trackable.State {
+ override val trackingState = TrackingState.Stopped
+ }
+ )
+ }
+
+ @Test
+ fun equals_sameObject_returnsTrue() {
+ val underTest = HitResult(1.0f, Pose(), TestTrackable())
+
+ assertThat(underTest.equals(underTest)).isTrue()
+ }
+
+ @Test
+ fun equals_differentObjectsSameValues_returnsTrue() {
+ val distance = 1.0f
+ val pose = Pose()
+ val trackable = TestTrackable()
+ val underTest1 = HitResult(distance, pose, trackable)
+ val underTest2 = HitResult(distance, pose, trackable)
+
+ assertThat(underTest1.equals(underTest2)).isTrue()
+ }
+
+ @Test
+ fun equals_differentObjectsDifferentValues_returnsFalse() {
+ val underTest1 = HitResult(1.0f, Pose(), TestTrackable())
+ val underTest2 = HitResult(2.0f, Pose(), TestTrackable())
+
+ assertThat(underTest1.equals(underTest2)).isFalse()
+ }
+
+ @Test
+ fun hashCode_differentObjectsSameValues_returnsSameHashCode() {
+ val distance = 1.0f
+ val pose = Pose()
+ val trackable = TestTrackable()
+ val underTest1 = HitResult(distance, pose, trackable)
+ val underTest2 = HitResult(distance, pose, trackable)
+
+ assertThat(underTest1.hashCode()).isEqualTo(underTest2.hashCode())
+ }
+
+ @Test
+ fun hashCode_differentObjectsDifferentValues_returnsDifferentHashCodes() {
+ val underTest1 = HitResult(1.0f, Pose(), TestTrackable())
+ val underTest2 = HitResult(2.0f, Pose(), TestTrackable())
+
+ assertThat(underTest1.hashCode()).isNotEqualTo(underTest2.hashCode())
+ }
+}
diff --git a/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/InteractionTest.kt b/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/InteractionTest.kt
new file mode 100644
index 0000000..8a91c6c
--- /dev/null
+++ b/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/InteractionTest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.arcore
+
+import android.app.Activity
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.rule.GrantPermissionRule
+import androidx.xr.runtime.CoreState
+import androidx.xr.runtime.Session
+import androidx.xr.runtime.SessionCreateSuccess
+import androidx.xr.runtime.internal.HitResult as RuntimeHitResult
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Ray
+import androidx.xr.runtime.math.Vector3
+import androidx.xr.runtime.testing.FakePerceptionManager
+import androidx.xr.runtime.testing.FakeRuntime
+import androidx.xr.runtime.testing.FakeRuntimePlane
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.TestTimeSource
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class InteractionTest {
+
+ private lateinit var session: Session
+ private lateinit var activity: Activity
+ private lateinit var timeSource: TestTimeSource
+ private lateinit var perceptionStateExtender: PerceptionStateExtender
+ private lateinit var perceptionManager: FakePerceptionManager
+
+ @get:Rule
+ val grantPermissionRule = GrantPermissionRule.grant("android.permission.SCENE_UNDERSTANDING")
+
+ @Before
+ fun setUp() {
+ session = createTestSession()
+ timeSource = (session.runtime as FakeRuntime).lifecycleManager.timeSource
+ perceptionStateExtender =
+ session.stateExtenders.filterIsInstance<PerceptionStateExtender>().first()
+ perceptionManager = perceptionStateExtender.perceptionManager as FakePerceptionManager
+ }
+
+ @Test
+ fun hitTest_successWithOneHitResult() = runTest {
+ val runtimePlane = FakeRuntimePlane()
+ perceptionManager.addTrackable(runtimePlane)
+ // Mock the behavior of session update.
+ val timeMark = timeSource.markNow()
+ val state = CoreState(timeMark)
+ perceptionStateExtender.extend(state)
+ check(state.perceptionState?.trackables?.size == 1)
+ val expectedTrackable = state.perceptionState?.trackables?.first()
+ val runtimeHitResult: RuntimeHitResult =
+ RuntimeHitResult(
+ distance = 1f,
+ hitPose = Pose(Vector3(1f, 2f, 3f), Quaternion(4f, 5f, 6f, 7f)),
+ trackable = runtimePlane,
+ )
+ perceptionManager.addHitResult(runtimeHitResult)
+
+ val hitResults = hitTest(session, Ray(Vector3(0f, 0f, 0f), Vector3(0f, 0f, 1f)))
+
+ assertThat(hitResults.size).isEqualTo(1)
+ assertThat(hitResults[0].distance).isEqualTo(runtimeHitResult.distance)
+ assertThat(hitResults[0].hitPose).isEqualTo(runtimeHitResult.hitPose)
+ assertThat(hitResults[0].trackable).isEqualTo(expectedTrackable)
+ }
+
+ private fun createTestSession(): Session {
+ var session: Session? = null
+ ActivityScenario.launch(Activity::class.java).use {
+ it.onActivity { activity ->
+ session =
+ (Session.create(activity, StandardTestDispatcher()) as SessionCreateSuccess)
+ .session
+ }
+ }
+ return checkNotNull(session) { "Session must not be null." }
+ }
+}
diff --git a/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/PerceptionStateExtenderTest.kt b/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/PerceptionStateExtenderTest.kt
new file mode 100644
index 0000000..4beb754
--- /dev/null
+++ b/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/PerceptionStateExtenderTest.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.arcore
+
+import android.app.Activity
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.runtime.CoreState
+import androidx.xr.runtime.internal.Trackable as RuntimeTrackable
+import androidx.xr.runtime.internal.TrackingState
+import androidx.xr.runtime.testing.FakeRuntime
+import androidx.xr.runtime.testing.FakeRuntimeFactory
+import androidx.xr.runtime.testing.FakeRuntimePlane
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.TestTimeSource
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class PerceptionStateExtenderTest {
+
+ private lateinit var fakeRuntime: FakeRuntime
+ private lateinit var timeSource: TestTimeSource
+ private lateinit var underTest: PerceptionStateExtender
+
+ @Before
+ fun setUp() {
+ fakeRuntime = FakeRuntimeFactory().createRuntime(Activity())
+ timeSource = fakeRuntime.lifecycleManager.timeSource
+ underTest = PerceptionStateExtender()
+ }
+
+ @After
+ fun tearDown() {
+ underTest.close()
+ }
+
+ @Test
+ fun extend_notInitialized_throwsIllegalStateException(): Unit = runBlocking {
+ val coreState = CoreState(timeSource.markNow())
+
+ assertFailsWith<IllegalStateException> { underTest.extend(coreState) }
+ }
+
+ @Test
+ fun extend_withOneState_addsAllTrackablesToTheCollection(): Unit = runBlocking {
+ // arrange
+ underTest.initialize(fakeRuntime)
+
+ val timeMark = timeSource.markNow()
+ val coreState = CoreState(timeMark)
+ val runtimeTrackable: RuntimeTrackable = FakeRuntimePlane()
+ fakeRuntime.perceptionManager.addTrackable(runtimeTrackable)
+
+ // act
+ underTest.extend(coreState)
+
+ // assert
+ val perceptionState = coreState.perceptionState!!
+ assertThat(perceptionState.trackables).hasSize(1)
+ assertThat(convertTrackable(perceptionState.trackables.last())).isEqualTo(runtimeTrackable)
+ }
+
+ @Test
+ fun extend_withTwoStates_updatesAllTrackablesInTheirCollection(): Unit = runBlocking {
+ // arrange
+ underTest.initialize(fakeRuntime)
+ fakeRuntime.perceptionManager.addTrackable(FakeRuntimePlane())
+ underTest.extend(CoreState(timeSource.markNow()))
+
+ timeSource += 10.milliseconds
+ val timeMark = timeSource.markNow()
+ fakeRuntime.perceptionManager.trackables.clear()
+ val runtimeTrackable = FakeRuntimePlane()
+ fakeRuntime.perceptionManager.addTrackable(runtimeTrackable)
+ val coreState = CoreState(timeMark)
+
+ // act
+ underTest.extend(coreState)
+
+ // assert
+ val perceptionState = coreState.perceptionState!!
+ assertThat(perceptionState.trackables).hasSize(1)
+ assertThat(convertTrackable(perceptionState.trackables.last())).isEqualTo(runtimeTrackable)
+ }
+
+ @Test
+ fun extend_withTwoStates_trackableStatusUpdated(): Unit = runBlocking {
+ // arrange
+ underTest.initialize(fakeRuntime)
+ val runtimeTrackable = FakeRuntimePlane()
+ fakeRuntime.perceptionManager.addTrackable(runtimeTrackable)
+ val coreState = CoreState(timeSource.markNow())
+ underTest.extend(coreState)
+ check(
+ coreState.perceptionState!!.trackables.last().state.value.trackingState ==
+ TrackingState.Tracking
+ )
+
+ // act
+ timeSource += 10.milliseconds
+ runtimeTrackable.trackingState = TrackingState.Stopped
+ val coreState2 = CoreState(timeSource.markNow())
+ underTest.extend(coreState2)
+
+ // assert
+ assertThat(coreState2.perceptionState!!.trackables.last().state.value.trackingState)
+ .isEqualTo(TrackingState.Stopped)
+ }
+
+ @Test
+ fun extend_perceptionStateMapSizeExceedsMax(): Unit = runBlocking {
+ // arrange
+ underTest.initialize(fakeRuntime)
+ val timeMark = timeSource.markNow()
+ val coreState = CoreState(timeMark)
+
+ // act
+ underTest.extend(coreState)
+ // make sure the perception state is added to the map at the beginning
+ check(coreState.perceptionState != null)
+
+ for (i in 1..PerceptionStateExtender.MAX_PERCEPTION_STATE_EXTENSION_SIZE) {
+ timeSource += 10.milliseconds
+ underTest.extend(CoreState(timeSource.markNow()))
+ }
+
+ // assert
+ assertThat(coreState.perceptionState).isNull()
+ }
+
+ @Test
+ fun close_cleanUpData(): Unit = runBlocking {
+ // arrange
+ underTest.initialize(fakeRuntime)
+ val timeMark = timeSource.markNow()
+ val coreState = CoreState(timeMark)
+ underTest.extend(coreState)
+ // make sure the perception state is added to the map at the beginning
+ check(coreState.perceptionState != null)
+
+ // act
+ underTest.close()
+
+ // assert
+ assertThat(coreState.perceptionState).isNull()
+ }
+
+ private fun convertTrackable(trackable: Trackable<Trackable.State>): RuntimeTrackable {
+ return when (trackable) {
+ is Plane -> trackable.runtimePlane
+ else ->
+ throw IllegalArgumentException("Unsupported trackable type: ${trackable.javaClass}")
+ }
+ }
+}
diff --git a/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/PlaneTest.kt b/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/PlaneTest.kt
new file mode 100644
index 0000000..cf0a7bc
--- /dev/null
+++ b/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/PlaneTest.kt
@@ -0,0 +1,268 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.arcore
+
+import android.app.Activity
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.rule.GrantPermissionRule
+import androidx.xr.runtime.Session
+import androidx.xr.runtime.SessionCreateSuccess
+import androidx.xr.runtime.internal.Plane as RuntimePlane
+import androidx.xr.runtime.internal.TrackingState
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Vector2
+import androidx.xr.runtime.math.Vector3
+import androidx.xr.runtime.testing.FakePerceptionManager
+import androidx.xr.runtime.testing.FakeRuntimePlane
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class PlaneTest {
+ private lateinit var xrResourcesManager: XrResourcesManager
+ private lateinit var testDispatcher: TestDispatcher
+ private lateinit var testScope: TestScope
+
+ @get:Rule
+ val grantPermissionRule = GrantPermissionRule.grant("android.permission.SCENE_UNDERSTANDING")
+
+ @Before
+ fun setUp() {
+ xrResourcesManager = XrResourcesManager()
+ testDispatcher = StandardTestDispatcher()
+ testScope = TestScope(testDispatcher)
+ }
+
+ @After
+ fun tearDown() {
+ xrResourcesManager.clear()
+ }
+
+ @Test
+ fun constructor_convertsRuntimePlaneType() {
+ val plane1 =
+ Plane(
+ FakeRuntimePlane(type = RuntimePlane.Type.HorizontalUpwardFacing),
+ xrResourcesManager
+ )
+ val plane2 =
+ Plane(
+ FakeRuntimePlane(type = RuntimePlane.Type.HorizontalDownwardFacing),
+ xrResourcesManager
+ )
+ val plane3 = Plane(FakeRuntimePlane(type = RuntimePlane.Type.Vertical), xrResourcesManager)
+
+ assertThat(plane1.type).isEqualTo(Plane.Type.HorizontalUpwardFacing)
+ assertThat(plane2.type).isEqualTo(Plane.Type.HorizontalDownwardFacing)
+ assertThat(plane3.type).isEqualTo(Plane.Type.Vertical)
+ }
+
+ @Test
+ fun constructor_convertsRuntimePlaneLabel() {
+ val plane1 = Plane(FakeRuntimePlane(label = RuntimePlane.Label.Unknown), xrResourcesManager)
+ val plane2 = Plane(FakeRuntimePlane(label = RuntimePlane.Label.Wall), xrResourcesManager)
+ val plane3 = Plane(FakeRuntimePlane(label = RuntimePlane.Label.Floor), xrResourcesManager)
+ val plane4 = Plane(FakeRuntimePlane(label = RuntimePlane.Label.Ceiling), xrResourcesManager)
+ val plane5 = Plane(FakeRuntimePlane(label = RuntimePlane.Label.Table), xrResourcesManager)
+
+ assertThat(plane1.state.value.label).isEqualTo(Plane.Label.Unknown)
+ assertThat(plane2.state.value.label).isEqualTo(Plane.Label.Wall)
+ assertThat(plane3.state.value.label).isEqualTo(Plane.Label.Floor)
+ assertThat(plane4.state.value.label).isEqualTo(Plane.Label.Ceiling)
+ assertThat(plane5.state.value.label).isEqualTo(Plane.Label.Table)
+ }
+
+ @Test
+ fun subscribe_collectReturnsPlane() =
+ runTest(testDispatcher) {
+ val session = createTestSession(testDispatcher)
+ val perceptionManager = session.runtime.perceptionManager as FakePerceptionManager
+ val runtimePlane = FakeRuntimePlane()
+ perceptionManager.addTrackable(runtimePlane)
+ awaitNewCoreState(session, testScope)
+
+ var underTest = emptyList<Plane>()
+ testScope.backgroundScope.launch(start = CoroutineStart.UNDISPATCHED) {
+ underTest = Plane.subscribe(session).first().toList()
+ }
+
+ assertThat(underTest.size).isEqualTo(1)
+ assertThat(underTest.first().runtimePlane).isEqualTo(runtimePlane)
+ }
+
+ @Test
+ fun createAnchor_usesGivenPose() {
+ val runtimePlane = FakeRuntimePlane()
+ xrResourcesManager.syncTrackables(listOf(runtimePlane))
+ val underTest = xrResourcesManager.trackablesMap.values.first() as Plane
+ val pose = Pose(Vector3(1.0f, 2.0f, 3.0f), Quaternion(1.0f, 2.0f, 3.0f, 4.0f))
+
+ val anchor = underTest.createAnchor(pose)
+
+ assertThat(anchor.state.value.pose).isEqualTo(pose)
+ }
+
+ @Test
+ fun update_trackingStateMatchesRuntime() = runBlocking {
+ // arrange
+ val runtimePlane = FakeRuntimePlane()
+ runtimePlane.trackingState = TrackingState.Stopped
+ xrResourcesManager.syncTrackables(listOf(runtimePlane))
+ val underTest = xrResourcesManager.trackablesMap.values.first() as Plane
+ check(underTest.state.value.trackingState == TrackingState.Stopped)
+
+ // act
+ runtimePlane.trackingState = TrackingState.Tracking
+ underTest.update()
+
+ // assert
+ assertThat(underTest.state.value.trackingState).isEqualTo(TrackingState.Tracking)
+ }
+
+ @Test
+ fun update_centerPoseMatchesRuntime() = runBlocking {
+ // arrange
+ val runtimePlane = FakeRuntimePlane()
+ xrResourcesManager.syncTrackables(listOf(runtimePlane))
+ val underTest = xrResourcesManager.trackablesMap.values.first() as Plane
+ check(
+ underTest.state.value.centerPose.equals(
+ Pose(Vector3(0f, 0f, 0f), Quaternion(0f, 0f, 0f, 1.0f))
+ )
+ )
+
+ // act
+ val newPose = Pose(Vector3(1.0f, 2.0f, 3.0f), Quaternion(1.0f, 2.0f, 3.0f, 4.0f))
+ runtimePlane.centerPose = newPose
+ underTest.update()
+
+ // assert
+ assertThat(underTest.state.value.centerPose).isEqualTo(newPose)
+ }
+
+ @Test
+ fun update_extentsMatchesRuntime() = runBlocking {
+ // arrange
+ val runtimePlane = FakeRuntimePlane()
+ val extents = Vector2(1.0f, 2.0f)
+ runtimePlane.extents = extents
+ xrResourcesManager.syncTrackables(listOf(runtimePlane))
+ val underTest = xrResourcesManager.trackablesMap.values.first() as Plane
+ check(underTest.state.value.extents == extents)
+
+ // act
+ val newExtents = Vector2(3.0f, 4.0f)
+ runtimePlane.extents = newExtents
+ underTest.update()
+
+ // assert
+ assertThat(underTest.state.value.extents).isEqualTo(newExtents)
+ }
+
+ @Test
+ fun update_verticesMatchesRuntime() = runBlocking {
+ // arrange
+ val runtimePlane = FakeRuntimePlane()
+ val vertices = listOf(Vector2(1.0f, 2.0f), Vector2(3.0f, 4.0f))
+ runtimePlane.vertices = vertices
+ xrResourcesManager.syncTrackables(listOf(runtimePlane))
+ val underTest = xrResourcesManager.trackablesMap.values.first() as Plane
+ check(underTest.state.value.vertices == vertices)
+
+ // act
+ val newVertices = listOf(Vector2(3.0f, 4.0f), Vector2(5.0f, 6.0f))
+ runtimePlane.vertices = newVertices
+ underTest.update()
+
+ // assert
+ assertThat(underTest.state.value.vertices).isEqualTo(newVertices)
+ }
+
+ @Test
+ fun update_subsumedByMatchesRuntime() = runBlocking {
+ // arrange
+ val runtimePlane = FakeRuntimePlane()
+ val subsumedByPlane = FakeRuntimePlane()
+ xrResourcesManager.syncTrackables(listOf(runtimePlane, subsumedByPlane))
+ xrResourcesManager.update()
+ val underTest = xrResourcesManager.trackablesMap[runtimePlane] as Plane
+ check(underTest.state.value.subsumedBy == null)
+
+ // act
+ runtimePlane.subsumedBy = subsumedByPlane
+ xrResourcesManager.update()
+
+ // assert
+ assertThat(underTest.state.value.subsumedBy).isNotNull()
+ assertThat(underTest.state.value.subsumedBy!!.runtimePlane).isEqualTo(subsumedByPlane)
+ }
+
+ @Test
+ fun labelToString_returnsCorrectString() {
+ assertThat(Plane.Label.Wall.toString()).isEqualTo("Wall")
+ assertThat(Plane.Label.Floor.toString()).isEqualTo("Floor")
+ assertThat(Plane.Label.Ceiling.toString()).isEqualTo("Ceiling")
+ assertThat(Plane.Label.Table.toString()).isEqualTo("Table")
+ assertThat(Plane.Label.Unknown.toString()).isEqualTo("Unknown")
+ }
+
+ @Test
+ fun typeToString_returnsCorrectString() {
+ assertThat(Plane.Type.HorizontalUpwardFacing.toString()).isEqualTo("HorizontalUpwardFacing")
+ assertThat(Plane.Type.HorizontalDownwardFacing.toString())
+ .isEqualTo("HorizontalDownwardFacing")
+ assertThat(Plane.Type.Vertical.toString()).isEqualTo("Vertical")
+ }
+
+ private fun createTestSession(
+ coroutineDispatcher: CoroutineDispatcher = StandardTestDispatcher()
+ ): Session {
+ var session: Session? = null
+ ActivityScenario.launch(Activity::class.java).use {
+ it.onActivity { activity ->
+ session =
+ (Session.create(activity, coroutineDispatcher) as SessionCreateSuccess).session
+ }
+ }
+ return checkNotNull(session) { "Session must not be null." }
+ }
+
+ /** Resumes and pauses the session just enough to emit a new CoreState. */
+ @OptIn(ExperimentalCoroutinesApi::class)
+ private suspend fun awaitNewCoreState(session: Session, testScope: TestScope) {
+ session.resume()
+ testScope.advanceUntilIdle()
+ session.pause()
+ }
+}
diff --git a/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/TrackingStateTest.kt b/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/TrackingStateTest.kt
new file mode 100644
index 0000000..63ad6d1
--- /dev/null
+++ b/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/TrackingStateTest.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.arcore
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TrackingStateTest {
+
+ @Test
+ fun toString_returnsCorrectString() {
+ assertThat(TrackingState.Tracking.toString()).isEqualTo("Tracking")
+ assertThat(TrackingState.Paused.toString()).isEqualTo("Paused")
+ assertThat(TrackingState.Stopped.toString()).isEqualTo("Stopped")
+ }
+}
diff --git a/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/XrResourcesManagerTest.kt b/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/XrResourcesManagerTest.kt
new file mode 100644
index 0000000..d2560e6
--- /dev/null
+++ b/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/XrResourcesManagerTest.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.arcore
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.testing.FakeRuntimeAnchor
+import androidx.xr.runtime.testing.FakeRuntimePlane
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class XrResourcesManagerTest {
+
+ private lateinit var underTest: XrResourcesManager
+
+ @Before
+ fun setUp() {
+ underTest = XrResourcesManager()
+ }
+
+ @After
+ fun tearDown() {
+ underTest.clear()
+ }
+
+ @Test
+ fun addUpdatable_addsUpdatable() {
+ val anchor = Anchor(FakeRuntimeAnchor(Pose()), underTest)
+ check(underTest.updatables.isEmpty())
+
+ underTest.addUpdatable(anchor)
+
+ assertThat(underTest.updatables).containsExactly(anchor)
+ }
+
+ @Test
+ fun removeUpdatable_removesUpdatable() {
+ val anchor = Anchor(FakeRuntimeAnchor(Pose()), underTest)
+ underTest.addUpdatable(anchor)
+ check(underTest.updatables.contains(anchor))
+ check(underTest.updatables.size == 1)
+
+ underTest.removeUpdatable(anchor)
+
+ assertThat(underTest.updatables).isEmpty()
+ }
+
+ @Test
+ fun clear_clearAllUpdatables() {
+ val runtimeAnchor = FakeRuntimeAnchor(Pose())
+ val runtimeAnchor2 = FakeRuntimeAnchor(Pose())
+ val anchor = Anchor(runtimeAnchor, underTest)
+ val anchor2 = Anchor(runtimeAnchor2, underTest)
+ underTest.addUpdatable(anchor)
+ underTest.addUpdatable(anchor2)
+ check(underTest.updatables.isNotEmpty())
+
+ underTest.clear()
+
+ assertThat(underTest.updatables).isEmpty()
+ }
+
+ @Test
+ fun syncTrackables_replacesExistingTrackables() {
+ val runtimeTrackable1 = FakeRuntimePlane()
+ val runtimeTrackable2 = FakeRuntimePlane()
+ val runtimeTrackable3 = FakeRuntimePlane()
+ underTest.syncTrackables(listOf(runtimeTrackable1, runtimeTrackable2))
+ check(underTest.trackablesMap[runtimeTrackable1] != null)
+ check(underTest.trackablesMap[runtimeTrackable2] != null)
+ check(underTest.trackablesMap[runtimeTrackable3] == null)
+
+ underTest.syncTrackables(listOf(runtimeTrackable2, runtimeTrackable3))
+
+ assertThat(underTest.trackablesMap[runtimeTrackable1]).isNull()
+ assertThat(underTest.trackablesMap[runtimeTrackable2]).isNotNull()
+ assertThat(underTest.trackablesMap[runtimeTrackable3]).isNotNull()
+ }
+
+ @Test
+ fun clear_clearsAllTrackables() {
+ underTest.syncTrackables(listOf(FakeRuntimePlane()))
+ check(underTest.trackablesMap.isNotEmpty())
+
+ underTest.clear()
+
+ assertThat(underTest.trackablesMap).isEmpty()
+ }
+}
diff --git a/camera/camera-effects-still-portrait/api/current.txt b/xr/compose/compose-testing/api/current.txt
similarity index 100%
copy from camera/camera-effects-still-portrait/api/current.txt
copy to xr/compose/compose-testing/api/current.txt
diff --git a/camera/camera-effects-still-portrait/api/res-current.txt b/xr/compose/compose-testing/api/res-current.txt
similarity index 100%
copy from camera/camera-effects-still-portrait/api/res-current.txt
copy to xr/compose/compose-testing/api/res-current.txt
diff --git a/xr/compose/compose-testing/api/restricted_current.txt b/xr/compose/compose-testing/api/restricted_current.txt
new file mode 100644
index 0000000..d738061
--- /dev/null
+++ b/xr/compose/compose-testing/api/restricted_current.txt
@@ -0,0 +1,91 @@
+// Signature format: 4.0
+package androidx.xr.compose.testing {
+
+ public final class SubspaceAssertionsKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @com.google.errorprone.annotations.CanIgnoreReturnValue public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertDepthIsAtLeast(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction, float expectedMinDepth);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @com.google.errorprone.annotations.CanIgnoreReturnValue public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertDepthIsEqualTo(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction, float expectedDepth);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @com.google.errorprone.annotations.CanIgnoreReturnValue public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertHeightIsAtLeast(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction, float expectedMinHeight);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @com.google.errorprone.annotations.CanIgnoreReturnValue public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertHeightIsEqualTo(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction, float expectedHeight);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void assertIsEqualTo(float, float expected, String subject, optional float tolerance);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @com.google.errorprone.annotations.CanIgnoreReturnValue public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertLeftPositionInRootIsEqualTo(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction, float expectedLeft);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @com.google.errorprone.annotations.CanIgnoreReturnValue public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertPositionInRootIsEqualTo(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction, float expectedX, float expectedY, float expectedZ);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @com.google.errorprone.annotations.CanIgnoreReturnValue public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertPositionIsEqualTo(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction, float expectedX, float expectedY, float expectedZ);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @com.google.errorprone.annotations.CanIgnoreReturnValue public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertRotationInRootIsEqualTo(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction, androidx.xr.runtime.math.Quaternion expected);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @com.google.errorprone.annotations.CanIgnoreReturnValue public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertRotationIsEqualTo(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction, androidx.xr.runtime.math.Quaternion expected);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @com.google.errorprone.annotations.CanIgnoreReturnValue public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertTopPositionInRootIsEqualTo(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction, float expectedTop);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @com.google.errorprone.annotations.CanIgnoreReturnValue public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertWidthIsAtLeast(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction, float expectedMinWidth);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @com.google.errorprone.annotations.CanIgnoreReturnValue public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertWidthIsEqualTo(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction, float expectedWidth);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @com.google.errorprone.annotations.CanIgnoreReturnValue public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertXPositionInRootIsEqualTo(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction, float expectedX);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @com.google.errorprone.annotations.CanIgnoreReturnValue public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertXPositionIsEqualTo(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction, float expectedX);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @com.google.errorprone.annotations.CanIgnoreReturnValue public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertYPositionInRootIsEqualTo(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction, float expectedY);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @com.google.errorprone.annotations.CanIgnoreReturnValue public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertYPositionIsEqualTo(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction, float expectedY);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @com.google.errorprone.annotations.CanIgnoreReturnValue public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertZPositionInRootIsEqualTo(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction, float expectedZ);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @com.google.errorprone.annotations.CanIgnoreReturnValue public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertZPositionIsEqualTo(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction, float expectedZ);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.runtime.math.Vector3 getPosition(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.runtime.math.Vector3 getPositionInRoot(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.runtime.math.Quaternion getRotation(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.runtime.math.Quaternion getRotationInRoot(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.unit.DpVolumeSize getSize(androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static float toDp(float);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static float toDp(int);
+ }
+
+ public final class SubspaceFiltersKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.testing.SubspaceSemanticsMatcher hasAnyAncestor(androidx.xr.compose.testing.SubspaceSemanticsMatcher matcher);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.testing.SubspaceSemanticsMatcher hasAnyChild(androidx.xr.compose.testing.SubspaceSemanticsMatcher matcher);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.testing.SubspaceSemanticsMatcher hasAnyDescendant(androidx.xr.compose.testing.SubspaceSemanticsMatcher matcher);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.testing.SubspaceSemanticsMatcher hasAnySibling(androidx.xr.compose.testing.SubspaceSemanticsMatcher matcher);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.testing.SubspaceSemanticsMatcher hasContentDescription(String value, optional boolean substring, optional boolean ignoreCase);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.testing.SubspaceSemanticsMatcher hasParent(androidx.xr.compose.testing.SubspaceSemanticsMatcher matcher);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.testing.SubspaceSemanticsMatcher hasTestTag(String testTag);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.testing.SubspaceSemanticsMatcher isFocusable();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.testing.SubspaceSemanticsMatcher isFocused();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.testing.SubspaceSemanticsMatcher isNotFocusable();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.testing.SubspaceSemanticsMatcher isNotFocused();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.testing.SubspaceSemanticsMatcher isRoot();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SubspaceSemanticsMatcher {
+ ctor public SubspaceSemanticsMatcher(String description, kotlin.jvm.functions.Function1<? super androidx.xr.compose.subspace.node.SubspaceSemanticsNode,java.lang.Boolean> matcher);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SubspaceSemanticsNodeInteraction {
+ ctor public SubspaceSemanticsNodeInteraction(androidx.xr.compose.testing.SubspaceTestContext testContext, androidx.xr.compose.testing.SubspaceSemanticsMatcher matcher);
+ method public void assertDoesNotExist();
+ method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction assertExists(optional String? errorMessageOnFail);
+ method public androidx.xr.compose.subspace.node.SubspaceSemanticsNode fetchSemanticsNode(optional String? errorMessageOnFail);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SubspaceSemanticsNodeInteractionCollection {
+ ctor public SubspaceSemanticsNodeInteractionCollection(androidx.xr.compose.testing.SubspaceTestContext testContext, androidx.xr.compose.testing.SubspaceSemanticsMatcher matcher);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SubspaceTestContext {
+ ctor public SubspaceTestContext(androidx.compose.ui.test.junit4.AndroidComposeTestRule<? extends java.lang.Object?,? extends java.lang.Object?> testRule);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SubspaceTestingActivity extends androidx.activity.ComponentActivity {
+ ctor public SubspaceTestingActivity();
+ method public androidx.xr.scenecore.testing.FakeXrExtensions getExtensions();
+ method public androidx.xr.scenecore.Session getSession();
+ property public final androidx.xr.scenecore.testing.FakeXrExtensions extensions;
+ property public final androidx.xr.scenecore.Session session;
+ }
+
+ public final class SubspaceTestingActivityKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.scenecore.JxrPlatformAdapter createFakeRuntime(androidx.xr.compose.testing.SubspaceTestingActivity activity);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.scenecore.Session createFakeSession(androidx.xr.compose.testing.SubspaceTestingActivity activity, optional androidx.xr.scenecore.JxrPlatformAdapter runtime);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteractionCollection onAllSubspaceNodes(androidx.compose.ui.test.junit4.AndroidComposeTestRule<? extends java.lang.Object?,androidx.xr.compose.testing.SubspaceTestingActivity>, androidx.xr.compose.testing.SubspaceSemanticsMatcher matcher);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteractionCollection onAllSubspaceNodesWithTag(androidx.compose.ui.test.junit4.AndroidComposeTestRule<? extends java.lang.Object?,androidx.xr.compose.testing.SubspaceTestingActivity>, String testTag);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction onSubspaceNode(androidx.compose.ui.test.junit4.AndroidComposeTestRule<? extends java.lang.Object?,androidx.xr.compose.testing.SubspaceTestingActivity>, androidx.xr.compose.testing.SubspaceSemanticsMatcher matcher);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.testing.SubspaceSemanticsNodeInteraction onSubspaceNodeWithTag(androidx.compose.ui.test.junit4.AndroidComposeTestRule<? extends java.lang.Object?,androidx.xr.compose.testing.SubspaceTestingActivity>, String testTag);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void setSubspaceContent(androidx.compose.ui.test.junit4.AndroidComposeTestRule<? extends java.lang.Object?,androidx.xr.compose.testing.SubspaceTestingActivity>, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void setSubspaceContent(androidx.compose.ui.test.junit4.AndroidComposeTestRule<? extends java.lang.Object?,androidx.xr.compose.testing.SubspaceTestingActivity>, kotlin.jvm.functions.Function0<kotlin.Unit> uiContent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
+ public final class TestSetupKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable public static void TestSetup(optional boolean isXrEnabled, optional boolean isFullSpace, optional androidx.xr.scenecore.JxrPlatformAdapter runtime, kotlin.jvm.functions.Function1<? super androidx.xr.scenecore.Session,kotlin.Unit> content);
+ }
+
+}
+
diff --git a/xr/compose/compose-testing/build.gradle b/xr/compose/compose-testing/build.gradle
new file mode 100644
index 0000000..313f888
--- /dev/null
+++ b/xr/compose/compose-testing/build.gradle
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+
+import androidx.build.LibraryType
+import androidx.build.KotlinTarget
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("AndroidXComposePlugin")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ api(project(":xr:compose:compose"))
+ api(project(":xr:runtime:runtime"))
+ api(project(":xr:scenecore:scenecore"))
+ api(project(":xr:scenecore:scenecore-testing"))
+
+ implementation(libs.mockitoKotlin)
+ implementation(libs.robolectric)
+ implementation(libs.testExtTruth)
+ implementation("androidx.annotation:annotation:1.8.1")
+ implementation("androidx.compose.runtime:runtime:1.2.1")
+ implementation("androidx.compose.ui:ui:1.2.1")
+ implementation("androidx.compose.ui:ui-unit:1.2.1")
+ implementation("androidx.compose.ui:ui-util:1.2.1")
+ implementation("androidx.compose.ui:ui-test-junit4:1.2.1")
+ implementation("com.google.ar:impress:0.0.1")
+}
+
+android {
+ defaultConfig {
+ // TODO: This should be lower, possibly 21.
+ // Address API calls that require higher versions.
+ minSdkVersion 30
+ }
+ namespace = "androidx.xr.compose.testing"
+}
+
+androidx {
+ name = "XR Compose Testing"
+ type = LibraryType.PUBLISHED_KOTLIN_ONLY_TEST_LIBRARY
+ inceptionYear = "2024"
+ description = "Libraries to aid in unit testing Compose for XR clients."
+ kotlinTarget = KotlinTarget.KOTLIN_1_9
+ // TODO: b/326456246
+ optOutJSpecify = true
+}
diff --git a/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceAssertions.kt b/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceAssertions.kt
new file mode 100644
index 0000000..39ca6de
--- /dev/null
+++ b/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceAssertions.kt
@@ -0,0 +1,507 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.testing
+
+import android.content.res.Resources
+import androidx.annotation.RestrictTo
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.isUnspecified
+import androidx.xr.compose.unit.DpVolumeSize
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Vector3
+import com.google.errorprone.annotations.CanIgnoreReturnValue
+import kotlin.math.abs
+
+/**
+ * Asserts that the layout of this node has width equal to [expectedWidth].
+ *
+ * @param expectedWidth The width to assert.
+ * @throws AssertionError if comparison fails.
+ */
+@CanIgnoreReturnValue
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.assertWidthIsEqualTo(
+ expectedWidth: Dp
+): SubspaceSemanticsNodeInteraction {
+ return withSize { it.width.assertIsEqualTo(expectedWidth, "width") }
+}
+
+/**
+ * Asserts that the layout of this node has height equal to [expectedHeight].
+ *
+ * @param expectedHeight The height to assert.
+ * @throws AssertionError if comparison fails.
+ */
+@CanIgnoreReturnValue
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.assertHeightIsEqualTo(
+ expectedHeight: Dp
+): SubspaceSemanticsNodeInteraction {
+ return withSize { it.height.assertIsEqualTo(expectedHeight, "height") }
+}
+
+/**
+ * Asserts that the layout of this node has depth equal to [expectedDepth].
+ *
+ * @param expectedDepth The depth to assert.
+ * @throws AssertionError if comparison fails.
+ */
+@CanIgnoreReturnValue
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.assertDepthIsEqualTo(
+ expectedDepth: Dp
+): SubspaceSemanticsNodeInteraction {
+ return withSize { it.depth.assertIsEqualTo(expectedDepth, "depth") }
+}
+
+/**
+ * Asserts that the layout of this node has width that is greater than or equal to
+ * [expectedMinWidth].
+ *
+ * @param expectedMinWidth The minimum width to assert.
+ * @throws AssertionError if comparison fails.
+ */
+@CanIgnoreReturnValue
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.assertWidthIsAtLeast(
+ expectedMinWidth: Dp
+): SubspaceSemanticsNodeInteraction {
+ return withSize { it.width.assertIsAtLeast(expectedMinWidth, "width") }
+}
+
+/**
+ * Asserts that the layout of this node has height that is greater than or equal to
+ * [expectedMinHeight].
+ *
+ * @param expectedMinHeight The minimum height to assert.
+ * @throws AssertionError if comparison fails.
+ */
+@CanIgnoreReturnValue
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.assertHeightIsAtLeast(
+ expectedMinHeight: Dp
+): SubspaceSemanticsNodeInteraction {
+ return withSize { it.height.assertIsAtLeast(expectedMinHeight, "height") }
+}
+
+/**
+ * Asserts that the layout of this node has depth that is greater than or equal to
+ * [expectedMinDepth].
+ *
+ * @param expectedMinDepth The minimum depth to assert.
+ * @throws AssertionError if comparison fails.
+ */
+@CanIgnoreReturnValue
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.assertDepthIsAtLeast(
+ expectedMinDepth: Dp
+): SubspaceSemanticsNodeInteraction {
+ return withSize { it.depth.assertIsAtLeast(expectedMinDepth, "depth") }
+}
+
+/**
+ * Asserts that the layout of this node has position in the root composable that is equal to the
+ * given position.
+ *
+ * @param expectedX The x position to assert.
+ * @param expectedY The y position to assert.
+ * @param expectedZ The z position to assert.
+ * @throws AssertionError if comparison fails.
+ */
+@CanIgnoreReturnValue
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.assertPositionInRootIsEqualTo(
+ expectedX: Dp,
+ expectedY: Dp,
+ expectedZ: Dp,
+): SubspaceSemanticsNodeInteraction {
+
+ return withPositionInRoot {
+ it.x.toDp().assertIsEqualTo(expectedX, "x")
+ it.y.toDp().assertIsEqualTo(expectedY, "y")
+ it.z.toDp().assertIsEqualTo(expectedZ, "z")
+ }
+}
+
+/**
+ * Asserts that the layout of this node has the x position in the root composable that is equal to
+ * the given position.
+ *
+ * @param expectedX The x position to assert.
+ * @throws AssertionError if comparison fails.
+ */
+@CanIgnoreReturnValue
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.assertXPositionInRootIsEqualTo(
+ expectedX: Dp
+): SubspaceSemanticsNodeInteraction {
+ return withPositionInRoot { it.x.toDp().assertIsEqualTo(expectedX, "x") }
+}
+
+/**
+ * Asserts that the layout of this node has the left position in the root composable that is equal
+ * to the given position.
+ *
+ * @param expectedLeft The left position to assert.
+ * @throws AssertionError if comparison fails.
+ */
+@CanIgnoreReturnValue
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.assertLeftPositionInRootIsEqualTo(
+ expectedLeft: Dp
+): SubspaceSemanticsNodeInteraction {
+ val node = fetchSemanticsNode("Failed to retrieve the node.")
+ (node.positionInRoot.x.toDp() - node.size.width.toDp() / 2.0f).assertIsEqualTo(
+ expectedLeft,
+ "left",
+ )
+ return this
+}
+
+/**
+ * Asserts that the layout of this node has the y position in the root composable that is equal to
+ * the given position.
+ *
+ * @param expectedY The y position to assert.
+ * @throws AssertionError if comparison fails.
+ */
+@CanIgnoreReturnValue
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.assertYPositionInRootIsEqualTo(
+ expectedY: Dp
+): SubspaceSemanticsNodeInteraction {
+ return withPositionInRoot { it.y.toDp().assertIsEqualTo(expectedY, "y") }
+}
+
+/**
+ * Asserts that the layout of this node has the top position in the root composable that is equal to
+ * the given position.
+ *
+ * @param expectedTop The top position to assert.
+ * @throws AssertionError if comparison fails.
+ */
+@CanIgnoreReturnValue
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.assertTopPositionInRootIsEqualTo(
+ expectedTop: Dp
+): SubspaceSemanticsNodeInteraction {
+ val node = fetchSemanticsNode("Failed to retrieve the node.")
+ (node.positionInRoot.y.toDp() + node.size.height.toDp() / 2.0f).assertIsEqualTo(
+ expectedTop,
+ "top",
+ )
+ return this
+}
+
+/**
+ * Asserts that the layout of this node has the z position in the root composable that is equal to
+ * the given position.
+ *
+ * @param expectedZ The z position to assert.
+ * @throws AssertionError if comparison fails.
+ */
+@CanIgnoreReturnValue
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.assertZPositionInRootIsEqualTo(
+ expectedZ: Dp
+): SubspaceSemanticsNodeInteraction {
+ return withPositionInRoot { it.z.toDp().assertIsEqualTo(expectedZ, "z") }
+}
+
+/**
+ * Asserts that the layout of this node has position that is equal to the given position.
+ *
+ * @param expectedX The x position to assert.
+ * @param expectedY The y position to assert.
+ * @param expectedZ The z position to assert.
+ * @throws AssertionError if comparison fails.
+ */
+@CanIgnoreReturnValue
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.assertPositionIsEqualTo(
+ expectedX: Dp,
+ expectedY: Dp,
+ expectedZ: Dp,
+): SubspaceSemanticsNodeInteraction {
+
+ return withPosition {
+ it.x.toDp().assertIsEqualTo(expectedX, "x")
+ it.y.toDp().assertIsEqualTo(expectedY, "y")
+ it.z.toDp().assertIsEqualTo(expectedZ, "z")
+ }
+}
+
+/**
+ * Asserts that the layout of this node has the x position that is equal to the given position.
+ *
+ * @param expectedX The x position to assert.
+ * @throws AssertionError if comparison fails.
+ */
+@CanIgnoreReturnValue
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.assertXPositionIsEqualTo(
+ expectedX: Dp
+): SubspaceSemanticsNodeInteraction {
+ return withPosition { it.x.toDp().assertIsEqualTo(expectedX, "x") }
+}
+
+/**
+ * Asserts that the layout of this node has the y position that is equal to the given position.
+ *
+ * @param expectedY The y position to assert.
+ * @throws AssertionError if comparison fails.
+ */
+@CanIgnoreReturnValue
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.assertYPositionIsEqualTo(
+ expectedY: Dp
+): SubspaceSemanticsNodeInteraction {
+ return withPosition { it.y.toDp().assertIsEqualTo(expectedY, "y") }
+}
+
+/**
+ * Asserts that the layout of this node has the z position that is equal to the given position.
+ *
+ * @param expectedZ The z position to assert.
+ * @throws AssertionError if comparison fails.
+ */
+@CanIgnoreReturnValue
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.assertZPositionIsEqualTo(
+ expectedZ: Dp
+): SubspaceSemanticsNodeInteraction {
+ return withPosition { it.z.toDp().assertIsEqualTo(expectedZ, "z") }
+}
+
+/**
+ * Asserts that the layout of this node has rotation in the root composable that is equal to the
+ * given rotation.
+ *
+ * @param expected The rotation to assert.
+ * @throws AssertionError if comparison fails.
+ */
+@CanIgnoreReturnValue
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.assertRotationInRootIsEqualTo(
+ expected: Quaternion
+): SubspaceSemanticsNodeInteraction {
+
+ val makeError = { subject: String, exp: Float, actual: Float ->
+ "Actual $subject is $actual: expected $exp"
+ }
+
+ return withRotationInRoot {
+ check(it.x.equals(expected.x)) { makeError.invoke("x", expected.x, it.x) }
+ check(it.y.equals(expected.y)) { makeError.invoke("y", expected.y, it.y) }
+ check(it.z.equals(expected.z)) { makeError.invoke("z", expected.z, it.z) }
+ check(it.w.equals(expected.w)) { makeError.invoke("w", expected.w, it.w) }
+ }
+}
+
+/**
+ * Asserts that the layout of this node has rotation that is equal to the given rotation.
+ *
+ * @param expected The rotation to assert.
+ * @throws AssertionError if comparison fails.
+ */
+@CanIgnoreReturnValue
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.assertRotationIsEqualTo(
+ expected: Quaternion
+): SubspaceSemanticsNodeInteraction {
+
+ return withRotation {
+ check(it.x.equals(expected.x)) { "Actual x is ${it.x}: expected ${expected.x}" }
+ check(it.y.equals(expected.y)) { "Actual y is ${it.y}: expected ${expected.y}" }
+ check(it.z.equals(expected.z)) { "Actual z is ${it.z}: expected ${expected.z}" }
+ check(it.w.equals(expected.w)) { "Actual w is ${it.w}: expected ${expected.w}" }
+ }
+}
+
+/**
+ * Returns the size of the node.
+ *
+ * Additional assertions with custom tolerances may be performed on the individual values.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.getSize(): DpVolumeSize {
+ lateinit var size: DpVolumeSize
+ withSize { size = it }
+ return size
+}
+
+/**
+ * Returns the position of the node relative to its parent layout node.
+ *
+ * Additional assertions with custom tolerances may be performed on the individual values.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.getPosition(): Vector3 {
+ lateinit var position: Vector3
+ withPosition { position = it }
+ return position
+}
+
+/**
+ * Returns the position of the node relative to the root node.
+ *
+ * Additional assertions with custom tolerances may be performed on the individual values.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.getPositionInRoot(): Vector3 {
+ lateinit var position: Vector3
+ withPositionInRoot { position = it }
+ return position
+}
+
+/**
+ * Returns the rotation of the node relative to its parent layout node.
+ *
+ * Additional assertions with custom tolerances may be performed on the individual values.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.getRotation(): Quaternion {
+ lateinit var rotation: Quaternion
+ withRotation { rotation = it }
+ return rotation
+}
+
+/**
+ * Returns the rotation of the node relative to the root node.
+ *
+ * Additional assertions with custom tolerances may be performed on the individual values.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceSemanticsNodeInteraction.getRotationInRoot(): Quaternion {
+ lateinit var rotation: Quaternion
+ withRotationInRoot { rotation = it }
+ return rotation
+}
+
+@CanIgnoreReturnValue
+private fun SubspaceSemanticsNodeInteraction.withSize(
+ assertion: (DpVolumeSize) -> Unit
+): SubspaceSemanticsNodeInteraction {
+ val node = fetchSemanticsNode("Failed to retrieve size of the node.")
+ val size = node.size.let { DpVolumeSize(it.width.toDp(), it.height.toDp(), it.depth.toDp()) }
+ assertion.invoke(size)
+ return this
+}
+
+@CanIgnoreReturnValue
+private fun SubspaceSemanticsNodeInteraction.withPosition(
+ assertion: (Vector3) -> Unit
+): SubspaceSemanticsNodeInteraction {
+ val node = fetchSemanticsNode("Failed to retrieve position of the node.")
+ assertion.invoke(node.position)
+ return this
+}
+
+@CanIgnoreReturnValue
+private fun SubspaceSemanticsNodeInteraction.withPositionInRoot(
+ assertion: (Vector3) -> Unit
+): SubspaceSemanticsNodeInteraction {
+ val node = fetchSemanticsNode("Failed to retrieve position of the node.")
+ assertion.invoke(node.positionInRoot)
+ return this
+}
+
+@CanIgnoreReturnValue
+private fun SubspaceSemanticsNodeInteraction.withRotation(
+ assertion: (Quaternion) -> Unit
+): SubspaceSemanticsNodeInteraction {
+ val node = fetchSemanticsNode("Failed to retrieve rotation of the node.")
+ assertion.invoke(node.rotation)
+ return this
+}
+
+@CanIgnoreReturnValue
+private fun SubspaceSemanticsNodeInteraction.withRotationInRoot(
+ assertion: (Quaternion) -> Unit
+): SubspaceSemanticsNodeInteraction {
+ val node = fetchSemanticsNode("Failed to retrieve rotation of the node.")
+ assertion.invoke(node.rotationInRoot)
+ return this
+}
+
+/**
+ * Returns if this value is equal to the [reference], within a given [tolerance]. If the reference
+ * value is [Float.NaN], [Float.POSITIVE_INFINITY] or [Float.NEGATIVE_INFINITY], this only returns
+ * true if this value is exactly the same (tolerance is disregarded).
+ */
+private fun Dp.isWithinTolerance(reference: Dp, tolerance: Dp): Boolean {
+ return when {
+ reference.isUnspecified -> this.isUnspecified
+ reference.value.isInfinite() -> this.value == reference.value
+ else -> abs(this.value - reference.value) <= tolerance.value
+ }
+}
+
+/**
+ * Asserts that this value is equal to the given [expected] value.
+ *
+ * Performs the comparison with the given [tolerance] or the default one if none is provided. It is
+ * recommended to use tolerance when comparing positions and size coming from the framework as there
+ * can be rounding operation performed by individual layouts so the values can be slightly off from
+ * the expected ones.
+ *
+ * @param expected The expected value to which this one should be equal to.
+ * @param subject Used in the error message to identify which item this assertion failed on.
+ * @param tolerance The tolerance within which the values should be treated as equal.
+ * @throws AssertionError if comparison fails.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun Dp.assertIsEqualTo(expected: Dp, subject: String, tolerance: Dp = Dp(.5f)) {
+ if (!isWithinTolerance(expected, tolerance)) {
+ // Comparison failed, report the error in DPs
+ throw AssertionError("Actual $subject is $this, expected $expected (tolerance: $tolerance)")
+ }
+}
+
+/**
+ * Asserts that this value is greater than or equal to the given [expected] value.
+ *
+ * Performs the comparison with the given [tolerance] or the default one if none is provided. It is
+ * recommended to use tolerance when comparing positions and size coming from the framework as there
+ * can be rounding operation performed by individual layouts so the values can be slightly off from
+ * the expected ones.
+ *
+ * @param expected The expected value to which this one should be greater than or equal to.
+ * @param subject Used in the error message to identify which item this assertion failed on.
+ * @param tolerance The tolerance within which the values should be treated as equal.
+ * @throws AssertionError if comparison fails.
+ */
+private fun Dp.assertIsAtLeast(expected: Dp, subject: String, tolerance: Dp = Dp(.5f)) {
+ if (!(isWithinTolerance(expected, tolerance) || (!isUnspecified && this > expected))) {
+ // Comparison failed, report the error in DPs
+ throw AssertionError(
+ "Actual $subject is $this, expected at least $expected (tolerance: $tolerance)"
+ )
+ }
+}
+
+/** Converts a float to a [Dp] value. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun Float.toDp(): Dp {
+ return Dp(this / Resources.getSystem().displayMetrics.density)
+}
+
+/** Converts an integer to a [Dp] value. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun Int.toDp(): Dp {
+ return Dp(this.toFloat() / Resources.getSystem().displayMetrics.density)
+}
diff --git a/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceFilters.kt b/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceFilters.kt
new file mode 100644
index 0000000..c8971c5
--- /dev/null
+++ b/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceFilters.kt
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.testing
+
+import androidx.annotation.RestrictTo
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.SemanticsPropertyKey
+import androidx.compose.ui.semantics.getOrNull
+import androidx.compose.ui.util.fastAny
+import androidx.xr.compose.subspace.node.SubspaceSemanticsNode
+
+/**
+ * Verifies that the node is focusable.
+ *
+ * @return matcher that matches the node if it is focusable.
+ * @see SemanticsProperties.Focused
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun isFocusable(): SubspaceSemanticsMatcher = hasKey(SemanticsProperties.Focused)
+
+/**
+ * Verifies that the node is not focusable.
+ *
+ * @return matcher that matches the node if it is not focusable.
+ * @see SemanticsProperties.Focused
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun isNotFocusable(): SubspaceSemanticsMatcher =
+ SubspaceSemanticsMatcher.keyNotDefined(SemanticsProperties.Focused)
+
+/**
+ * Verifies that the node is focused.
+ *
+ * @return matcher that matches the node if it is focused.
+ * @see SemanticsProperties.Focused
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun isFocused(): SubspaceSemanticsMatcher =
+ SubspaceSemanticsMatcher.expectValue(SemanticsProperties.Focused, true)
+
+/**
+ * Verifies that the node is not focused.
+ *
+ * @return matcher that matches the node if it is not focused.
+ * @see SemanticsProperties.Focused
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun isNotFocused(): SubspaceSemanticsMatcher =
+ SubspaceSemanticsMatcher.expectValue(SemanticsProperties.Focused, false)
+
+/**
+ * Verifies the node's content description.
+ *
+ * @param value Value to match as one of the items in the list of content descriptions.
+ * @param substring Whether to use substring matching.
+ * @param ignoreCase Whether case should be ignored.
+ * @return true if the node's content description contains the given [value].
+ * @see SemanticsProperties.ContentDescription
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun hasContentDescription(
+ value: String,
+ substring: Boolean = false,
+ ignoreCase: Boolean = false,
+): SubspaceSemanticsMatcher {
+ return if (substring) {
+ SubspaceSemanticsMatcher(
+ "${SemanticsProperties.ContentDescription.name} contains '$value' " +
+ "(ignoreCase: $ignoreCase)"
+ ) {
+ it.config.getOrNull(SemanticsProperties.ContentDescription)?.any { item ->
+ item.contains(value, ignoreCase)
+ } ?: false
+ }
+ } else {
+ SubspaceSemanticsMatcher(
+ "${SemanticsProperties.ContentDescription.name} = '$value' (ignoreCase: $ignoreCase)"
+ ) {
+ it.config.getOrNull(SemanticsProperties.ContentDescription)?.any { item ->
+ item.equals(value, ignoreCase)
+ } ?: false
+ }
+ }
+}
+
+/**
+ * Verifies the node's test tag.
+ *
+ * @param testTag Value to match.
+ * @return true if the node is annotated by the given test tag.
+ * @see SemanticsProperties.TestTag
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun hasTestTag(testTag: String): SubspaceSemanticsMatcher =
+ SubspaceSemanticsMatcher.expectValue(SemanticsProperties.TestTag, testTag)
+
+/**
+ * Verifies that the node is the root semantics node.
+ *
+ * There is always one root in every node tree, added implicitly by Compose.
+ *
+ * @return true if the node is the root semantics node.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun isRoot(): SubspaceSemanticsMatcher = SubspaceSemanticsMatcher("isRoot") { it.isRoot }
+
+/**
+ * Verifies the node's parent.
+ *
+ * @param matcher The matcher to use to check the parent.
+ * @return true if the node's parent satisfies the given matcher.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun hasParent(matcher: SubspaceSemanticsMatcher): SubspaceSemanticsMatcher {
+ return SubspaceSemanticsMatcher("hasParentThat(${matcher.description})") {
+ it.parent?.run { matcher.matches(this) } ?: false
+ }
+}
+
+/**
+ * Verifies the node's children.
+ *
+ * @param matcher The matcher to use to check the children.
+ * @return true if the node has at least one child that satisfies the given matcher.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun hasAnyChild(matcher: SubspaceSemanticsMatcher): SubspaceSemanticsMatcher {
+ return SubspaceSemanticsMatcher("hasAnyChildThat(${matcher.description})") {
+ matcher.matchesAny(it.children)
+ }
+}
+
+/**
+ * Verifies the node's siblings.
+ *
+ * @param matcher The matcher to use to check the siblings. Sibling is defined as a any other node
+ * that shares the same parent.
+ * @return true if the node has at least one sibling that satisfies the given matcher.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun hasAnySibling(matcher: SubspaceSemanticsMatcher): SubspaceSemanticsMatcher {
+ return SubspaceSemanticsMatcher("hasAnySiblingThat(${matcher.description})") {
+ val node = it
+ it.parent?.run { matcher.matchesAny(this.children.filter { child -> child.id != node.id }) }
+ ?: false
+ }
+}
+
+/**
+ * Verifies the node's ancestors.
+ *
+ * @param matcher The matcher to use to check the ancestors. Example: For the following tree
+ *
+ * ```
+ * |-X
+ * |-A
+ * |-B
+ * |-C1
+ * |-C2
+ * ```
+ *
+ * In case of C1, we would check the matcher against A and B.
+ *
+ * @return true if the node has at least one ancestor that satisfies the given matcher.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun hasAnyAncestor(matcher: SubspaceSemanticsMatcher): SubspaceSemanticsMatcher {
+ return SubspaceSemanticsMatcher("hasAnyAncestorThat(${matcher.description})") {
+ matcher.matchesAny(it.ancestors)
+ }
+}
+
+/**
+ * Verifies the node's descendants.
+ *
+ * @param matcher The matcher to use to check the descendants. Example: For the following tree
+ *
+ * ```
+ * |-X
+ * |-A
+ * |-B
+ * |-C1
+ * |-C2
+ * ```
+ *
+ * In case of A, we would check the matcher against B, C1 and C2.
+ *
+ * @return true if the node has at least one descendant that satisfies the given matcher.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun hasAnyDescendant(matcher: SubspaceSemanticsMatcher): SubspaceSemanticsMatcher {
+ fun checkIfSubtreeMatches(
+ matcher: SubspaceSemanticsMatcher,
+ node: SubspaceSemanticsNode,
+ ): Boolean {
+ if (matcher.matchesAny(node.children)) {
+ return true
+ }
+
+ return node.children.fastAny { checkIfSubtreeMatches(matcher, it) }
+ }
+
+ return SubspaceSemanticsMatcher("hasAnyDescendantThat(${matcher.description})") {
+ checkIfSubtreeMatches(matcher, it)
+ }
+}
+
+private val SubspaceSemanticsNode.ancestors: Iterable<SubspaceSemanticsNode>
+ get() =
+ object : Iterable<SubspaceSemanticsNode> {
+ override fun iterator(): Iterator<SubspaceSemanticsNode> {
+ return object : Iterator<SubspaceSemanticsNode> {
+ var next = parent
+
+ override fun hasNext(): Boolean {
+ return next != null
+ }
+
+ override fun next(): SubspaceSemanticsNode {
+ return next!!.also { next = it.parent }
+ }
+ }
+ }
+ }
+
+private fun hasKey(key: SemanticsPropertyKey<*>): SubspaceSemanticsMatcher =
+ SubspaceSemanticsMatcher.keyIsDefined(key)
diff --git a/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceSemanticsMatcher.kt b/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceSemanticsMatcher.kt
new file mode 100644
index 0000000..4c4c73a
--- /dev/null
+++ b/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceSemanticsMatcher.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.testing
+
+import androidx.annotation.RestrictTo
+import androidx.compose.ui.semantics.SemanticsPropertyKey
+import androidx.xr.compose.subspace.node.SubspaceSemanticsNode
+
+/**
+ * Wrapper for semantics matcher lambdas that allows to build string explaining to the developer
+ * what conditions were being tested.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SubspaceSemanticsMatcher(
+ internal val description: String,
+ private val matcher: (SubspaceSemanticsNode) -> Boolean,
+) {
+
+ internal companion object {
+ /**
+ * Builds a predicate that tests whether the value of the given [key] is equal to
+ * [expectedValue].
+ */
+ internal fun <T> expectValue(
+ key: SemanticsPropertyKey<T>,
+ expectedValue: T,
+ ): SubspaceSemanticsMatcher {
+ return SubspaceSemanticsMatcher("${key.name} = '$expectedValue'") {
+ it.config.getOrElseNullable(key) { null } == expectedValue
+ }
+ }
+
+ /** Builds a predicate that tests whether the given [key] is defined in semantics. */
+ internal fun <T> keyIsDefined(key: SemanticsPropertyKey<T>): SubspaceSemanticsMatcher {
+ return SubspaceSemanticsMatcher("${key.name} is defined") { key in it.config }
+ }
+
+ /** Builds a predicate that tests whether the given [key] is NOT defined in semantics. */
+ internal fun <T> keyNotDefined(key: SemanticsPropertyKey<T>): SubspaceSemanticsMatcher {
+ return SubspaceSemanticsMatcher("${key.name} is NOT defined") { key !in it.config }
+ }
+ }
+
+ /** Returns whether the given node is matched by this matcher. */
+ internal fun matches(node: SubspaceSemanticsNode): Boolean {
+ return matcher(node)
+ }
+
+ /** Returns whether at least one of the given nodes is matched by this matcher. */
+ internal fun matchesAny(nodes: Iterable<SubspaceSemanticsNode>): Boolean {
+ return nodes.any(matcher)
+ }
+
+ internal infix fun and(other: SubspaceSemanticsMatcher): SubspaceSemanticsMatcher {
+ return SubspaceSemanticsMatcher("($description) && (${other.description})") {
+ matcher(it) && other.matches(it)
+ }
+ }
+
+ internal infix fun or(other: SubspaceSemanticsMatcher): SubspaceSemanticsMatcher {
+ return SubspaceSemanticsMatcher("($description) || (${other.description})") {
+ matcher(it) || other.matches(it)
+ }
+ }
+
+ internal operator fun not(): SubspaceSemanticsMatcher {
+ return SubspaceSemanticsMatcher("NOT ($description)") { !matcher(it) }
+ }
+}
diff --git a/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceSemanticsNodeInteraction.kt b/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceSemanticsNodeInteraction.kt
new file mode 100644
index 0000000..cdad0f4
--- /dev/null
+++ b/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceSemanticsNodeInteraction.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.testing
+
+import androidx.annotation.RestrictTo
+import androidx.xr.compose.subspace.node.SubspaceSemanticsNode
+import com.google.errorprone.annotations.CanIgnoreReturnValue
+
+/**
+ * Represents a semantics node and the path to fetch it from the semantics tree. One can perform
+ * assertions or navigate to other nodes such as [onChildren].
+ *
+ * An instance of [SubspaceSemanticsNodeInteraction] can be obtained from [onSubspaceNode] and
+ * convenience methods that use a specific filter.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SubspaceSemanticsNodeInteraction
+internal constructor(
+ private val testContext: SubspaceTestContext,
+ private val selector: SubspaceSemanticsSelector,
+) {
+ public constructor(
+ testContext: SubspaceTestContext,
+ matcher: SubspaceSemanticsMatcher,
+ ) : this(testContext, SubspaceSemanticsSelector(matcher))
+
+ private fun fetchSemanticsNodes(
+ atLeastOneRootRequired: Boolean,
+ errorMessageOnFail: String? = null,
+ ): SubspaceSelectionResult {
+ return selector.map(
+ testContext.getAllSemanticsNodes(atLeastOneRootRequired = atLeastOneRootRequired),
+ errorMessageOnFail.orEmpty(),
+ )
+ }
+
+ /**
+ * Returns the semantics node captured by this object.
+ *
+ * Note: Accessing this object involves synchronization with your UI. If you are accessing this
+ * multiple times in one atomic operation, it is better to cache the result instead of calling
+ * this API multiple times.
+ *
+ * This will fail if there is 0 or multiple nodes matching.
+ *
+ * @param errorMessageOnFail Error message prefix to be added to the message in case this fetch
+ * fails. This is typically used by operations that rely on this assert. Example prefix could
+ * be: "Failed to perform doOnClick.".
+ * @throws [AssertionError] if 0 or multiple nodes found.
+ */
+ public fun fetchSemanticsNode(errorMessageOnFail: String? = null): SubspaceSemanticsNode {
+ return fetchOneOrThrow(errorMessageOnFail)
+ }
+
+ /**
+ * Asserts that no item was found or that the item is no longer in the hierarchy.
+ *
+ * This will synchronize with the UI and fetch all the nodes again to ensure it has latest data.
+ *
+ * @throws [AssertionError] if the assert fails.
+ */
+ public fun assertDoesNotExist() {
+ val result =
+ fetchSemanticsNodes(
+ atLeastOneRootRequired = false,
+ errorMessageOnFail = "Failed: assertDoesNotExist.",
+ )
+ if (result.selectedNodes.isNotEmpty()) {
+ throw AssertionError(
+ "Failed: assertDoesNotExist. Expected 0 but found ${result.selectedNodes.size} nodes."
+ )
+ }
+ }
+
+ /**
+ * Asserts that the component was found and is part of the component tree.
+ *
+ * This will synchronize with the UI and fetch all the nodes again to ensure it has latest data.
+ * If you are using [fetchSemanticsNode] you don't need to call this. In fact you would just
+ * introduce additional overhead.
+ *
+ * @param errorMessageOnFail Error message prefix to be added to the message in case this assert
+ * fails. This is typically used by operations that rely on this assert. Example prefix could
+ * be: "Failed to perform doOnClick.".
+ * @throws [AssertionError] if the assert fails.
+ */
+ @CanIgnoreReturnValue
+ public fun assertExists(errorMessageOnFail: String? = null): SubspaceSemanticsNodeInteraction {
+ fetchOneOrThrow(errorMessageOnFail)
+ return this
+ }
+
+ @CanIgnoreReturnValue
+ private fun fetchOneOrThrow(errorMessageOnFail: String? = null): SubspaceSemanticsNode {
+ val finalErrorMessage = errorMessageOnFail ?: "Failed: assertExists."
+
+ val result =
+ fetchSemanticsNodes(
+ atLeastOneRootRequired = true,
+ errorMessageOnFail = finalErrorMessage
+ )
+ if (result.selectedNodes.count() != 1) {
+ if (result.customErrorOnNoMatch != null) {
+ throw AssertionError(finalErrorMessage + "\n" + result.customErrorOnNoMatch)
+ }
+
+ throw AssertionError(finalErrorMessage)
+ }
+
+ return result.selectedNodes.first()
+ }
+}
+
+/**
+ * Represents a collection of semantics nodes and the path to fetch them from the semantics tree.
+ * One can interact with these nodes by performing assertions such as [assertCountEquals], or
+ * navigate to other nodes such as [get].
+ *
+ * An instance of [SubspaceSemanticsNodeInteractionCollection] can be obtained from
+ * [onAllSubspaceNodes] and convenience methods that use a specific filter, such as
+ * [onAllNodesWithText].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SubspaceSemanticsNodeInteractionCollection
+private constructor(
+ internal val testContext: SubspaceTestContext,
+ internal val selector: SubspaceSemanticsSelector,
+) {
+ @Suppress("PrimitiveInCollection") private var nodeIds: List<Int>? = null
+
+ public constructor(
+ testContext: SubspaceTestContext,
+ matcher: SubspaceSemanticsMatcher,
+ ) : this(testContext, SubspaceSemanticsSelector(matcher))
+
+ /**
+ * Returns the semantics nodes captured by this object.
+ *
+ * Note: Accessing this object involves synchronization with your UI. If you are accessing this
+ * multiple times in one atomic operation, it is better to cache the result instead of calling
+ * this API multiple times.
+ *
+ * @param atLeastOneRootRequired Whether to throw an error in case there is no compose content
+ * in the current test app.
+ * @param errorMessageOnFail Error message prefix to be added to the message in case this fetch
+ * fails. This is typically used by operations that rely on this assert. Example prefix could
+ * be: "Failed to perform doOnClick.".
+ */
+ private fun fetchSemanticsNodes(
+ atLeastOneRootRequired: Boolean = true,
+ errorMessageOnFail: String? = null,
+ ): List<SubspaceSemanticsNode> {
+ if (nodeIds == null) {
+ return selector
+ .map(
+ testContext.getAllSemanticsNodes(atLeastOneRootRequired),
+ errorMessageOnFail.orEmpty()
+ )
+ .apply { nodeIds = selectedNodes.map { it.id }.toList() }
+ .selectedNodes
+ }
+
+ return testContext.getAllSemanticsNodes(atLeastOneRootRequired).filter {
+ it.id in nodeIds!!
+ }
+ }
+
+ /**
+ * Retrieve node at the given index of this collection.
+ *
+ * Any subsequent operation on its result will expect exactly one element found (unless
+ * [SubspaceSemanticsNodeInteraction.assertDoesNotExist] is used) and will throw
+ * [AssertionError] if none or more than one element is found.
+ */
+ private operator fun get(index: Int): SubspaceSemanticsNodeInteraction {
+ return SubspaceSemanticsNodeInteraction(testContext, selector.addIndexSelector(index))
+ }
+}
diff --git a/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceSemanticsSelector.kt b/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceSemanticsSelector.kt
new file mode 100644
index 0000000..891d0a8d
--- /dev/null
+++ b/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceSemanticsSelector.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.testing
+
+import androidx.xr.compose.subspace.node.SubspaceSemanticsNode
+
+/**
+ * Projects the given set of nodes to a new set of nodes.
+ *
+ * @param description Description that is displayed to the developer in error outputs.
+ * @param requiresExactlyOneNode Whether this selector should expect to receive exactly 1 node.
+ * @param chainedInputSelector Optional selector to apply before this selector gets applied.
+ * @param selector The lambda that implements the projection.
+ */
+internal class SubspaceSemanticsSelector(
+ internal val description: String,
+ private val requiresExactlyOneNode: Boolean,
+ private val chainedInputSelector: SubspaceSemanticsSelector? = null,
+ private val selector: (Iterable<SubspaceSemanticsNode>) -> SubspaceSelectionResult,
+) {
+
+ /**
+ * Maps the given list of nodes to a new list of nodes.
+ *
+ * @throws AssertionError if required prerequisites to perform the selection were not satisfied.
+ */
+ internal fun map(
+ nodes: Iterable<SubspaceSemanticsNode>,
+ errorOnFail: String,
+ ): SubspaceSelectionResult {
+ val chainedResult = chainedInputSelector?.map(nodes, errorOnFail)
+ val inputNodes = chainedResult?.selectedNodes ?: nodes
+ if (requiresExactlyOneNode && inputNodes.count() != 1) {
+ throw AssertionError(
+ chainedResult?.customErrorOnNoMatch
+ ?: "Required exactly one node but found ${inputNodes.count()} nodes."
+ )
+ }
+ return selector(inputNodes)
+ }
+}
+
+/** Creates a new [SubspaceSemanticsSelector] based on the given [SubspaceSemanticsMatcher]. */
+internal fun SubspaceSemanticsSelector(
+ matcher: SubspaceSemanticsMatcher
+): SubspaceSemanticsSelector {
+ return SubspaceSemanticsSelector(
+ matcher.description,
+ requiresExactlyOneNode = false,
+ chainedInputSelector = null,
+ ) { nodes ->
+ SubspaceSelectionResult(nodes.filter { matcher.matches(it) })
+ }
+}
+
+/**
+ * Result of [SubspaceSemanticsSelector] projection.
+ *
+ * @param selectedNodes The result nodes found.
+ * @param customErrorOnNoMatch If the projection failed to map nodes due to wrong input (e.g.
+ * selector expected only 1 node but got multiple) it will provide a custom error exactly
+ * explaining what selection was performed and what nodes it received.
+ */
+internal class SubspaceSelectionResult(
+ internal val selectedNodes: List<SubspaceSemanticsNode>,
+ internal val customErrorOnNoMatch: String? = null,
+)
+
+/**
+ * Chains the given selector to be performed after this one.
+ *
+ * The new selector will expect to receive exactly one node (otherwise will fail).
+ */
+internal fun SubspaceSemanticsSelector.addSelectionFromSingleNode(
+ description: String,
+ selector: (SubspaceSemanticsNode) -> List<SubspaceSemanticsNode>,
+): SubspaceSemanticsSelector {
+ return SubspaceSemanticsSelector(
+ "(${this.description}).$description",
+ requiresExactlyOneNode = true,
+ chainedInputSelector = this,
+ ) { nodes ->
+ SubspaceSelectionResult(selector(nodes.first()))
+ }
+}
+
+/** Chains a new selector that retrieves node from this selector at the given [index]. */
+internal fun SubspaceSemanticsSelector.addIndexSelector(index: Int): SubspaceSemanticsSelector {
+ return SubspaceSemanticsSelector(
+ "(${this.description})[$index]",
+ requiresExactlyOneNode = false,
+ chainedInputSelector = this,
+ ) { nodes ->
+ val nodesList = nodes.toList()
+ if (index >= 0 && index < nodesList.size) {
+ SubspaceSelectionResult(listOf(nodesList[index]))
+ } else {
+ val errorMessage = "Index out of bounds: $index"
+ SubspaceSelectionResult(emptyList(), errorMessage)
+ }
+ }
+}
+
+/** Chains a new selector that retrieves the last node returned from this selector. */
+internal fun SubspaceSemanticsSelector.addLastNodeSelector(): SubspaceSemanticsSelector {
+ return SubspaceSemanticsSelector(
+ "(${this.description}).last",
+ requiresExactlyOneNode = false,
+ chainedInputSelector = this,
+ ) { nodes ->
+ SubspaceSelectionResult(nodes.toList().takeLast(1))
+ }
+}
+
+/**
+ * Chains a new selector that selects all the nodes matching the given [matcher] from the nodes
+ * returned by this selector.
+ */
+internal fun SubspaceSemanticsSelector.addSelectorViaMatcher(
+ selectorName: String,
+ matcher: SubspaceSemanticsMatcher,
+): SubspaceSemanticsSelector {
+ return SubspaceSemanticsSelector(
+ "(${this.description}).$selectorName(${matcher.description})",
+ requiresExactlyOneNode = false,
+ chainedInputSelector = this,
+ ) { nodes ->
+ SubspaceSelectionResult(nodes.filter { matcher.matches(it) })
+ }
+}
diff --git a/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceTestContext.kt b/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceTestContext.kt
new file mode 100644
index 0000000..a20cf18
--- /dev/null
+++ b/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceTestContext.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.testing
+
+import androidx.annotation.RestrictTo
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.util.fastForEach
+import androidx.xr.compose.platform.SceneManager
+import androidx.xr.compose.subspace.node.SubspaceSemanticsNode
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SubspaceTestContext(private val testRule: AndroidComposeTestRule<*, *>) {
+ /**
+ * Collects all [SubspaceSemanticsNode]s from all compose hierarchies.
+ *
+ * Can crash in case it hits time out. This is not supposed to be handled as it surfaces only in
+ * incorrect tests.
+ */
+ internal fun getAllSemanticsNodes(
+ atLeastOneRootRequired: Boolean
+ ): Iterable<SubspaceSemanticsNode> {
+ // Block and wait for compose state to settle before looking for root nodes.
+ testRule.waitForIdle()
+ val roots = SceneManager.getAllRootSubspaceSemanticsNodes()
+ check(!atLeastOneRootRequired || roots.isNotEmpty()) {
+ """No subspace compose hierarchies found in the app. Possible reasons include:
+ (1) the Activity that calls setSubspaceContent did not launch;
+ (2) setSubspaceContent was not called;
+ (3) setSubspaceContent was called before the ComposeTestRule ran;
+ (4) a Subspace was not used in setContent
+ If setSubspaceContent is called by the Activity, make sure the Activity is
+ launched after the ComposeTestRule runs"""
+ }
+
+ return roots.flatMap { it.getAllSemanticsNodes() }
+ }
+}
+
+private fun SubspaceSemanticsNode.getAllSemanticsNodes(): Iterable<SubspaceSemanticsNode> {
+ val nodes = mutableListOf<SubspaceSemanticsNode>()
+
+ fun findAllSemanticNodesRecursive(currentNode: SubspaceSemanticsNode) {
+ nodes.add(currentNode)
+ currentNode.children.fastForEach { child -> findAllSemanticNodesRecursive(child) }
+ }
+
+ findAllSemanticNodesRecursive(this)
+
+ return nodes
+}
diff --git a/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceTestingActivity.kt b/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceTestingActivity.kt
new file mode 100644
index 0000000..6704c65
--- /dev/null
+++ b/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/SubspaceTestingActivity.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.testing
+
+import android.view.Display
+import androidx.activity.ComponentActivity
+import androidx.annotation.NonNull
+import androidx.annotation.RestrictTo
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.xr.compose.platform.SceneManager
+import androidx.xr.compose.platform.setSubspaceContent
+import androidx.xr.compose.subspace.SubspaceComposable
+import androidx.xr.scenecore.JxrPlatformAdapter
+import androidx.xr.scenecore.Session
+import androidx.xr.scenecore.impl.JxrPlatformAdapterAxr
+import androidx.xr.scenecore.impl.perception.PerceptionLibrary
+import androidx.xr.scenecore.testing.FakeImpressApi
+import androidx.xr.scenecore.testing.FakeScheduledExecutorService
+import androidx.xr.scenecore.testing.FakeXrExtensions
+import com.google.androidxr.splitengine.SplitEngineSubspaceManager
+import com.google.ar.imp.view.splitengine.ImpSplitEngineRenderer
+import org.mockito.Mockito.mock
+import org.robolectric.shadows.ShadowDisplay
+
+/** Custom test class that should be used for testing [SubspaceComposable] content. */
+@Suppress("ForbiddenSuperClass")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SubspaceTestingActivity : ComponentActivity() {
+ public val extensions: FakeXrExtensions = FakeXrExtensions()
+ public val session: Session by lazy { createFakeSession(this) }
+
+ /** Throws an exception by default under test; return Robolectric Display impl instead. */
+ @NonNull override fun getDisplay(): Display = ShadowDisplay.getDefaultDisplay()
+
+ override fun onStart() {
+ SceneManager.start()
+ super.onStart()
+ }
+
+ override fun onDestroy() {
+ SceneManager.stop()
+ super.onDestroy()
+ }
+}
+
+/** Analog to [AndroidComposeTestRule.setContent] for testing [SubspaceComposable] content. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun AndroidComposeTestRule<*, SubspaceTestingActivity>.setSubspaceContent(
+ content: @Composable @SubspaceComposable () -> Unit
+) {
+ setContent {} // Necessary to avoid crashes, as ComposeTestRule expects a call to setContent {}
+ activity.setSubspaceContent(
+ session = activity.session,
+ enableXrForTesting = true,
+ content = content,
+ )
+}
+
+/** Analog to [AndroidComposeTestRule.setContent] for testing [SubspaceComposable] content. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun AndroidComposeTestRule<*, SubspaceTestingActivity>.setSubspaceContent(
+ uiContent: @Composable () -> Unit,
+ content: @Composable @SubspaceComposable () -> Unit,
+) {
+ setContent(uiContent)
+ activity.setSubspaceContent(
+ session = activity.session,
+ enableXrForTesting = true,
+ content = content,
+ )
+}
+
+/** Subspace version of onNode in Compose. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun AndroidComposeTestRule<*, SubspaceTestingActivity>.onSubspaceNode(
+ matcher: SubspaceSemanticsMatcher
+): SubspaceSemanticsNodeInteraction =
+ SubspaceSemanticsNodeInteraction(SubspaceTestContext(this), matcher)
+
+/** Subspace version of onAllNodes in Compose. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun AndroidComposeTestRule<*, SubspaceTestingActivity>.onAllSubspaceNodes(
+ matcher: SubspaceSemanticsMatcher
+): SubspaceSemanticsNodeInteractionCollection =
+ SubspaceSemanticsNodeInteractionCollection(SubspaceTestContext(this), matcher)
+
+/** Subspace version of onNodeWithTag in Compose. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun AndroidComposeTestRule<*, SubspaceTestingActivity>.onSubspaceNodeWithTag(
+ testTag: String
+): SubspaceSemanticsNodeInteraction = onSubspaceNode(hasTestTag(testTag))
+
+/** Subspace version of onAllNodesWithTag in Compose. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun AndroidComposeTestRule<*, SubspaceTestingActivity>.onAllSubspaceNodesWithTag(
+ testTag: String
+): SubspaceSemanticsNodeInteractionCollection = onAllSubspaceNodes(hasTestTag(testTag))
+
+/**
+ * Create a fake [Session] for testing.
+ *
+ * TODO(b/370856223) Update documentation to include params
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun createFakeSession(
+ activity: SubspaceTestingActivity,
+ runtime: JxrPlatformAdapter = createFakeRuntime(activity),
+): Session = Session.create(activity, runtime)
+
+/**
+ * Create a fake [JxrPlatformAdapter] for testing.
+ *
+ * TODO(b/370856223) Update documentation to include params
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun createFakeRuntime(activity: SubspaceTestingActivity): JxrPlatformAdapter =
+ JxrPlatformAdapterAxr.create(
+ /* activity = */ activity,
+ /* executor = */ FakeScheduledExecutorService(),
+ /* extensions = */ activity.extensions,
+ /* impressApi = */ FakeImpressApi(),
+ /* perceptionLibrary = */ PerceptionLibrary(),
+ /* splitEngineSubspaceManager = */ mock(SplitEngineSubspaceManager::class.java),
+ /* splitEngineRenderer = */ mock(ImpSplitEngineRenderer::class.java),
+ )
diff --git a/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/TestSetup.kt b/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/TestSetup.kt
new file mode 100644
index 0000000..6a7737a
--- /dev/null
+++ b/xr/compose/compose-testing/src/main/kotlin/androidx/xr/compose/testing/TestSetup.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.testing
+
+import android.app.Activity
+import android.content.Context
+import android.content.ContextWrapper
+import androidx.annotation.RestrictTo
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.xr.compose.platform.LocalHasXrSpatialFeature
+import androidx.xr.compose.platform.LocalSession
+import androidx.xr.scenecore.JxrPlatformAdapter
+import androidx.xr.scenecore.Session
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+
+/**
+ * A Test environment composable wrapper to support testing elevated components locally
+ *
+ * TODO(b/370856223) Update documentation
+ */
+@Composable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun TestSetup(
+ isXrEnabled: Boolean = true,
+ isFullSpace: Boolean = true,
+ runtime: JxrPlatformAdapter =
+ createFakeRuntime(LocalContext.current.getActivity() as SubspaceTestingActivity),
+ content: @Composable Session.() -> Unit,
+) {
+ val activity = LocalContext.current.getActivity() as SubspaceTestingActivity
+ val session = remember {
+ if (isXrEnabled) {
+ createFakeSession(activity, runtime).apply {
+ if (isFullSpace) {
+ requestFullSpaceMode()
+ } else {
+ requestHomeSpaceMode()
+ }
+ }
+ } else {
+ createNonXrSession(activity)
+ }
+ }
+ CompositionLocalProvider(
+ LocalSession provides session,
+ LocalHasXrSpatialFeature provides isXrEnabled,
+ ) {
+ session.content()
+ }
+}
+
+private fun createNonXrSession(activity: Activity): Session {
+ return Session.create(
+ activity,
+ mock<JxrPlatformAdapter> {
+ on { spatialEnvironment } doReturn mock<JxrPlatformAdapter.SpatialEnvironment>()
+ on { activitySpace } doReturn
+ mock<JxrPlatformAdapter.ActivitySpace>(
+ defaultAnswer = { throw UnsupportedOperationException() }
+ )
+ on { headActivityPose } doReturn mock<JxrPlatformAdapter.HeadActivityPose>()
+ on { perceptionSpaceActivityPose } doReturn
+ mock<JxrPlatformAdapter.PerceptionSpaceActivityPose>(
+ defaultAnswer = { throw UnsupportedOperationException() }
+ )
+ on { mainPanelEntity } doReturn mock<JxrPlatformAdapter.PanelEntity>()
+ on { requestHomeSpaceMode() } doAnswer { throw UnsupportedOperationException() }
+ on { requestFullSpaceMode() } doAnswer { throw UnsupportedOperationException() }
+ on { createActivityPanelEntity(any(), any(), any(), any(), any()) } doAnswer
+ {
+ throw UnsupportedOperationException()
+ }
+ on { createAnchorEntity(any(), any(), any(), any()) } doAnswer
+ {
+ throw UnsupportedOperationException()
+ }
+ on { createEntity(any(), any(), any()) } doAnswer
+ {
+ throw UnsupportedOperationException()
+ }
+ on { createGltfEntity(any(), any(), any()) } doAnswer
+ {
+ throw UnsupportedOperationException()
+ }
+ on { createPanelEntity(any(), any(), any(), any(), any(), any(), any()) } doAnswer
+ {
+ throw UnsupportedOperationException()
+ }
+ on { createLoggingEntity(any()) } doAnswer { throw UnsupportedOperationException() }
+ },
+ )
+}
+
+private tailrec fun Context.getActivity(): Activity =
+ when (this) {
+ is Activity -> this
+ is ContextWrapper -> baseContext.getActivity()
+ else -> error("Unexpected Context type when trying to resolve the context's Activity.")
+ }
diff --git a/xr/compose/compose/api/current.txt b/xr/compose/compose/api/current.txt
new file mode 100644
index 0000000..c1c118f
--- /dev/null
+++ b/xr/compose/compose/api/current.txt
@@ -0,0 +1,134 @@
+// Signature format: 4.0
+package androidx.xr.compose.unit {
+
+ public final class DpVolumeSize {
+ ctor public DpVolumeSize(float width, float height, float depth);
+ method public float getDepth();
+ method public float getHeight();
+ method public float getWidth();
+ property public float depth;
+ property public float height;
+ property public float width;
+ field public static final androidx.xr.compose.unit.DpVolumeSize.Companion Companion;
+ }
+
+ public static final class DpVolumeSize.Companion {
+ method public androidx.xr.compose.unit.DpVolumeSize getZero();
+ property public final androidx.xr.compose.unit.DpVolumeSize Zero;
+ }
+
+ public final class IntVolumeSize {
+ ctor public IntVolumeSize(int width, int height, int depth);
+ method public int getDepth();
+ method public int getHeight();
+ method public int getWidth();
+ property public final int depth;
+ property public final int height;
+ property public final int width;
+ field public static final androidx.xr.compose.unit.IntVolumeSize.Companion Companion;
+ }
+
+ public static final class IntVolumeSize.Companion {
+ method public androidx.xr.compose.unit.IntVolumeSize getZero();
+ property public final androidx.xr.compose.unit.IntVolumeSize Zero;
+ }
+
+ @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class Meter implements java.lang.Comparable<androidx.xr.compose.unit.Meter> {
+ ctor public Meter(float value);
+ method public int compareTo(float other);
+ method public inline operator float div(double other);
+ method public inline operator float div(float other);
+ method public inline operator float div(int other);
+ method public float getValue();
+ method public inline operator float minus(float other);
+ method public inline operator float plus(float other);
+ method public inline int roundToPx(androidx.compose.ui.unit.Density density);
+ method public inline operator float times(double other);
+ method public inline operator float times(float other);
+ method public inline operator float times(int other);
+ method public inline float toCm();
+ method public inline float toDp();
+ method public inline float toM();
+ method public inline float toMm();
+ method public inline float toPx(androidx.compose.ui.unit.Density density);
+ property public final inline boolean isFinite;
+ property public final inline boolean isSpecified;
+ property public final float value;
+ field public static final androidx.xr.compose.unit.Meter.Companion Companion;
+ }
+
+ public static final class Meter.Companion {
+ method public inline float fromPixel(float px, androidx.compose.ui.unit.Density density);
+ method public float getCentimeters();
+ method public float getCentimeters();
+ method public float getCentimeters();
+ method public float getInfinity();
+ method public float getMeters();
+ method public float getMeters();
+ method public float getMeters();
+ method public float getMillimeters();
+ method public float getMillimeters();
+ method public float getMillimeters();
+ method public float getNaN();
+ property public float Infinity;
+ property public float NaN;
+ property public float centimeters;
+ property public float centimeters;
+ property public float centimeters;
+ property public float meters;
+ property public float meters;
+ property public float meters;
+ property public float millimeters;
+ property public float millimeters;
+ property public float millimeters;
+ }
+
+ public final class MeterKt {
+ method public static inline operator float div(double, float other);
+ method public static inline operator float div(float, float other);
+ method public static inline operator float div(int, float other);
+ method public static inline operator float times(double, float other);
+ method public static inline operator float times(float, float other);
+ method public static inline operator float times(int, float other);
+ method public static inline float toMeter(float);
+ }
+
+ public final class VolumeConstraints {
+ ctor public VolumeConstraints(int minWidth, int maxWidth, int minHeight, int maxHeight, optional int minDepth, optional int maxDepth);
+ method public androidx.xr.compose.unit.VolumeConstraints copy(optional int minWidth, optional int maxWidth, optional int minHeight, optional int maxHeight, optional int minDepth, optional int maxDepth);
+ method public int getMaxDepth();
+ method public int getMaxHeight();
+ method public int getMaxWidth();
+ method public int getMinDepth();
+ method public int getMinHeight();
+ method public int getMinWidth();
+ method public boolean hasBoundedDepth();
+ method public boolean hasBoundedHeight();
+ method public boolean hasBoundedWidth();
+ property public final boolean hasBoundedDepth;
+ property public final boolean hasBoundedHeight;
+ property public final boolean hasBoundedWidth;
+ property public final int maxDepth;
+ property public final int maxHeight;
+ property public final int maxWidth;
+ property public final int minDepth;
+ property public final int minHeight;
+ property public final int minWidth;
+ field public static final androidx.xr.compose.unit.VolumeConstraints.Companion Companion;
+ field public static final int INFINITY = 2147483647; // 0x7fffffff
+ }
+
+ public static final class VolumeConstraints.Companion {
+ property public static final int INFINITY;
+ }
+
+ public final class VolumeConstraintsKt {
+ method public static androidx.xr.compose.unit.VolumeConstraints constrain(androidx.xr.compose.unit.VolumeConstraints, androidx.xr.compose.unit.VolumeConstraints otherConstraints);
+ method public static int constrainDepth(androidx.xr.compose.unit.VolumeConstraints, int depth);
+ method public static int constrainHeight(androidx.xr.compose.unit.VolumeConstraints, int height);
+ method public static int constrainWidth(androidx.xr.compose.unit.VolumeConstraints, int width);
+ method public static androidx.xr.compose.unit.VolumeConstraints offset(androidx.xr.compose.unit.VolumeConstraints, optional int horizontal, optional int vertical, optional int depth, optional boolean resetMins);
+ }
+
+}
+
diff --git a/camera/camera-effects-still-portrait/api/res-current.txt b/xr/compose/compose/api/res-current.txt
similarity index 100%
copy from camera/camera-effects-still-portrait/api/res-current.txt
copy to xr/compose/compose/api/res-current.txt
diff --git a/xr/compose/compose/api/restricted_current.txt b/xr/compose/compose/api/restricted_current.txt
new file mode 100644
index 0000000..8d9457c
--- /dev/null
+++ b/xr/compose/compose/api/restricted_current.txt
@@ -0,0 +1,843 @@
+// Signature format: 4.0
+package androidx.xr.compose.platform {
+
+ public final class ActivityKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void setSubspaceContent(androidx.activity.ComponentActivity, optional androidx.xr.scenecore.Session? session, optional boolean enableXrForTesting, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
+ public final class LocalSessionKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.xr.scenecore.Session?> getLocalSession();
+ property @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.xr.scenecore.Session?> LocalSession;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class Logger {
+ method public void init(android.content.Context context);
+ method public void log(String tag, kotlin.jvm.functions.Function0<java.lang.String> messageGenerator);
+ field public static final androidx.xr.compose.platform.Logger INSTANCE;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SceneManager implements java.lang.AutoCloseable {
+ method public void close();
+ method public java.util.List<androidx.xr.compose.subspace.node.SubspaceSemanticsNode> getAllRootSubspaceSemanticsNodes();
+ method public int getSceneCount();
+ method public void start();
+ method public void stop();
+ field public static final androidx.xr.compose.platform.SceneManager INSTANCE;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public fun interface SessionCallbackProvider {
+ method public operator androidx.xr.compose.platform.SessionCallbacks get(androidx.xr.scenecore.Session session);
+ field public static final androidx.xr.compose.platform.SessionCallbackProvider.Companion Companion;
+ }
+
+ public static final class SessionCallbackProvider.Companion {
+ method public androidx.xr.compose.platform.SessionCallbackProvider getDefault();
+ property public final androidx.xr.compose.platform.SessionCallbackProvider default;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SessionCallbacks {
+ method public java.io.Closeable onBoundsChanged(kotlin.jvm.functions.Function1<? super androidx.xr.scenecore.Dimensions,kotlin.Unit> callback);
+ method public java.io.Closeable onFullSpaceMode(kotlin.jvm.functions.Function0<kotlin.Unit> callback);
+ method public java.io.Closeable onHomeSpaceMode(kotlin.jvm.functions.Function1<? super androidx.xr.scenecore.Dimensions,kotlin.Unit> callback);
+ method public java.io.Closeable onSpaceModeChanged(kotlin.jvm.functions.Function1<? super androidx.xr.compose.platform.SpaceMode,kotlin.Unit> callback);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.JvmInline public final value class SpaceMode {
+ method public int getValue();
+ property public final int value;
+ field public static final androidx.xr.compose.platform.SpaceMode.Companion Companion;
+ }
+
+ public static final class SpaceMode.Companion {
+ method public int getFull();
+ method public int getHome();
+ method public int getNotApplicable();
+ method public int getUnspecified();
+ property public int Full;
+ property public int Home;
+ property public int NotApplicable;
+ property public int Unspecified;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SpatialCapabilities {
+ method public boolean isAppEnvironmentEnabled();
+ method public boolean isContent3dEnabled();
+ method public boolean isPassthroughControlEnabled();
+ method public boolean isSpatialAudioEnabled();
+ method public boolean isSpatialUiEnabled();
+ property public abstract boolean isAppEnvironmentEnabled;
+ property public abstract boolean isContent3dEnabled;
+ property public abstract boolean isPassthroughControlEnabled;
+ property public abstract boolean isSpatialAudioEnabled;
+ property public abstract boolean isSpatialUiEnabled;
+ field public static final androidx.xr.compose.platform.SpatialCapabilities.Companion Companion;
+ }
+
+ public static final class SpatialCapabilities.Companion {
+ method public androidx.xr.compose.platform.SpatialCapabilities getOrCreate(androidx.xr.scenecore.Session session);
+ }
+
+ public final class SpatialCapabilitiesKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.compose.runtime.CompositionLocal<androidx.xr.compose.platform.SpatialCapabilities> getLocalSpatialCapabilities();
+ property @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final androidx.compose.runtime.CompositionLocal<androidx.xr.compose.platform.SpatialCapabilities> LocalSpatialCapabilities;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SpatialConfiguration {
+ method public default androidx.xr.compose.unit.DpVolumeSize getBounds();
+ method public default boolean hasXrSpatialFeature();
+ method public default void requestFullSpaceMode();
+ method public default void requestHomeSpaceMode();
+ property public default androidx.xr.compose.unit.DpVolumeSize bounds;
+ property public default boolean hasXrSpatialFeature;
+ field public static final androidx.xr.compose.platform.SpatialConfiguration.Companion Companion;
+ }
+
+ public static final class SpatialConfiguration.Companion {
+ method public androidx.xr.compose.platform.SpatialConfiguration getOrCreate(androidx.xr.scenecore.Session session, boolean hasXrSpatialFeature);
+ method public boolean hasXrSpatialFeature(android.content.Context context);
+ }
+
+ public final class SpatialConfigurationKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalHasXrSpatialFeature();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.compose.runtime.CompositionLocal<androidx.xr.compose.platform.SpatialConfiguration> getLocalSpatialConfiguration();
+ property @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalHasXrSpatialFeature;
+ property @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final androidx.compose.runtime.CompositionLocal<androidx.xr.compose.platform.SpatialConfiguration> LocalSpatialConfiguration;
+ }
+
+}
+
+package androidx.xr.compose.spatial {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class EdgeOffset {
+ method public androidx.xr.compose.spatial.EdgeOffset copy(optional float amount, optional int type);
+ method public float getAmount();
+ method public int getType();
+ property public final float amount;
+ property public int type;
+ field public static final androidx.xr.compose.spatial.EdgeOffset.Companion Companion;
+ }
+
+ public static final class EdgeOffset.Companion {
+ method @androidx.compose.runtime.Composable public androidx.xr.compose.spatial.EdgeOffset inner(float offset);
+ method @androidx.compose.runtime.Composable public androidx.xr.compose.spatial.EdgeOffset outer(float offset);
+ method @androidx.compose.runtime.Composable public androidx.xr.compose.spatial.EdgeOffset overlap(float offset);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OrbiterDefaults {
+ method public androidx.xr.compose.spatial.OrbiterSettings getOrbiterSettings();
+ method public androidx.xr.compose.subspace.layout.SpatialShape getShape();
+ property public final androidx.xr.compose.spatial.OrbiterSettings orbiterSettings;
+ property public final androidx.xr.compose.subspace.layout.SpatialShape shape;
+ field public static final androidx.xr.compose.spatial.OrbiterDefaults INSTANCE;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public sealed interface OrbiterEdge {
+ field public static final androidx.xr.compose.spatial.OrbiterEdge.Companion Companion;
+ }
+
+ public static final class OrbiterEdge.Companion {
+ method public int getBottom();
+ method public int getEnd();
+ method public int getStart();
+ method public int getTop();
+ property public int Bottom;
+ property public int End;
+ property public int Start;
+ property public int Top;
+ }
+
+ @kotlin.jvm.JvmInline public static final value class OrbiterEdge.Horizontal implements androidx.xr.compose.spatial.OrbiterEdge {
+ field public static final androidx.xr.compose.spatial.OrbiterEdge.Horizontal.Companion Companion;
+ }
+
+ public static final class OrbiterEdge.Horizontal.Companion {
+ method public int getBottom();
+ method public int getTop();
+ property public int Bottom;
+ property public int Top;
+ }
+
+ @kotlin.jvm.JvmInline public static final value class OrbiterEdge.Vertical implements androidx.xr.compose.spatial.OrbiterEdge {
+ field public static final androidx.xr.compose.spatial.OrbiterEdge.Vertical.Companion Companion;
+ }
+
+ public static final class OrbiterEdge.Vertical.Companion {
+ method public int getEnd();
+ method public int getStart();
+ property public int End;
+ property public int Start;
+ }
+
+ public final class OrbiterKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable public static void Orbiter(int position, androidx.xr.compose.spatial.EdgeOffset offset, optional androidx.compose.ui.Alignment.Horizontal alignment, optional androidx.xr.compose.spatial.OrbiterSettings settings, optional androidx.xr.compose.subspace.layout.SpatialShape shape, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable public static void Orbiter(int position, androidx.xr.compose.spatial.EdgeOffset offset, optional androidx.compose.ui.Alignment.Vertical alignment, optional androidx.xr.compose.spatial.OrbiterSettings settings, optional androidx.xr.compose.subspace.layout.SpatialShape shape, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable public static void Orbiter(int position, optional float offset, optional androidx.compose.ui.Alignment.Horizontal alignment, optional androidx.xr.compose.spatial.OrbiterSettings settings, optional androidx.xr.compose.subspace.layout.SpatialShape shape, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable public static void Orbiter(int position, optional float offset, optional androidx.compose.ui.Alignment.Vertical alignment, optional androidx.xr.compose.spatial.OrbiterSettings settings, optional androidx.xr.compose.subspace.layout.SpatialShape shape, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.JvmInline public final value class OrbiterOffsetType {
+ field public static final androidx.xr.compose.spatial.OrbiterOffsetType.Companion Companion;
+ }
+
+ public static final class OrbiterOffsetType.Companion {
+ method public int getInnerEdge();
+ method public int getOuterEdge();
+ property public int InnerEdge;
+ property public int OuterEdge;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OrbiterSettings {
+ ctor public OrbiterSettings();
+ ctor public OrbiterSettings(optional boolean shouldRenderInNonSpatial);
+ method public androidx.xr.compose.spatial.OrbiterSettings copy(optional boolean shouldRenderInNonSpatial);
+ method public boolean shouldRenderInNonSpatial();
+ property public final boolean shouldRenderInNonSpatial;
+ }
+
+ public final class SpatialDialogKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable public static void SpatialDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.xr.compose.spatial.SpatialDialogProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SpatialDialogProperties {
+ ctor public SpatialDialogProperties();
+ ctor public SpatialDialogProperties(optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional boolean usePlatformDefaultWidth, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> restingLevelAnimationSpec, optional float spatialElevationLevel);
+ method public androidx.xr.compose.spatial.SpatialDialogProperties copy(optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional boolean usePlatformDefaultWidth, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> restingLevelAnimationSpec, optional float spatialElevationLevel);
+ method public boolean getDismissOnBackPress();
+ method public boolean getDismissOnClickOutside();
+ method public androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> getRestingLevelAnimationSpec();
+ method public float getSpatialElevationLevel();
+ method public boolean getUsePlatformDefaultWidth();
+ property public final boolean dismissOnBackPress;
+ property public final boolean dismissOnClickOutside;
+ property public final androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> restingLevelAnimationSpec;
+ property public float spatialElevationLevel;
+ property public final boolean usePlatformDefaultWidth;
+ }
+
+ public final class SpatialElevationKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable public static void SpatialElevation(optional float spatialElevationLevel, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.JvmInline public final value class SpatialElevationLevel {
+ method public float getLevel();
+ property public final float level;
+ field public static final androidx.xr.compose.spatial.SpatialElevationLevel.Companion Companion;
+ }
+
+ public static final class SpatialElevationLevel.Companion {
+ method public float getLevel0();
+ method public float getLevel1();
+ method public float getLevel2();
+ method public float getLevel3();
+ method public float getLevel4();
+ method public float getLevel5();
+ property public float Level0;
+ property public float Level1;
+ property public float Level2;
+ property public float Level3;
+ property public float Level4;
+ property public float Level5;
+ }
+
+ public final class SpatialPopupKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable public static void SpatialPopup(optional androidx.compose.ui.Alignment alignment, optional long offset, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onDismissRequest, optional androidx.xr.compose.spatial.SpatialPopupProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable public static void SpatialPopup(optional float spatialElevationLevel, kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,kotlin.Unit>,kotlin.Unit> content);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SpatialPopupProperties {
+ ctor public SpatialPopupProperties();
+ ctor public SpatialPopupProperties(optional boolean focusable, optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional boolean clippingEnabled, optional float spatialElevationLevel);
+ method public androidx.xr.compose.spatial.SpatialPopupProperties copy(optional boolean focusable, optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional boolean clippingEnabled, optional float spatialElevationLevel);
+ method public boolean getClippingEnabled();
+ method public boolean getDismissOnBackPress();
+ method public boolean getDismissOnClickOutside();
+ method public boolean getFocusable();
+ method public float getSpatialElevationLevel();
+ property public final boolean clippingEnabled;
+ property public final boolean dismissOnBackPress;
+ property public final boolean dismissOnClickOutside;
+ property public final boolean focusable;
+ property public float spatialElevationLevel;
+ }
+
+ public final class SubspaceKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable public static void Subspace(kotlin.jvm.functions.Function1<? super androidx.xr.compose.subspace.SpatialBoxScope,kotlin.Unit> content);
+ }
+
+}
+
+package androidx.xr.compose.subspace {
+
+ public final class RememberComposeViewKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable public static androidx.compose.ui.platform.ComposeView rememberComposeView(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
+ public final class SpatialBoxKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SpatialBox(optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.layout.SpatialAlignment alignment, optional boolean propagateMinConstraints, optional String name, kotlin.jvm.functions.Function1<? super androidx.xr.compose.subspace.SpatialBoxScope,kotlin.Unit> content);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.foundation.layout.LayoutScopeMarker public interface SpatialBoxScope {
+ method public androidx.xr.compose.subspace.layout.SubspaceModifier align(androidx.xr.compose.subspace.layout.SubspaceModifier, androidx.xr.compose.subspace.layout.SpatialAlignment alignment);
+ }
+
+ public final class SpatialColumnKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SpatialColumn(optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.layout.SpatialAlignment alignment, optional String name, kotlin.jvm.functions.Function1<? super androidx.xr.compose.subspace.SpatialColumnScope,kotlin.Unit> content);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.foundation.layout.LayoutScopeMarker public interface SpatialColumnScope {
+ method public androidx.xr.compose.subspace.layout.SubspaceModifier align(androidx.xr.compose.subspace.layout.SubspaceModifier, androidx.xr.compose.subspace.layout.SpatialAlignment.Depth alignment);
+ method public androidx.xr.compose.subspace.layout.SubspaceModifier align(androidx.xr.compose.subspace.layout.SubspaceModifier, androidx.xr.compose.subspace.layout.SpatialAlignment.Horizontal alignment);
+ method public androidx.xr.compose.subspace.layout.SubspaceModifier weight(androidx.xr.compose.subspace.layout.SubspaceModifier, @FloatRange(from=0.0, fromInclusive=false) float weight, optional boolean fill);
+ }
+
+ public final class SpatialLayoutSpacerKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SpatialLayoutSpacer(optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SpatialLayoutSpacer(optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional String name);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SpatialPanelDefaults {
+ method public androidx.xr.compose.subspace.layout.SpatialShape getShape();
+ property public final androidx.xr.compose.subspace.layout.SpatialShape shape;
+ field public static final androidx.xr.compose.subspace.SpatialPanelDefaults INSTANCE;
+ }
+
+ public final class SpatialPanelKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void MainPanel(optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.layout.SpatialShape shape);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SpatialPanel(android.content.Intent intent, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional String name, optional androidx.xr.compose.subspace.layout.SpatialShape shape);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SpatialPanel(android.view.View view, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional String name, optional androidx.xr.compose.subspace.layout.SpatialShape shape);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SpatialPanel(optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional String name, optional androidx.xr.compose.subspace.layout.SpatialShape shape, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
+ public final class SpatialRowKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SpatialRow(optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.layout.SpatialAlignment alignment, optional float curveRadius, optional String name, kotlin.jvm.functions.Function1<? super androidx.xr.compose.subspace.SpatialRowScope,kotlin.Unit> content);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.foundation.layout.LayoutScopeMarker public interface SpatialRowScope {
+ method public androidx.xr.compose.subspace.layout.SubspaceModifier align(androidx.xr.compose.subspace.layout.SubspaceModifier, androidx.xr.compose.subspace.layout.SpatialAlignment.Depth alignment);
+ method public androidx.xr.compose.subspace.layout.SubspaceModifier align(androidx.xr.compose.subspace.layout.SubspaceModifier, androidx.xr.compose.subspace.layout.SpatialAlignment.Vertical alignment);
+ method public androidx.xr.compose.subspace.layout.SubspaceModifier weight(androidx.xr.compose.subspace.layout.SubspaceModifier, @FloatRange(from=0.0, fromInclusive=false) float weight, optional boolean fill);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.ComposableTargetMarker(description="Subspace Composable") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FILE, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.TYPE, kotlin.annotation.AnnotationTarget.TYPE_PARAMETER}) public @interface SubspaceComposable {
+ }
+
+ public final class VolumeKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void Volume(optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional String name, kotlin.jvm.functions.Function1<? super androidx.xr.scenecore.Entity,kotlin.Unit> onVolumeEntity);
+ }
+
+}
+
+package androidx.xr.compose.subspace.layout {
+
+ public final class AlphaKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier alpha(androidx.xr.compose.subspace.layout.SubspaceModifier, @FloatRange(from=0.0, to=1.0) float alpha);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class CombinedSubspaceModifier implements androidx.xr.compose.subspace.layout.SubspaceModifier {
+ ctor public CombinedSubspaceModifier(androidx.xr.compose.subspace.layout.SubspaceModifier outer, androidx.xr.compose.subspace.layout.SubspaceModifier inner);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Measurable {
+ method public void adjustParams(androidx.xr.compose.subspace.layout.ParentLayoutParamsAdjustable params);
+ method public androidx.xr.compose.subspace.layout.Placeable measure(androidx.xr.compose.unit.VolumeConstraints constraints);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public fun interface MeasurePolicy {
+ method public androidx.xr.compose.subspace.layout.MeasureResult measure(androidx.xr.compose.subspace.layout.MeasureScope, java.util.List<? extends androidx.xr.compose.subspace.layout.Measurable> measurables, androidx.xr.compose.unit.VolumeConstraints constraints);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface MeasureResult {
+ method public int getDepth();
+ method public int getHeight();
+ method public int getWidth();
+ method public void placeChildren(androidx.xr.compose.subspace.layout.Placeable.PlacementScope placementScope);
+ property public abstract int depth;
+ property public abstract int height;
+ property public abstract int width;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface MeasureScope extends androidx.compose.ui.unit.Density {
+ method public default float getDensity();
+ method public default float getFontScale();
+ method public default androidx.xr.compose.subspace.layout.MeasureResult layout(int width, int height, int depth, kotlin.jvm.functions.Function1<? super androidx.xr.compose.subspace.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+ property public default float density;
+ property public default float fontScale;
+ }
+
+ public final class MovableKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier movable(androidx.xr.compose.subspace.layout.SubspaceModifier, optional boolean enabled, optional boolean stickyPose, optional kotlin.jvm.functions.Function1<? super androidx.xr.runtime.math.Pose,java.lang.Boolean> onPoseChange);
+ }
+
+ public final class OffsetKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier offset(androidx.xr.compose.subspace.layout.SubspaceModifier, optional float x, optional float y, optional float z);
+ }
+
+ public final class OnGloballyPositionedModifierKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier onGloballyPositioned(androidx.xr.compose.subspace.layout.SubspaceModifier, kotlin.jvm.functions.Function1<? super androidx.xr.compose.subspace.layout.SubspaceLayoutCoordinates,kotlin.Unit> onGloballyPositioned);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OnGloballyPositionedNode extends androidx.xr.compose.subspace.layout.SubspaceModifier.Node {
+ ctor public OnGloballyPositionedNode(kotlin.jvm.functions.Function1<? super androidx.xr.compose.subspace.layout.SubspaceLayoutCoordinates,kotlin.Unit> callback);
+ method public kotlin.jvm.functions.Function1<androidx.xr.compose.subspace.layout.SubspaceLayoutCoordinates,kotlin.Unit> getCallback();
+ method public void setCallback(kotlin.jvm.functions.Function1<? super androidx.xr.compose.subspace.layout.SubspaceLayoutCoordinates,kotlin.Unit>);
+ property public final kotlin.jvm.functions.Function1<androidx.xr.compose.subspace.layout.SubspaceLayoutCoordinates,kotlin.Unit> callback;
+ }
+
+ public final class PaddingKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier padding(androidx.xr.compose.subspace.layout.SubspaceModifier, float all);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier padding(androidx.xr.compose.subspace.layout.SubspaceModifier, optional float horizontal, optional float vertical, optional float depth);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier padding(androidx.xr.compose.subspace.layout.SubspaceModifier, optional float left, optional float top, optional float right, optional float bottom, optional float front, optional float back);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface ParentLayoutParamsAdjustable {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface ParentLayoutParamsModifier {
+ method public void adjustParams(androidx.xr.compose.subspace.layout.ParentLayoutParamsAdjustable params);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class Placeable {
+ ctor public Placeable();
+ method public final int getMeasuredDepth();
+ method public final int getMeasuredHeight();
+ method public final int getMeasuredWidth();
+ method protected abstract void placeAt(androidx.xr.runtime.math.Pose pose);
+ method public final void setMeasuredDepth(int);
+ method public final void setMeasuredHeight(int);
+ method public final void setMeasuredWidth(int);
+ property public final int measuredDepth;
+ property public final int measuredHeight;
+ property public final int measuredWidth;
+ }
+
+ public abstract static class Placeable.PlacementScope {
+ ctor public Placeable.PlacementScope();
+ method public androidx.xr.compose.subspace.layout.SubspaceLayoutCoordinates? getCoordinates();
+ method public final void place(androidx.xr.compose.subspace.layout.Placeable, androidx.xr.runtime.math.Pose pose);
+ property public androidx.xr.compose.subspace.layout.SubspaceLayoutCoordinates? coordinates;
+ }
+
+ public final class ResizableKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier resizable(androidx.xr.compose.subspace.layout.SubspaceModifier, optional boolean enabled, optional androidx.xr.compose.unit.DpVolumeSize minimumSize, optional androidx.xr.compose.unit.DpVolumeSize maximumSize, optional boolean maintainAspectRatio, optional kotlin.jvm.functions.Function1<? super androidx.xr.compose.unit.IntVolumeSize,java.lang.Boolean> onSizeChange);
+ }
+
+ public final class RotateKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier rotate(androidx.xr.compose.subspace.layout.SubspaceModifier, androidx.xr.runtime.math.Quaternion quaternion);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier rotate(androidx.xr.compose.subspace.layout.SubspaceModifier, androidx.xr.runtime.math.Vector3 axisAngle, float rotation);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier rotate(androidx.xr.compose.subspace.layout.SubspaceModifier, float pitch, float yaw, float roll);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class RotationNode extends androidx.xr.compose.subspace.layout.SubspaceModifier.Node implements androidx.xr.compose.subspace.node.SubspaceLayoutModifierNode {
+ ctor public RotationNode(androidx.xr.runtime.math.Quaternion quaternion);
+ method public androidx.xr.runtime.math.Quaternion getQuaternion();
+ method public androidx.xr.compose.subspace.layout.MeasureResult measure(androidx.xr.compose.subspace.layout.MeasureScope, androidx.xr.compose.subspace.layout.Measurable measurable, androidx.xr.compose.unit.VolumeConstraints constraints);
+ method public void setQuaternion(androidx.xr.runtime.math.Quaternion);
+ property public final androidx.xr.runtime.math.Quaternion quaternion;
+ }
+
+ public final class ScaleKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier scale(androidx.xr.compose.subspace.layout.SubspaceModifier, float scale);
+ }
+
+ public final class SemanticsModifierKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier semantics(androidx.xr.compose.subspace.layout.SubspaceModifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.semantics.SemanticsPropertyReceiver,kotlin.Unit> properties);
+ }
+
+ public final class SizeKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier depth(androidx.xr.compose.subspace.layout.SubspaceModifier, float depth);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier fillMaxDepth(androidx.xr.compose.subspace.layout.SubspaceModifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier fillMaxHeight(androidx.xr.compose.subspace.layout.SubspaceModifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier fillMaxSize(androidx.xr.compose.subspace.layout.SubspaceModifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier fillMaxWidth(androidx.xr.compose.subspace.layout.SubspaceModifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier height(androidx.xr.compose.subspace.layout.SubspaceModifier, float height);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier requiredDepth(androidx.xr.compose.subspace.layout.SubspaceModifier, float depth);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier requiredHeight(androidx.xr.compose.subspace.layout.SubspaceModifier, float height);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier requiredSize(androidx.xr.compose.subspace.layout.SubspaceModifier, androidx.xr.compose.unit.DpVolumeSize size);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier requiredSize(androidx.xr.compose.subspace.layout.SubspaceModifier, float size);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier requiredWidth(androidx.xr.compose.subspace.layout.SubspaceModifier, float width);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier size(androidx.xr.compose.subspace.layout.SubspaceModifier, androidx.xr.compose.unit.DpVolumeSize size);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier size(androidx.xr.compose.subspace.layout.SubspaceModifier, float size);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier width(androidx.xr.compose.subspace.layout.SubspaceModifier, float width);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SpatialAlignment {
+ method public int depthOffset(int depth, int space);
+ method public static androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Depth getBack();
+ method public static androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Vertical getBottom();
+ method public static androidx.xr.compose.subspace.layout.SpatialAlignment getBottomCenter();
+ method public static androidx.xr.compose.subspace.layout.SpatialAlignment getBottomLeft();
+ method public static androidx.xr.compose.subspace.layout.SpatialAlignment getBottomRight();
+ method public static androidx.xr.compose.subspace.layout.SpatialAlignment getCenter();
+ method public static androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Depth getCenterDepthwise();
+ method public static androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Horizontal getCenterHorizontally();
+ method public static androidx.xr.compose.subspace.layout.SpatialAlignment getCenterLeft();
+ method public static androidx.xr.compose.subspace.layout.SpatialAlignment getCenterRight();
+ method public static androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Vertical getCenterVertically();
+ method public static androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Depth getFront();
+ method public static androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Horizontal getLeft();
+ method public static androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Horizontal getRight();
+ method public static androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Vertical getTop();
+ method public static androidx.xr.compose.subspace.layout.SpatialAlignment getTopCenter();
+ method public static androidx.xr.compose.subspace.layout.SpatialAlignment getTopLeft();
+ method public static androidx.xr.compose.subspace.layout.SpatialAlignment getTopRight();
+ method public int horizontalOffset(int width, int space);
+ method public androidx.xr.runtime.math.Vector3 position(androidx.xr.compose.unit.IntVolumeSize size, androidx.xr.compose.unit.IntVolumeSize space);
+ method public int verticalOffset(int height, int space);
+ field public static final androidx.xr.compose.subspace.layout.SpatialAlignment.Companion Companion;
+ }
+
+ public static final class SpatialAlignment.Companion {
+ method public androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Depth getBack();
+ method public androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Vertical getBottom();
+ method public androidx.xr.compose.subspace.layout.SpatialAlignment getBottomCenter();
+ method public androidx.xr.compose.subspace.layout.SpatialAlignment getBottomLeft();
+ method public androidx.xr.compose.subspace.layout.SpatialAlignment getBottomRight();
+ method public androidx.xr.compose.subspace.layout.SpatialAlignment getCenter();
+ method public androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Depth getCenterDepthwise();
+ method public androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Horizontal getCenterHorizontally();
+ method public androidx.xr.compose.subspace.layout.SpatialAlignment getCenterLeft();
+ method public androidx.xr.compose.subspace.layout.SpatialAlignment getCenterRight();
+ method public androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Vertical getCenterVertically();
+ method public androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Depth getFront();
+ method public androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Horizontal getLeft();
+ method public androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Horizontal getRight();
+ method public androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Vertical getTop();
+ method public androidx.xr.compose.subspace.layout.SpatialAlignment getTopCenter();
+ method public androidx.xr.compose.subspace.layout.SpatialAlignment getTopLeft();
+ method public androidx.xr.compose.subspace.layout.SpatialAlignment getTopRight();
+ property public final androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Depth Back;
+ property public final androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Vertical Bottom;
+ property public final androidx.xr.compose.subspace.layout.SpatialAlignment BottomCenter;
+ property public final androidx.xr.compose.subspace.layout.SpatialAlignment BottomLeft;
+ property public final androidx.xr.compose.subspace.layout.SpatialAlignment BottomRight;
+ property public final androidx.xr.compose.subspace.layout.SpatialAlignment Center;
+ property public final androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Depth CenterDepthwise;
+ property public final androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Horizontal CenterHorizontally;
+ property public final androidx.xr.compose.subspace.layout.SpatialAlignment CenterLeft;
+ property public final androidx.xr.compose.subspace.layout.SpatialAlignment CenterRight;
+ property public final androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Vertical CenterVertically;
+ property public final androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Depth Front;
+ property public final androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Horizontal Left;
+ property public final androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Horizontal Right;
+ property public final androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Vertical Top;
+ property public final androidx.xr.compose.subspace.layout.SpatialAlignment TopCenter;
+ property public final androidx.xr.compose.subspace.layout.SpatialAlignment TopLeft;
+ property public final androidx.xr.compose.subspace.layout.SpatialAlignment TopRight;
+ }
+
+ public static interface SpatialAlignment.Depth {
+ method public int offset(int depth, int space);
+ }
+
+ public static interface SpatialAlignment.Horizontal {
+ method public int offset(int width, int space);
+ }
+
+ public static interface SpatialAlignment.Vertical {
+ method public int offset(int height, int space);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SpatialBiasAlignment implements androidx.xr.compose.subspace.layout.SpatialAlignment {
+ ctor public SpatialBiasAlignment(float horizontalBias, float verticalBias, float depthBias);
+ method public androidx.xr.compose.subspace.layout.SpatialBiasAlignment copy(optional float horizontalBias, optional float verticalBias, optional float depthBias);
+ method public int depthOffset(int depth, int space);
+ method public float getDepthBias();
+ method public float getHorizontalBias();
+ method public float getVerticalBias();
+ method public int horizontalOffset(int width, int space);
+ method public androidx.xr.runtime.math.Vector3 position(androidx.xr.compose.unit.IntVolumeSize size, androidx.xr.compose.unit.IntVolumeSize space);
+ method public int verticalOffset(int height, int space);
+ property public final float depthBias;
+ property public final float horizontalBias;
+ property public final float verticalBias;
+ field public static final androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Companion Companion;
+ }
+
+ public static final class SpatialBiasAlignment.Companion {
+ }
+
+ public static final class SpatialBiasAlignment.Depth implements androidx.xr.compose.subspace.layout.SpatialAlignment.Depth {
+ ctor public SpatialBiasAlignment.Depth(float bias);
+ method public androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Depth copy(optional float bias);
+ method public float getBias();
+ method public int offset(int depth, int space);
+ property public final float bias;
+ }
+
+ public static final class SpatialBiasAlignment.Horizontal implements androidx.xr.compose.subspace.layout.SpatialAlignment.Horizontal {
+ ctor public SpatialBiasAlignment.Horizontal(float bias);
+ method public androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Horizontal copy(optional float bias);
+ method public float getBias();
+ method public int offset(int width, int space);
+ property public final float bias;
+ }
+
+ public static final class SpatialBiasAlignment.Vertical implements androidx.xr.compose.subspace.layout.SpatialAlignment.Vertical {
+ ctor public SpatialBiasAlignment.Vertical(float bias);
+ method public androidx.xr.compose.subspace.layout.SpatialBiasAlignment.Vertical copy(optional float bias);
+ method public float getBias();
+ method public int offset(int height, int space);
+ property public final float bias;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SpatialRoundedCornerShape extends androidx.xr.compose.subspace.layout.SpatialShape {
+ ctor public SpatialRoundedCornerShape(androidx.compose.foundation.shape.CornerSize size);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class SpatialShape {
+ ctor public SpatialShape();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SubspaceLayoutCoordinates {
+ method public androidx.xr.runtime.math.Pose getPose();
+ method public androidx.xr.runtime.math.Pose getPoseInParentEntity();
+ method public androidx.xr.runtime.math.Pose getPoseInRoot();
+ method @Deprecated public default androidx.xr.runtime.math.Vector3 getPosition();
+ method @Deprecated public default androidx.xr.runtime.math.Vector3 getPositionInParentEntity();
+ method @Deprecated public default androidx.xr.runtime.math.Vector3 getPositionInRoot();
+ method @Deprecated public default androidx.xr.runtime.math.Quaternion getRotation();
+ method @Deprecated public default androidx.xr.runtime.math.Quaternion getRotationInParentEntity();
+ method @Deprecated public default androidx.xr.runtime.math.Quaternion getRotationInRoot();
+ method public androidx.xr.compose.unit.IntVolumeSize getSize();
+ property public abstract androidx.xr.runtime.math.Pose pose;
+ property public abstract androidx.xr.runtime.math.Pose poseInParentEntity;
+ property public abstract androidx.xr.runtime.math.Pose poseInRoot;
+ property @Deprecated public default androidx.xr.runtime.math.Vector3 position;
+ property @Deprecated public default androidx.xr.runtime.math.Vector3 positionInParentEntity;
+ property @Deprecated public default androidx.xr.runtime.math.Vector3 positionInRoot;
+ property @Deprecated public default androidx.xr.runtime.math.Quaternion rotation;
+ property @Deprecated public default androidx.xr.runtime.math.Quaternion rotationInParentEntity;
+ property @Deprecated public default androidx.xr.runtime.math.Quaternion rotationInRoot;
+ property public abstract androidx.xr.compose.unit.IntVolumeSize size;
+ }
+
+ public final class SubspaceLayoutCoordinatesKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static String toDebugString(androidx.xr.compose.subspace.layout.SubspaceLayoutCoordinates);
+ }
+
+ public final class SubspaceLayoutKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SubspaceLayout(optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional String name, androidx.xr.compose.subspace.layout.MeasurePolicy measurePolicy);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SubspaceLayout(kotlin.jvm.functions.Function0<kotlin.Unit> content, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional String name, androidx.xr.compose.subspace.layout.MeasurePolicy measurePolicy);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static String defaultSubspaceLayoutName();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SubspaceModifier {
+ method public default boolean all(kotlin.jvm.functions.Function1<? super androidx.xr.compose.subspace.node.SubspaceModifierElement<androidx.xr.compose.subspace.layout.SubspaceModifier.Node>,java.lang.Boolean> predicate);
+ method public default boolean any(kotlin.jvm.functions.Function1<? super androidx.xr.compose.subspace.node.SubspaceModifierElement<androidx.xr.compose.subspace.layout.SubspaceModifier.Node>,java.lang.Boolean> predicate);
+ method public default <R> R foldIn(R initial, kotlin.jvm.functions.Function2<? super R,? super androidx.xr.compose.subspace.node.SubspaceModifierElement<androidx.xr.compose.subspace.layout.SubspaceModifier.Node>,? extends R> operation);
+ method public default <R> R foldOut(R initial, kotlin.jvm.functions.Function2<? super androidx.xr.compose.subspace.node.SubspaceModifierElement<androidx.xr.compose.subspace.layout.SubspaceModifier.Node>,? super R,? extends R> operation);
+ method public default infix androidx.xr.compose.subspace.layout.SubspaceModifier then(androidx.xr.compose.subspace.layout.SubspaceModifier other);
+ field public static final androidx.xr.compose.subspace.layout.SubspaceModifier.Companion Companion;
+ }
+
+ public static final class SubspaceModifier.Companion implements androidx.xr.compose.subspace.layout.SubspaceModifier {
+ method public infix androidx.xr.compose.subspace.layout.SubspaceModifier then(androidx.xr.compose.subspace.node.SubspaceModifierElement<androidx.xr.compose.subspace.layout.SubspaceModifier.Node> other);
+ }
+
+ public abstract static class SubspaceModifier.Node {
+ ctor public SubspaceModifier.Node();
+ }
+
+ public final class SubspaceModifierKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static kotlin.sequences.Sequence<androidx.xr.compose.subspace.layout.SubspaceModifier.Node> traverseAncestors(androidx.xr.compose.subspace.layout.SubspaceModifier.Node);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static kotlin.sequences.Sequence<androidx.xr.compose.subspace.layout.SubspaceModifier.Node> traverseDescendants(androidx.xr.compose.subspace.layout.SubspaceModifier.Node);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static kotlin.sequences.Sequence<androidx.xr.compose.subspace.layout.SubspaceModifier.Node> traverseSelfThenAncestors(androidx.xr.compose.subspace.layout.SubspaceModifier.Node);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static kotlin.sequences.Sequence<androidx.xr.compose.subspace.layout.SubspaceModifier.Node> traverseSelfThenDescendants(androidx.xr.compose.subspace.layout.SubspaceModifier.Node);
+ }
+
+ public final class TestTagKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.compose.subspace.layout.SubspaceModifier testTag(androidx.xr.compose.subspace.layout.SubspaceModifier, String tag);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class TestTagNode extends androidx.xr.compose.subspace.layout.SubspaceModifier.Node implements androidx.xr.compose.subspace.node.SubspaceSemanticsModifierNode {
+ ctor public TestTagNode(String tag);
+ method public void applySemantics(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method public String getTag();
+ method public void setTag(String);
+ property public final String tag;
+ }
+
+}
+
+package androidx.xr.compose.subspace.node {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SubspaceLayoutModifierNode {
+ method public androidx.xr.compose.subspace.layout.MeasureResult measure(androidx.xr.compose.subspace.layout.MeasureScope, androidx.xr.compose.subspace.layout.Measurable measurable, androidx.xr.compose.unit.VolumeConstraints constraints);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class SubspaceModifierElement<N extends androidx.xr.compose.subspace.layout.SubspaceModifier.Node> implements androidx.xr.compose.subspace.layout.SubspaceModifier {
+ ctor public SubspaceModifierElement();
+ method public abstract N create();
+ method public abstract boolean equals(Object? other);
+ method public abstract int hashCode();
+ method public abstract void update(N node);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SubspaceSemanticsModifierNode {
+ method public void applySemantics(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SubspaceSemanticsNode {
+ method public java.util.List<androidx.xr.compose.subspace.node.SubspaceSemanticsNode> getChildren();
+ method public java.util.List<androidx.xr.scenecore.Component>? getComponents();
+ method public androidx.compose.ui.semantics.SemanticsConfiguration getConfig();
+ method public int getId();
+ method public androidx.xr.compose.subspace.node.SubspaceSemanticsNode? getParent();
+ method public androidx.xr.runtime.math.Vector3 getPosition();
+ method public androidx.xr.runtime.math.Vector3 getPositionInRoot();
+ method public androidx.xr.runtime.math.Quaternion getRotation();
+ method public androidx.xr.runtime.math.Quaternion getRotationInRoot();
+ method public float getScale();
+ method public androidx.xr.compose.unit.IntVolumeSize getSize();
+ method public boolean isRoot();
+ property public final java.util.List<androidx.xr.compose.subspace.node.SubspaceSemanticsNode> children;
+ property public final java.util.List<androidx.xr.scenecore.Component>? components;
+ property public final androidx.compose.ui.semantics.SemanticsConfiguration config;
+ property public final int id;
+ property public final boolean isRoot;
+ property public final androidx.xr.compose.subspace.node.SubspaceSemanticsNode? parent;
+ property public final androidx.xr.runtime.math.Vector3 position;
+ property public final androidx.xr.runtime.math.Vector3 positionInRoot;
+ property public final androidx.xr.runtime.math.Quaternion rotation;
+ property public final androidx.xr.runtime.math.Quaternion rotationInRoot;
+ property public final float scale;
+ property public final androidx.xr.compose.unit.IntVolumeSize size;
+ }
+
+}
+
+package androidx.xr.compose.unit {
+
+ public final class DpVolumeSize {
+ ctor public DpVolumeSize(float width, float height, float depth);
+ method public float getDepth();
+ method public float getHeight();
+ method public float getWidth();
+ property public float depth;
+ property public float height;
+ property public float width;
+ field public static final androidx.xr.compose.unit.DpVolumeSize.Companion Companion;
+ }
+
+ public static final class DpVolumeSize.Companion {
+ method public androidx.xr.compose.unit.DpVolumeSize getZero();
+ property public final androidx.xr.compose.unit.DpVolumeSize Zero;
+ }
+
+ public final class IntVolumeSize {
+ ctor public IntVolumeSize(int width, int height, int depth);
+ method public int getDepth();
+ method public int getHeight();
+ method public int getWidth();
+ property public final int depth;
+ property public final int height;
+ property public final int width;
+ field public static final androidx.xr.compose.unit.IntVolumeSize.Companion Companion;
+ }
+
+ public static final class IntVolumeSize.Companion {
+ method public androidx.xr.compose.unit.IntVolumeSize getZero();
+ property public final androidx.xr.compose.unit.IntVolumeSize Zero;
+ }
+
+ @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class Meter implements java.lang.Comparable<androidx.xr.compose.unit.Meter> {
+ ctor public Meter(float value);
+ method public int compareTo(float other);
+ method public inline operator float div(double other);
+ method public inline operator float div(float other);
+ method public inline operator float div(int other);
+ method public float getValue();
+ method public inline operator float minus(float other);
+ method public inline operator float plus(float other);
+ method public inline int roundToPx(androidx.compose.ui.unit.Density density);
+ method public inline operator float times(double other);
+ method public inline operator float times(float other);
+ method public inline operator float times(int other);
+ method public inline float toCm();
+ method public inline float toDp();
+ method public inline float toM();
+ method public inline float toMm();
+ method public inline float toPx(androidx.compose.ui.unit.Density density);
+ property public final inline boolean isFinite;
+ property public final inline boolean isSpecified;
+ property public final float value;
+ field public static final androidx.xr.compose.unit.Meter.Companion Companion;
+ field @kotlin.PublishedApi internal static final float DP_PER_METER;
+ }
+
+ public static final class Meter.Companion {
+ method public inline float fromPixel(float px, androidx.compose.ui.unit.Density density);
+ method public float getCentimeters();
+ method public float getCentimeters();
+ method public float getCentimeters();
+ method public float getInfinity();
+ method public float getMeters();
+ method public float getMeters();
+ method public float getMeters();
+ method public float getMillimeters();
+ method public float getMillimeters();
+ method public float getMillimeters();
+ method public float getNaN();
+ property @kotlin.PublishedApi internal final float DP_PER_METER;
+ property public float Infinity;
+ property public float NaN;
+ property public float centimeters;
+ property public float centimeters;
+ property public float centimeters;
+ property public float meters;
+ property public float meters;
+ property public float meters;
+ property public float millimeters;
+ property public float millimeters;
+ property public float millimeters;
+ }
+
+ public final class MeterKt {
+ method public static inline operator float div(double, float other);
+ method public static inline operator float div(float, float other);
+ method public static inline operator float div(int, float other);
+ method public static inline operator float times(double, float other);
+ method public static inline operator float times(float, float other);
+ method public static inline operator float times(int, float other);
+ method public static inline float toMeter(float);
+ }
+
+ public final class VolumeConstraints {
+ ctor public VolumeConstraints(int minWidth, int maxWidth, int minHeight, int maxHeight, optional int minDepth, optional int maxDepth);
+ method public androidx.xr.compose.unit.VolumeConstraints copy(optional int minWidth, optional int maxWidth, optional int minHeight, optional int maxHeight, optional int minDepth, optional int maxDepth);
+ method public int getMaxDepth();
+ method public int getMaxHeight();
+ method public int getMaxWidth();
+ method public int getMinDepth();
+ method public int getMinHeight();
+ method public int getMinWidth();
+ method public boolean hasBoundedDepth();
+ method public boolean hasBoundedHeight();
+ method public boolean hasBoundedWidth();
+ property public final boolean hasBoundedDepth;
+ property public final boolean hasBoundedHeight;
+ property public final boolean hasBoundedWidth;
+ property public final int maxDepth;
+ property public final int maxHeight;
+ property public final int maxWidth;
+ property public final int minDepth;
+ property public final int minHeight;
+ property public final int minWidth;
+ field public static final androidx.xr.compose.unit.VolumeConstraints.Companion Companion;
+ field public static final int INFINITY = 2147483647; // 0x7fffffff
+ }
+
+ public static final class VolumeConstraints.Companion {
+ property public static final int INFINITY;
+ }
+
+ public final class VolumeConstraintsKt {
+ method public static androidx.xr.compose.unit.VolumeConstraints constrain(androidx.xr.compose.unit.VolumeConstraints, androidx.xr.compose.unit.VolumeConstraints otherConstraints);
+ method public static int constrainDepth(androidx.xr.compose.unit.VolumeConstraints, int depth);
+ method public static int constrainHeight(androidx.xr.compose.unit.VolumeConstraints, int height);
+ method public static int constrainWidth(androidx.xr.compose.unit.VolumeConstraints, int width);
+ method public static androidx.xr.compose.unit.VolumeConstraints offset(androidx.xr.compose.unit.VolumeConstraints, optional int horizontal, optional int vertical, optional int depth, optional boolean resetMins);
+ }
+
+}
+
diff --git a/xr/compose/compose/build.gradle b/xr/compose/compose/build.gradle
new file mode 100644
index 0000000..b134495
--- /dev/null
+++ b/xr/compose/compose/build.gradle
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+
+import androidx.build.LibraryType
+import androidx.build.KotlinTarget
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("AndroidXComposePlugin")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+
+ implementation("androidx.annotation:annotation:1.8.1")
+ implementation("androidx.savedstate:savedstate:1.2.1")
+ implementation("androidx.activity:activity:1.9.3")
+ implementation("androidx.lifecycle:lifecycle-common:2.8.7")
+ implementation("androidx.lifecycle:lifecycle-runtime:2.8.7")
+ implementation(project(":xr:scenecore:scenecore"))
+
+ implementation("androidx.compose.animation:animation-core:1.7.5")
+ implementation("androidx.compose.foundation:foundation:1.7.5")
+ implementation("androidx.compose.foundation:foundation-layout:1.7.5")
+ implementation("androidx.compose.runtime:runtime:1.7.5")
+ implementation("androidx.compose.ui:ui:1.7.5")
+ implementation("androidx.compose.ui:ui-unit:1.7.5")
+
+ testImplementation(libs.junit)
+ testImplementation(libs.kotlinTest)
+ testImplementation(libs.robolectric)
+ testImplementation(libs.testExtJunit)
+ testImplementation(libs.testRunner)
+ testImplementation(libs.truth)
+
+ testImplementation("androidx.compose.material3:material3:1.3.1")
+ testImplementation("androidx.compose.ui:ui-test:1.7.5")
+ testImplementation("androidx.compose.ui:ui-test-junit4:1.7.5")
+ testImplementation(project(":xr:compose:compose-testing"))
+}
+
+android {
+ defaultConfig {
+ // TODO: This should be lower, possibly 21.
+ // Address API calls that require higher versions.
+ minSdkVersion 30
+ }
+ namespace "androidx.xr.compose"
+}
+
+androidx {
+ name = "XR Compose"
+ type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
+ inceptionYear = "2024"
+ description = "Compose libraries for the androidx.xr namespace."
+ kotlinTarget = KotlinTarget.KOTLIN_1_9
+ // TODO: b/326456246
+ optOutJSpecify = true
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/Activity.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/Activity.kt
new file mode 100644
index 0000000..a9305b5
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/Activity.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import android.app.Activity
+import android.content.Context
+import android.content.ContextWrapper
+import androidx.activity.ComponentActivity
+import androidx.annotation.RestrictTo
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.Density
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.xr.compose.subspace.SubspaceComposable
+import androidx.xr.scenecore.Session
+
+private val activityToSpatialComposeScene = mutableMapOf<Activity, SpatialComposeScene>()
+
+/**
+ * Composes the provided composable [content] into this activity's subspace.
+ *
+ * This method only takes effect in Android XR. Calling it will cause the activity to register
+ * itself as a subspace, allowing it to control its own panel (i.e. move and resize within its
+ * subspace 3D bounds) and show additional spatial content around it.
+ *
+ * @param session A custom session to be provided for testing purposes. If null, a suitable session
+ * for production use will be created.
+ * @param enableXrForTesting When set, this method will assume it is running on Android XR even if
+ * it is not. Should only be used for testing, it will cause crashes on non-Android XR devices.
+ * @param content A `@Composable` function declaring the spatial UI content.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun ComponentActivity.setSubspaceContent(
+ session: Session? = null,
+ enableXrForTesting: Boolean = false,
+ content: @Composable @SubspaceComposable () -> Unit,
+) {
+ // Do nothing if we aren't running on an XR device or in an XR test.
+ if (!SpatialConfiguration.hasXrSpatialFeature(this) && !enableXrForTesting) {
+ return
+ }
+ val jxrSession = session ?: defaultSession
+ val spatialComposeScene = getOrCreateSpatialSceneForActivity(jxrSession)
+
+ spatialComposeScene.setContent {
+ DisposableEffect(jxrSession) {
+ jxrSession.mainPanelEntity.setHidden(true)
+ onDispose { jxrSession.mainPanelEntity.setHidden(false) }
+ }
+
+ // TODO(b/354009078) Why does rendering content in full space mode break presubmits.
+
+ // We need to emulate the composition locals that setContent provides
+ CompositionLocalProvider(
+ LocalConfiguration provides resources.configuration,
+ LocalDensity provides Density(this),
+ LocalView provides window.decorView,
+ content = content,
+ )
+ }
+}
+
+/**
+ * Looks up an existing [SpatialComposeScene] for this activity. If it doesn't exist, adds it to the
+ * map.
+ *
+ * @param session A custom session to be provided for testing purposes. If null, a suitable session
+ * for production use will be created.
+ */
+private fun ComponentActivity.getOrCreateSpatialSceneForActivity(
+ session: Session = defaultSession
+): SpatialComposeScene {
+ return activityToSpatialComposeScene.computeIfAbsent(this) {
+ SpatialComposeScene(this, session).also { subspace -> setUpSubspace(subspace) }
+ }
+}
+
+/**
+ * Sets up the newly created [spatialComposeScene] to listen to [this] lifecycle state changes and
+ * to be cleaned up when [this] is destroyed.
+ */
+private fun ComponentActivity.setUpSubspace(spatialComposeScene: SpatialComposeScene) {
+ lifecycle.addObserver(spatialComposeScene)
+ lifecycle.addObserver(
+ object : LifecycleEventObserver {
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+ if (event == Lifecycle.Event.ON_DESTROY) {
+ activityToSpatialComposeScene.remove(this@setUpSubspace)
+ }
+ }
+ }
+ )
+}
+
+/** Get the default [Session] for this [ComponentActivity]. */
+private val ComponentActivity.defaultSession
+ get() = Session.create(this)
+
+/** Utility extension function to fetch the current [Activity] based on the [Context] object. */
+internal tailrec fun Context.getActivity(): Activity {
+ return when (this) {
+ is Activity -> this
+ is ContextWrapper -> baseContext.getActivity()
+ else -> error("Unexpected Context type when trying to resolve the context's Activity.")
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/AndroidComposeSpatialElement.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/AndroidComposeSpatialElement.kt
new file mode 100644
index 0000000..5be8507
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/AndroidComposeSpatialElement.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.xr.compose.subspace.node.SubspaceLayoutNode
+import androidx.xr.compose.subspace.node.SubspaceOwner
+import androidx.xr.scenecore.PanelEntity
+
+/**
+ * An implementation of the [SubspaceOwner] interface, bridging the Compose layout and rendering
+ * phases.
+ *
+ * Compose decouples layout computation from rendering. This necessitates two distinct trees that
+ * [SubspaceOwner] defines and owns:
+ * 1. The layout tree: Created by [SubspaceLayoutNode], this represents the app's defined hierarchy
+ * and is used for layout calculations.
+ * 2. The rendering tree: A flat tree rooted at the [SubspaceOwner]. This is used for rendering. The
+ * hierarchy is necessary to ensure [SpatialElement] instances are visible on a screen (detached
+ * [SpatialElement] instances are always hidden). The positions of [SpatialElement] are updated
+ * post-layout computation, during the placement phase.
+ *
+ * This class draws inspiration from the [androidx/compose/ui/platform/AndroidComposeView].
+ */
+internal class AndroidComposeSpatialElement :
+ SpatialElement(), SubspaceOwner, DefaultLifecycleObserver {
+ override val root: SubspaceLayoutNode = SubspaceLayoutNode()
+
+ internal var wrappedComposition: WrappedComposition? = null
+
+ /**
+ * Callback that is registered in [setOnSubspaceAvailable] to be executed when this element is
+ * attached a subspace.
+ */
+ private var onSubspaceAvailable: ((LifecycleOwner) -> Unit)? = null
+
+ private var windowLeashLayoutNode: SubspaceLayoutNode? = null
+
+ init {
+ root.attach(this)
+ }
+
+ /**
+ * Registers the [callback] to be executed when this element is attached to a
+ * [spatialComposeScene].
+ *
+ * Note that the [callback] will be invoked immediately if [spatialComposeScene] is already
+ * available.
+ */
+ internal fun setOnSubspaceAvailable(callback: (LifecycleOwner) -> Unit) {
+ if (spatialComposeScene != null) {
+ callback(spatialComposeScene!!)
+ } else {
+ onSubspaceAvailable = callback
+ }
+ }
+
+ override fun onAttachedToSubspace(spatialComposeScene: SpatialComposeScene) {
+ super.onAttachedToSubspace(spatialComposeScene)
+
+ spatialComposeScene.lifecycle.addObserver(this)
+ onSubspaceAvailable?.invoke(spatialComposeScene)
+ onSubspaceAvailable = null
+ }
+
+ override fun onDetachedFromSubspace(spatialComposeScene: SpatialComposeScene) {
+ super.onDetachedFromSubspace(spatialComposeScene)
+
+ spatialComposeScene.lifecycle.removeObserver(this)
+ }
+
+ override fun onAttach(node: SubspaceLayoutNode) {
+ node.coreEntity?.entity.let { entity ->
+ if (entity is PanelEntity && entity.isMainPanelEntity) {
+ check(windowLeashLayoutNode == null) {
+ "Cannot add $node as there is already another SubspaceLayoutNode for the Window Leash Node"
+ }
+ windowLeashLayoutNode = node
+ }
+ }
+ }
+
+ override fun onDetach(node: SubspaceLayoutNode) {
+ node.coreEntity?.entity.let { entity ->
+ if (entity is PanelEntity && entity.isMainPanelEntity) {
+ windowLeashLayoutNode = null
+ }
+ }
+ }
+
+ override fun onResume(owner: LifecycleOwner) {
+ super.onResume(owner)
+ // TODO: "Refresh the layout hierarchy."
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ super.onDestroy(owner)
+ root.detach()
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/CompositionLocals.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/CompositionLocals.kt
new file mode 100644
index 0000000..5a0c8e1
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/CompositionLocals.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.xr.compose.subspace.SubspaceComposable
+
+@OptIn(InternalSubspaceApi::class)
+@Composable
+internal fun ProvideCompositionLocals(
+ owner: AndroidComposeSpatialElement,
+ content: @Composable @SubspaceComposable () -> Unit,
+) {
+ val subspace =
+ checkNotNull(owner.spatialComposeScene) { "Owner element must be attached to a subspace." }
+ val context = subspace.ownerActivity
+
+ CompositionLocalProvider(
+ LocalContext provides context,
+ LocalLifecycleOwner provides subspace.ownerActivity,
+ LocalSession provides subspace.jxrSession,
+ content = content,
+ )
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/DialogManager.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/DialogManager.kt
new file mode 100644
index 0000000..816f083
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/DialogManager.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.staticCompositionLocalOf
+
+/** Tracks the Elevated Status of SpatialDialog */
+internal interface DialogManager {
+ /** `true` when a SpatialDialog is triggered, `false` otherwise. */
+ public val isSpatialDialogActive: MutableState<Boolean>
+}
+
+/** Default implementation of [DialogManager]. */
+private class DefaultDialogManager : DialogManager {
+ override val isSpatialDialogActive: MutableState<Boolean> = mutableStateOf(false)
+}
+
+internal val LocalDialogManager: ProvidableCompositionLocal<DialogManager> =
+ staticCompositionLocalOf<DialogManager> { DefaultDialogManager() }
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/GlobalSnapshotManager.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/GlobalSnapshotManager.kt
new file mode 100644
index 0000000..fe8cf9e
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/GlobalSnapshotManager.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.platform.AndroidUiDispatcher
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.consumeEach
+import kotlinx.coroutines.launch
+
+/**
+ * Platform-specific mechanism for starting a monitor of global snapshot state writes in order to
+ * schedule the periodic dispatch of snapshot apply notifications.
+ *
+ * This process should remain platform-specific; it is tied to the threading and update model of a
+ * particular platform and framework target.
+ *
+ * Composition bootstrapping mechanisms for a particular platform/framework should call
+ * [ensureStarted] during setup to initialize periodic global snapshot notifications.
+ *
+ * For Android, these notifications are always sent on [AndroidUiDispatcher.Main]. Other platforms
+ * may establish different policies for these notifications.
+ *
+ * See [androidx.compose.ui.platform.GlobalSnapshotManager]
+ */
+internal object GlobalSnapshotManager {
+ private val started = AtomicBoolean(false)
+ private val sent = AtomicBoolean(false)
+
+ fun ensureStarted() {
+ if (started.compareAndSet(false, true)) {
+ val channel = Channel<Unit>(1)
+ CoroutineScope(AndroidUiDispatcher.Main).launch {
+ channel.consumeEach {
+ sent.set(false)
+ Snapshot.sendApplyNotifications()
+ }
+ }
+ Snapshot.registerGlobalWriteObserver {
+ if (sent.compareAndSet(false, true)) {
+ @Suppress("UNUSED_VARIABLE") val unused = channel.trySend(Unit)
+ }
+ }
+ }
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/InternalSubspaceApi.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/InternalSubspaceApi.kt
new file mode 100644
index 0000000..96d3df0
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/InternalSubspaceApi.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+@RequiresOptIn(level = RequiresOptIn.Level.ERROR, message = "This API is internal to the library.")
+@Target(
+ AnnotationTarget.CLASS,
+ AnnotationTarget.FUNCTION,
+ AnnotationTarget.PROPERTY,
+ AnnotationTarget.FIELD,
+ AnnotationTarget.PROPERTY_GETTER,
+)
+@Retention(AnnotationRetention.BINARY)
+internal annotation class InternalSubspaceApi
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/LocalPanelEntity.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/LocalPanelEntity.kt
new file mode 100644
index 0000000..d05da44
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/LocalPanelEntity.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.compositionLocalOf
+import androidx.xr.scenecore.PanelEntity
+
+/**
+ * A CompositionLocal that holds the current [PanelEntity] acting as the parent panel for any
+ * containing composed UI.
+ */
+internal val LocalPanelEntity: ProvidableCompositionLocal<PanelEntity?> =
+ compositionLocalOf<PanelEntity?> { null }
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/LocalSession.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/LocalSession.kt
new file mode 100644
index 0000000..4310a8e
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/LocalSession.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import androidx.annotation.RestrictTo
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.compositionLocalWithComputedDefaultOf
+import androidx.compose.ui.platform.LocalContext
+import androidx.xr.scenecore.Session
+
+/**
+ * A composition local that provides the current Jetpack XR [Session].
+ *
+ * In non-XR environments, this composition local will return `null`.
+ */
+@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public val LocalSession: ProvidableCompositionLocal<Session?> =
+ compositionLocalWithComputedDefaultOf {
+ if (SpatialConfiguration.hasXrSpatialFeature(LocalContext.currentValue)) {
+ Session.create(LocalContext.currentValue.getActivity())
+ } else {
+ null
+ }
+ }
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/Logger.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/Logger.kt
new file mode 100644
index 0000000..ef412ee
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/Logger.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.util.Log
+import androidx.annotation.RestrictTo
+
+/**
+ * Logger for Subspace Compose. This is a simple wrapper around Log.v that is only enabled if the
+ * androidx.xr.compose.platform.Logger metadata flag is set in the AndroidManifest.xml.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public object Logger {
+ private const val METADATA_KEY = "androidx.xr.compose.platform.Logger"
+ private var isDebug: Boolean = false
+
+ /**
+ * Initializes the logger. This method expects
+ * android:name="androidx.xr.compose.platform.Logger" to be set in a metadata tag in the
+ * AndroidManifest.xml. Typically the value of this key is set to false but can be set to true
+ * to enable debug logging. For example:
+ *
+ * <meta-data android:name="androidx.xr.compose.platform.Logger" android:value="true" />
+ *
+ * @param context The application context for the current application.
+ */
+ public fun init(context: Context) {
+ isDebug =
+ context.packageManager
+ .getApplicationInfo(context.packageName, PackageManager.GET_META_DATA)
+ .metaData
+ .getBoolean(METADATA_KEY, false)
+ }
+
+ /**
+ * Logs a message to the logcat if debugging was enabled in the init method.
+ *
+ * @param tag The tag to use for the log message.
+ * @param messageGenerator This generates the message to log.
+ */
+ public fun log(tag: String, messageGenerator: () -> String) {
+ if (isDebug) {
+ Log.v(tag, messageGenerator())
+ }
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SceneManager.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SceneManager.kt
new file mode 100644
index 0000000..3656543
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SceneManager.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import android.util.CloseGuard
+import androidx.annotation.RestrictTo
+import androidx.xr.compose.subspace.node.SubspaceSemanticsNode
+
+/**
+ * Manager for all [SpatialComposeScene]s that are created when the [SceneManager] is running.
+ *
+ * This is used by the testing framework to keep track of all scene compositions that were created
+ * for the purpose of finding the semantic roots.
+ */
+@Suppress("NotCloseable")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public object SceneManager : AutoCloseable {
+ private val registeredScenes: MutableList<SpatialComposeScene> = mutableListOf()
+ private var isRunning = false
+ private val guard = CloseGuard()
+
+ /**
+ * Start keeping track of the scenes that are created. Scenes created before [SceneManager] is
+ * running will not be tracked.
+ */
+ public fun start() {
+ isRunning = true
+ guard.open("stop")
+ }
+
+ /**
+ * Stop tracking the created scenes and clear the set of scenes that [SceneManager] was keeping
+ * track of.
+ */
+ public fun stop() {
+ guard.close()
+ isRunning = false
+ registeredScenes.clear()
+ }
+
+ /** Alias to [SceneManager.stop] To implement the [AutoCloseable] interface. */
+ override fun close() {
+ stop()
+ }
+
+ internal fun onSceneCreated(scene: SpatialComposeScene) {
+ if (isRunning) {
+ registeredScenes.add(scene)
+ }
+ }
+
+ internal fun onSceneDisposed(scene: SpatialComposeScene) {
+ if (isRunning) {
+ registeredScenes.remove(scene)
+ }
+ }
+
+ /**
+ * Returns all root subspace semantics nodes of all registered scenes.
+ *
+ * [SceneManager.start] should be called before attempting to get the root subspace semantics
+ * nodes. This will throw an [IllegalStateException] if the [SceneManager] is not in a running
+ * state.
+ */
+ public fun getAllRootSubspaceSemanticsNodes(): List<SubspaceSemanticsNode> {
+ check(isRunning) { "SceneManager is not started. Call SceneManager.start() first." }
+ return registeredScenes.map { SubspaceSemanticsNode(it.rootElement.compositionOwner.root) }
+ }
+
+ public fun getSceneCount(): Int {
+ return registeredScenes.size
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SessionCallbackProvider.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SessionCallbackProvider.kt
new file mode 100644
index 0000000..1f91439
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SessionCallbackProvider.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import androidx.annotation.RestrictTo
+import androidx.xr.scenecore.Dimensions
+import androidx.xr.scenecore.Session
+import java.io.Closeable
+
+/** Provides a [SessionCallbacks] for the current Activity. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun interface SessionCallbackProvider {
+ public operator fun get(session: Session): SessionCallbacks
+
+ public companion object {
+ public val default: SessionCallbackProvider =
+ object : SessionCallbackProvider {
+ val callbackMap = mutableMapOf<Session, SessionCallbacks>()
+
+ override fun get(session: Session): SessionCallbacks =
+ callbackMap.computeIfAbsent(session, ::DefaultSessionCallbacks)
+ }
+ }
+}
+
+/** Class to store callback lambdas associated with the Session. */
+@Suppress("SingularCallback")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface SessionCallbacks {
+ /**
+ * Register a [callback] to be triggered when the app goes into full space mode. It will be
+ * called immediately upon registration if the system is currently in full space mode and its
+ * state is known.
+ *
+ * @param callback the method that will be called when the application enters full space mode.
+ * @return a closeable to unregister the callback
+ */
+ public fun onFullSpaceMode(callback: () -> Unit): Closeable
+
+ /**
+ * Register a [callback] to be triggered when the app goes into home space mode. It will be
+ * called immediately upon registration if the system is currently in home space mode and its
+ * state is known.
+ *
+ * @param callback the method that will be called when the application enters home space mode.
+ * It will be called with the current application bounds as its argument.
+ * @return a closeable to unregister the callback
+ */
+ public fun onHomeSpaceMode(callback: (Dimensions) -> Unit): Closeable
+
+ /**
+ * Register a [callback] to be triggered when the app changes space mode. It will be called
+ * immediately with the current state if that state is available.
+ *
+ * @param callback the method that will be called when the space mode changes. It will accept an
+ * enum [SpaceMode] value indicating the current mode.
+ * @return a closeable to unregister the callback
+ */
+ public fun onSpaceModeChanged(callback: (spaceMode: SpaceMode) -> Unit): Closeable
+
+ /**
+ * Register a [callback] to be triggered when the bounds of the app change. It will be called
+ * immediately with the current state if that state is available.
+ *
+ * @param callback the method that will be called when the application bounds change. The
+ * argument passed to the callback represents the current bounds of the application.
+ * @return a closeable to unregister the callback
+ */
+ public fun onBoundsChanged(callback: (Dimensions) -> Unit): Closeable
+}
+
+/**
+ * Represents immersion level capabilities of the current application environment.
+ *
+ * In XR environments, there are a few different modes that an application can run in:
+ * - Home Space Mode - the application can run side-by-side with other applications for
+ * multitasking; however, spatialization (e.g. Orbiters, Panels, 3D objects, etc.) is not allowed.
+ * - Full Space Mode - the application is given the entire space and has access to spatialization.
+ * It may render multiple panels and 3D objects.
+ */
+@JvmInline
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public value class SpaceMode private constructor(public val value: Int) {
+ public companion object {
+ /**
+ * Current space mode or capabilities unknown. Awaiting checks to complete to indicate the
+ * proper space mode.
+ */
+ public val Unspecified: SpaceMode = SpaceMode(0)
+
+ /**
+ * Space mode is not applicable to the current system. This indicates that the current
+ * system is not XR (e.g. Phone, Tablet, etc.)
+ */
+ public val NotApplicable: SpaceMode = SpaceMode(1)
+
+ /**
+ * The XR application is currently in Full Space Mode.
+ *
+ * In Full Space Mode, the application is given the entire space for its own content. It has
+ * access to spatialization and may render multiple panels and 3D objects.
+ */
+ public val Full: SpaceMode = SpaceMode(2)
+
+ /**
+ * The XR application is currently in Home Space Mode.
+ *
+ * In Home Space Mode, the application does not have access to spatialization. It may not
+ * render multiple panels, orbiters, or 3D objects. However, it may run side-by-side with
+ * other applications for multitasking.
+ */
+ public val Home: SpaceMode = SpaceMode(3)
+ }
+}
+
+private class DefaultSessionCallbacks(session: Session) : SessionCallbacks {
+ private var currentDimensions: Dimensions? = null
+ private val onBoundsChangeListeners: MutableList<(Dimensions) -> Unit> = mutableListOf()
+
+ init {
+ // TODO: b/370618645 - This uses a direct executor for the callbacks, to maintain consistent
+ // behavior for clients using Closeables returned by other methods in this class. We should
+ // reconsider this approach, see the linked bug for more details.
+ session.activitySpace.addBoundsChangedListener({ runnable -> runnable.run() }) {
+ onBoundsChangeListeners.forEach { callback -> callback.invoke(it) }
+ currentDimensions = it
+ }
+ }
+
+ override fun onFullSpaceMode(callback: () -> Unit): Closeable = add {
+ if (it.isFullSpace) {
+ callback()
+ }
+ }
+
+ override fun onHomeSpaceMode(callback: (Dimensions) -> Unit): Closeable = add {
+ if (!it.isFullSpace) {
+ callback(it)
+ }
+ }
+
+ override fun onSpaceModeChanged(callback: (SpaceMode) -> Unit): Closeable = add {
+ // Invoke the callback if the current full space state is not equal to the next full
+ // space state or if the current dimensions are the same object reference as the
+ // next dimensions (initial call when this callback is registered)
+ if (it.isFullSpace != currentDimensions?.isFullSpace || it === currentDimensions) {
+ callback(if (it.isFullSpace) SpaceMode.Full else SpaceMode.Home)
+ }
+ }
+
+ override fun onBoundsChanged(callback: (Dimensions) -> Unit): Closeable = add(callback)
+
+ private fun add(callback: (nextDimensions: Dimensions) -> Unit): Closeable {
+ if (currentDimensions != null) {
+ callback(currentDimensions!!)
+ }
+ onBoundsChangeListeners.add(callback)
+ return Closeable {
+
+ // Tests break if this line stays uncommented
+ onBoundsChangeListeners.remove(callback)
+ }
+ }
+}
+
+private val Dimensions.isFullSpace: Boolean
+ get() =
+ width == Float.POSITIVE_INFINITY &&
+ height == Float.POSITIVE_INFINITY &&
+ depth == Float.POSITIVE_INFINITY
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SpatialCapabilities.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SpatialCapabilities.kt
new file mode 100644
index 0000000..d30726e
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SpatialCapabilities.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import androidx.annotation.RestrictTo
+import androidx.compose.runtime.CompositionLocal
+import androidx.compose.runtime.compositionLocalWithComputedDefaultOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.xr.scenecore.Session
+import androidx.xr.scenecore.SpatialCapabilities.Companion.SPATIAL_CAPABILITY_3D_CONTENT
+import androidx.xr.scenecore.SpatialCapabilities.Companion.SPATIAL_CAPABILITY_APP_ENVIRONMENT
+import androidx.xr.scenecore.SpatialCapabilities.Companion.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL
+import androidx.xr.scenecore.SpatialCapabilities.Companion.SPATIAL_CAPABILITY_SPATIAL_AUDIO
+import androidx.xr.scenecore.SpatialCapabilities.Companion.SPATIAL_CAPABILITY_UI
+
+@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public val LocalSpatialCapabilities: CompositionLocal<SpatialCapabilities> =
+ compositionLocalWithComputedDefaultOf {
+ if (LocalHasXrSpatialFeature.currentValue) {
+ SpatialCapabilities.getOrCreate(
+ checkNotNull(LocalSession.currentValue) { "Session must be initialized." }
+ )
+ } else {
+ NoSpatialCapabilities()
+ }
+ }
+
+/**
+ * Provides information and functionality related to the spatial capabilities of the application.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface SpatialCapabilities {
+ /**
+ * Indicates whether the application may create spatial UI elements (e.g. SpatialPanel).
+ *
+ * This is a State-based value that should trigger recomposition in composable functions.
+ */
+ public val isSpatialUiEnabled: Boolean
+
+ /**
+ * Indicates whether the application may create 3D objects.
+ *
+ * This is a State-based value that should trigger recomposition in composable functions.
+ */
+ public val isContent3dEnabled: Boolean
+
+ /**
+ * Indicates whether the application may set the environment.
+ *
+ * This is a State-based value that should trigger recomposition in composable functions.
+ */
+ public val isAppEnvironmentEnabled: Boolean
+
+ /**
+ * Indicates whether the application may control the passthrough state.
+ *
+ * This is a State-based value that should trigger recomposition in composable functions.
+ */
+ public val isPassthroughControlEnabled: Boolean
+
+ /**
+ * Indicates whether the application may use spatial audio.
+ *
+ * This is a State-based value that should trigger recomposition in composable functions.
+ */
+ public val isSpatialAudioEnabled: Boolean
+
+ public companion object {
+ private val sessionInstances: MutableMap<Session, SpatialCapabilities> = mutableMapOf()
+
+ public fun getOrCreate(session: Session): SpatialCapabilities =
+ sessionInstances.getOrPut(session) { SessionSpatialCapabilities(session) }
+ }
+}
+
+private class SessionSpatialCapabilities(session: Session) : SpatialCapabilities {
+ private var capabilities by
+ mutableStateOf(session.getSpatialCapabilities()).apply {
+ session.addSpatialCapabilitiesChangedListener { value = it }
+ }
+
+ override val isSpatialUiEnabled: Boolean
+ get() = capabilities.hasCapability(SPATIAL_CAPABILITY_UI)
+
+ override val isContent3dEnabled: Boolean
+ get() = capabilities.hasCapability(SPATIAL_CAPABILITY_3D_CONTENT)
+
+ override val isAppEnvironmentEnabled: Boolean
+ get() = capabilities.hasCapability(SPATIAL_CAPABILITY_APP_ENVIRONMENT)
+
+ override val isPassthroughControlEnabled: Boolean
+ get() = capabilities.hasCapability(SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL)
+
+ override val isSpatialAudioEnabled: Boolean
+ get() = capabilities.hasCapability(SPATIAL_CAPABILITY_SPATIAL_AUDIO)
+}
+
+private class NoSpatialCapabilities : SpatialCapabilities {
+ override val isSpatialUiEnabled: Boolean = false
+ override val isContent3dEnabled: Boolean = false
+ override val isAppEnvironmentEnabled: Boolean = false
+ override val isPassthroughControlEnabled: Boolean = false
+ override val isSpatialAudioEnabled: Boolean = false
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SpatialComposeElement.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SpatialComposeElement.kt
new file mode 100644
index 0000000..d49541d
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SpatialComposeElement.kt
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composition
+import androidx.compose.runtime.CompositionContext
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.InternalComposeUiApi
+import androidx.xr.compose.subspace.SubspaceComposable
+import androidx.xr.compose.subspace.layout.CoreEntity
+
+/**
+ * Base class for custom [SpatialElement]s implemented using Jetpack Compose UI.
+ *
+ * Subclasses should implement the [Content] function with the appropriate content.
+ *
+ * Attempts to call [addChild] or its variants/overloads will result in an [IllegalStateException].
+ *
+ * This class is based on the existing `AbstractComposeView` class in
+ * [androidx.compose.ui.platform].
+ *
+ * @property compositionContext The [CompositionContext] used for compositions of this element's
+ * hierarchy.
+ *
+ * If this composition was created as the top level composition in the hierarchy, then the
+ * recomposer will be cancelled if this element is detached from the subspace. Therefore, this
+ * instance should not be shared or reused in other trees.
+ *
+ * @property rootCoreEntity The [CoreEntity] associated with the root of this composition. This root
+ * CoreEntity will be the parent entity of the entire composition.
+ *
+ * It is not necessary for a composition to have a root entity; however, it may be provided to
+ * ensure that the composition is properly parented when it is a sub-composition of another
+ * composition.
+ */
+internal abstract class AbstractComposeElement(
+ internal var compositionContext: CompositionContext? = null,
+ internal val rootCoreEntity: CoreEntity? = null,
+) : SpatialElement() {
+
+ /**
+ * Whether the composition should be created when the element is attached to a
+ * [SpatialComposeScene].
+ *
+ * If `true`, this [SpatialElement]'s composition will be created when it is attached to a
+ * [SpatialComposeScene] for the first time. Defaults to `true`.
+ *
+ * Subclasses may override this property to prevent eager initial composition if the element's
+ * content is not yet ready.
+ */
+ @get:Suppress("GetterSetterNames")
+ protected open val shouldCreateCompositionOnAttachedToSpatialComposeScene: Boolean
+ get() = true
+
+ private var creatingComposition = false
+
+ private var composition: Composition? = null
+
+ /**
+ * The [AndroidComposeSpatialElement] that will be used to host the composition for this
+ * element.
+ */
+ internal val compositionOwner: AndroidComposeSpatialElement = AndroidComposeSpatialElement()
+
+ /**
+ * The Jetpack Compose [SubspaceComposable] UI content for this element.
+ *
+ * Subclasses must implement this method to provide content. Initial composition will occur when
+ * the element is attached to a [SpatialComposeScene] or when [createComposition] is called,
+ * whichever comes first.
+ */
+ @Composable @SubspaceComposable protected abstract fun Content()
+
+ override fun addChild(element: SpatialElement) {
+ if (!creatingComposition) {
+ throw UnsupportedOperationException(
+ "May only add $element to $this during composition."
+ )
+ }
+
+ super.addChild(element)
+ }
+
+ override fun onAttachedToSubspace(spatialComposeScene: SpatialComposeScene) {
+ super.onAttachedToSubspace(spatialComposeScene)
+
+ if (shouldCreateCompositionOnAttachedToSpatialComposeScene) {
+ createComposition()
+ }
+ }
+
+ /**
+ * Performs the initial composition for this element.
+ *
+ * This method has no effect if the composition has already been created.
+ *
+ * This method should only be called if this element is attached to a [SpatialComposeScene] or
+ * if a parent [CompositionContext] has been set explicitly.
+ */
+ @OptIn(InternalComposeUiApi::class)
+ protected fun createComposition() {
+ if (composition != null) return
+
+ check(isAttachedToSpatialComposeScene) {
+ "Element.createComposition() requires the Element to be attached to the subspace."
+ }
+ check(!hasAncestorWithCompositionContext()) {
+ "Cannot construct a composition for $this element. The tree it is currently attached " +
+ "to has another composition context."
+ }
+ check(children.isEmpty()) {
+ "Cannot set the composable content. $parent element already contains a subtree."
+ }
+
+ creatingComposition = true
+
+ GlobalSnapshotManager.ensureStarted()
+ addChild(compositionOwner)
+ compositionOwner.root.coreEntity = rootCoreEntity
+ composition =
+ WrappedComposition(
+ compositionOwner,
+ compositionContext
+ ?: SubspaceRecomposerPolicy.createAndInstallSubspaceRecomposer(this),
+ )
+ .also {
+ compositionOwner.wrappedComposition = it
+ it.setContent { Content() }
+ }
+
+ creatingComposition = false
+ }
+
+ /** Whether any of the ancestor elements have a [CompositionContext]. */
+ private fun hasAncestorWithCompositionContext(): Boolean =
+ generateSequence(parent) { it.parent }
+ .any { it is AbstractComposeElement && it.compositionContext != null }
+
+ /**
+ * Disposes the composition for this element.
+ *
+ * This method has no effect if the composition has already been disposed.
+ */
+ public fun disposeComposition() {
+ composition?.dispose()
+ composition = null
+ }
+}
+
+/**
+ * An [SpatialElement] that can host Jetpack Compose [SubspaceComposable] content.
+ *
+ * Use [setContent] to provide the content composable function for the element.
+ *
+ * This class is based on the existing `ComposeView` class in [androidx.compose.ui.platform].
+ *
+ * @param scene The [SpatialComposeScene] that this element is attached to.
+ * @param compositionContext the [CompositionContext] from a parent composition to propagate
+ * composition state. Should be `null` when this instance is the top-level composition context, in
+ * which case a new [CompositionContext] will be created. This value should be provided when this
+ * instance is a sub-composition of another composition.
+ * @param rootCoreEntity The [CoreEntity] associated with the root layout of this composition (see
+ * [AbstractComposeElement.rootCoreEntity])
+ */
+internal class SpatialComposeElement(
+ scene: SpatialComposeScene,
+ compositionContext: CompositionContext? = null,
+ rootCoreEntity: CoreEntity? = null,
+) : AbstractComposeElement(compositionContext, rootCoreEntity) {
+ init {
+ spatialComposeScene = scene
+ }
+
+ private val content = mutableStateOf<(@Composable @SubspaceComposable () -> Unit)?>(null)
+
+ @get:Suppress("GetterSetterNames")
+ override var shouldCreateCompositionOnAttachedToSpatialComposeScene: Boolean = false
+ private set
+
+ @Composable
+ @SubspaceComposable
+ override fun Content() {
+ content.value?.invoke()
+ }
+
+ /**
+ * Sets the Jetpack Compose [SubspaceComposable] UI content for this element.
+ *
+ * Initial composition will occur when the element is attached to a [SpatialComposeScene] or
+ * when [createComposition] is called, whichever comes first.
+ *
+ * @param content the composable content to display in this element.
+ */
+ public fun setContent(content: @Composable @SubspaceComposable () -> Unit) {
+ shouldCreateCompositionOnAttachedToSpatialComposeScene = true
+
+ this.content.value = content
+
+ if (isAttachedToSpatialComposeScene) {
+ createComposition()
+ }
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SpatialComposeScene.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SpatialComposeScene.kt
new file mode 100644
index 0000000..618b9fa
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SpatialComposeScene.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import androidx.activity.ComponentActivity
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionContext
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.xr.compose.subspace.SubspaceComposable
+import androidx.xr.compose.subspace.layout.CoreEntity
+import androidx.xr.scenecore.Session
+
+/**
+ * A 3D scene represented via Compose elements and coordinated with SceneCore.
+ *
+ * This class manages the lifecycle and root element of the spatial scene. It also provides access
+ * to the SceneCore session and environment.
+ *
+ * @param parentCompositionContext the optional composition context when this is a sub-composition
+ * @param rootEntity the optional [CoreEntity] to associate with the root of this composition
+ * @property ownerActivity the [ComponentActivity] that owns this scene.
+ * @property jxrSession the [Session] used to interact with SceneCore.
+ */
+internal class SpatialComposeScene(
+ /** Context of the activity that this scene is rooted on. */
+ public val ownerActivity: ComponentActivity,
+ @InternalSubspaceApi public val jxrSession: Session,
+ parentCompositionContext: CompositionContext? = null,
+ rootEntity: CoreEntity? = null,
+) : DefaultLifecycleObserver, LifecycleOwner {
+ init {
+ SceneManager.onSceneCreated(this)
+ }
+
+ /** Root of the spatial scene graph of this [SpatialComposeScene]. */
+ internal val rootElement: SpatialComposeElement =
+ SpatialComposeElement(this, parentCompositionContext, rootEntity)
+
+ public fun setContent(content: @Composable @SubspaceComposable () -> Unit) {
+ rootElement.setContent(content)
+ }
+
+ public fun dispose() {
+ rootElement.disposeComposition()
+ SceneManager.onSceneDisposed(this)
+ }
+
+ override val lifecycle: Lifecycle
+ get() = ownerActivity.lifecycle
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SpatialConfiguration.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SpatialConfiguration.kt
new file mode 100644
index 0000000..3e7f1d6
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SpatialConfiguration.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import android.content.Context
+import androidx.annotation.RestrictTo
+import androidx.compose.runtime.CompositionLocal
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.compositionLocalWithComputedDefaultOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.xr.compose.unit.DpVolumeSize
+import androidx.xr.compose.unit.toDpVolumeSize
+import androidx.xr.scenecore.Session
+
+/**
+ * The name of the system feature that indicates whether the system supports XR Spatial features.
+ */
+internal const val XR_IMMERSIVE_FEATURE = "android.software.xr.immersive"
+
+/** CompositionLocal indicating whether the system XR Spatial feature is enabled. */
+@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public val LocalHasXrSpatialFeature: ProvidableCompositionLocal<Boolean> =
+ compositionLocalWithComputedDefaultOf {
+ SpatialConfiguration.hasXrSpatialFeature(LocalContext.currentValue)
+ }
+
+@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public val LocalSpatialConfiguration: CompositionLocal<SpatialConfiguration> =
+ compositionLocalWithComputedDefaultOf {
+ if (LocalHasXrSpatialFeature.currentValue) {
+ SpatialConfiguration.getOrCreate(
+ checkNotNull(LocalSession.currentValue) { "session must be initialized" },
+ LocalHasXrSpatialFeature.currentValue,
+ )
+ } else {
+ ContextOnlySpatialConfiguration(LocalContext.currentValue)
+ }
+ }
+
+/**
+ * Provides information and functionality related to the spatial configuration of the application.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface SpatialConfiguration {
+ /**
+ * A volume whose width, height, and depth represent the space available to the application.
+ *
+ * In XR, an application's available space is not related to the display or window dimensions;
+ * instead, it will always be some subset of the virtual 3D space. The app bounds will change
+ * when switching between home space or full space modes.
+ *
+ * In non-XR environments, the width and height will represent the screen width and height
+ * available to the application (see [android.content.res.Configuration.screenWidthDp] and
+ * [android.content.res.Configuration.screenHeightDp]) and the depth will be zero.
+ *
+ * This is a state-based value that will trigger recomposition.
+ */
+ public val bounds: DpVolumeSize
+ get() = DpVolumeSize.Zero
+
+ /**
+ * XR Spatial APIs are supported for this system. This is equivalent to
+ * PackageManager.hasSystemFeature(FEATURE_XR_SPATIAL, version) where version is the minimum
+ * version for features available in the XR Compose library used.
+ */
+ @Suppress("INAPPLICABLE_JVM_NAME")
+ @get:JvmName("hasXrSpatialFeature")
+ public val hasXrSpatialFeature: Boolean
+ get() = false
+
+ /**
+ * Request that the system places the application into home space mode. This will execute
+ * asynchronously. If it completes successfully then [bounds] will change. This method will
+ * throw an [UnsupportedOperationException] if the application is not in an XR environment.
+ *
+ * In home space, the visible space may be shared with other applications; however, applications
+ * in home space will have their spatial capabilities and physical bounds limited.
+ */
+ public fun requestHomeSpaceMode() {
+ throw UnsupportedOperationException(
+ "Cannot request mode changes when not in an Android XR environment."
+ )
+ }
+
+ /**
+ * Request that the system places the application into full space mode. This will execute
+ * asynchronously. If it completes successfully then [bounds] will change. This method will
+ * throw an [UnsupportedOperationException] if the application is not in an XR environment.
+ *
+ * In full space, this application will be the only application in the visible space, its
+ * spatial capabilities will be expanded, and its physical bounds will expand to fill the entire
+ * virtual space.
+ */
+ public fun requestFullSpaceMode() {
+ throw UnsupportedOperationException(
+ "Cannot request mode changes when not in an Android XR environment."
+ )
+ }
+
+ public companion object {
+ /**
+ * XR Spatial APIs are supported for this system. This is equivalent to
+ * PackageManager.hasSystemFeature(FEATURE_XR_SPATIAL, version) where version is the minimum
+ * version for features available in the XR Compose library used.
+ */
+ public fun hasXrSpatialFeature(context: Context): Boolean {
+ return context.packageManager.hasSystemFeature(XR_IMMERSIVE_FEATURE)
+ }
+
+ private val sessionInstances: MutableMap<Session, SpatialConfiguration> = mutableMapOf()
+
+ public fun getOrCreate(
+ session: Session,
+ hasXrSpatialFeature: Boolean
+ ): SpatialConfiguration =
+ sessionInstances.getOrPut(session) {
+ SessionSpatialConfiguration(session, hasXrSpatialFeature)
+ }
+ }
+}
+
+/** A [SpatialConfiguration] that only has access to the current activity context. */
+private class ContextOnlySpatialConfiguration(private val context: Context) : SpatialConfiguration {
+ override val hasXrSpatialFeature: Boolean
+ get() = SpatialConfiguration.hasXrSpatialFeature(context)
+
+ override val bounds: DpVolumeSize
+ get() =
+ DpVolumeSize(
+ context.getActivity().resources.configuration.screenWidthDp.dp,
+ context.getActivity().resources.configuration.screenHeightDp.dp,
+ 0.dp,
+ )
+}
+
+/** A [SpatialConfiguration] that is attached to the current [Session]. */
+private class SessionSpatialConfiguration(
+ private val session: Session,
+ override val hasXrSpatialFeature: Boolean,
+) : SpatialConfiguration {
+ private var boundsState by
+ mutableStateOf(session.activitySpace.getBounds()).apply {
+ session.activitySpace.addBoundsChangedListener { value = it }
+ }
+
+ override val bounds: DpVolumeSize
+ get() = boundsState.toDpVolumeSize()
+
+ override fun requestHomeSpaceMode() {
+ session.requestHomeSpaceMode()
+ }
+
+ override fun requestFullSpaceMode() {
+ session.requestFullSpaceMode()
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SpatialElement.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SpatialElement.kt
new file mode 100644
index 0000000..040eeae
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SpatialElement.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import androidx.annotation.CallSuper
+
+/**
+ * Represents the basic building block for a [SpatialElement] that can contain other
+ * [SpatialElement] instances.
+ *
+ * It provides functionality for managing child SpatialElements and for attaching and detaching from
+ * a [SpatialComposeScene].
+ */
+internal open class SpatialElement {
+
+ /**
+ * Interface definition for callbacks that are invoked when this element's attachment state
+ * changes.
+ */
+ public interface OnAttachStateChangeListener {
+ /**
+ * Called when the [SpatialElement] is attached to a [SpatialComposeScene].
+ *
+ * @param spatialComposeScene The [SpatialComposeScene] that the element was attached to.
+ */
+ public fun onElementAttachedToSubspace(spatialComposeScene: SpatialComposeScene) {}
+
+ /**
+ * Called when the [SpatialElement] is detached from a [SpatialComposeScene].
+ *
+ * @param spatialComposeScene The [SpatialComposeScene] that the element was detached from.
+ */
+ public fun onElementDetachedFromSubspace(spatialComposeScene: SpatialComposeScene) {}
+ }
+
+ /**
+ * The parent of this [SpatialElement].
+ *
+ * This property is `null` if the element is not attached to a parent [SpatialElement].
+ */
+ public var parent: SpatialElement? = null
+ internal set(parentEl) {
+ field = parentEl
+ spatialComposeScene = parentEl?.spatialComposeScene
+ }
+
+ /**
+ * The [SpatialComposeScene] that this element is attached to, or `null` if it is not attached
+ * to any subspace.
+ */
+ public var spatialComposeScene: SpatialComposeScene? = null
+ set(value) {
+ if (field == value) {
+ return
+ }
+
+ if (value == null) {
+ val oldScene = checkNotNull(field) { "Scene must be non-null before clearing." }
+ onDetachedFromSubspace(oldScene)
+ onAttachStateChangeListeners?.forEach { it.onElementDetachedFromSubspace(oldScene) }
+ } else {
+ onAttachedToSubspace(value)
+ onAttachStateChangeListeners?.forEach { it.onElementAttachedToSubspace(value) }
+ }
+ field = value
+ }
+
+ /** Whether the element is attached to a [SpatialComposeScene]. */
+ protected val isAttachedToSpatialComposeScene: Boolean
+ get() = spatialComposeScene != null
+
+ private var onAttachStateChangeListeners: MutableList<OnAttachStateChangeListener>? = null
+
+ /**
+ * Detaches this element from its parent [SpatialElement], setting the [parent] to `null` and
+ * removing from parent's [SpatialElement.children] list.
+ */
+ private fun detachFromParent() {
+ @Suppress("UNUSED_VARIABLE") val unused = parent?.removeChild(this)
+ parent = null
+ }
+
+ /**
+ * Registers the [listener] whose callbacks are invoked when this [SpatialElement]'s attachment
+ * state to [SpatialComposeScene] changes.
+ *
+ * Use [removeOnAttachStateChangeListener] to unregister the [listener].
+ */
+ @Suppress("ExecutorRegistration")
+ public fun addOnAttachStateChangeListener(listener: OnAttachStateChangeListener) {
+ val listeners = onAttachStateChangeListeners
+
+ if (listeners == null) {
+ onAttachStateChangeListeners = mutableListOf(listener)
+ } else {
+ listeners.add(listener)
+ }
+ }
+
+ /**
+ * Registers a one-time callback to be invoked when this [SpatialElement] is detached from the
+ * [SpatialComposeScene].
+ *
+ * @param listener the callback to be invoked when the element is detached.
+ */
+ public fun onDetachedFromSubspaceOnce(listener: (SpatialComposeScene) -> Unit) {
+ addOnAttachStateChangeListener(
+ object : OnAttachStateChangeListener {
+ override fun onElementDetachedFromSubspace(
+ spatialComposeScene: SpatialComposeScene
+ ) {
+ removeOnAttachStateChangeListener(this)
+ listener(spatialComposeScene)
+ }
+ }
+ )
+ }
+
+ /**
+ * Removes and unregisters the previously registered [listener].
+ *
+ * The [listener] will no longer receive any further notification whenever [spatialComposeScene]
+ * attachment changes.
+ */
+ public fun removeOnAttachStateChangeListener(listener: OnAttachStateChangeListener) {
+ onAttachStateChangeListeners?.remove(listener)
+ }
+
+ private val _children = mutableListOf<SpatialElement>()
+
+ /** Immediate children of this [SpatialElement]. */
+ public val children: List<SpatialElement>
+ get() = _children
+
+ /**
+ * Appends the given [element] to the [children] of this element.
+ *
+ * @param element the element to add as a child.
+ */
+ @CallSuper
+ public open fun addChild(element: SpatialElement) {
+ element.parent = this
+ _children.add(element)
+ }
+
+ /**
+ * Removes the given [element] from the [children] of this element.
+ *
+ * @param element the element to remove as a child.
+ * @return `true` if element was successfully removed from the list, `false` otherwise.
+ */
+ @CallSuper
+ public open fun removeChild(element: SpatialElement): Boolean {
+ if (_children.remove(element)) {
+ element.parent = null
+ return true
+ }
+
+ return false
+ }
+
+ /** Detaches all of its children and clears the [children] list. */
+ @CallSuper
+ public open fun removeChildren() {
+ _children.forEach { it.parent = null }
+ _children.clear()
+ }
+
+ /**
+ * Update children's spatialComposeScene to [SpatialComposeScene] when this element is attached
+ * to it.
+ *
+ * @param spatialComposeScene the [SpatialComposeScene] this element is attached to.
+ */
+ @CallSuper
+ public open fun onAttachedToSubspace(spatialComposeScene: SpatialComposeScene) {
+ // Make sure all children have the same `spatialComposeScene` reference too.
+ _children.forEach { it.spatialComposeScene = spatialComposeScene }
+ }
+
+ /**
+ * Called when this element is detached from a [SpatialComposeScene].
+ *
+ * @param spatialComposeScene the [SpatialComposeScene] this element is detached from.
+ */
+ @CallSuper
+ public open fun onDetachedFromSubspace(spatialComposeScene: SpatialComposeScene) {
+ // make sure `spatialComposeScene` references of all children are cleaned up too.
+ _children.forEach { it.spatialComposeScene = null }
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SubspaceRecomposer.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SubspaceRecomposer.kt
new file mode 100644
index 0000000..cad2eb6
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/SubspaceRecomposer.kt
@@ -0,0 +1,335 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import android.content.Context
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Looper
+import android.provider.Settings
+import androidx.compose.runtime.MonotonicFrameClock
+import androidx.compose.runtime.PausableMonotonicFrameClock
+import androidx.compose.runtime.Recomposer
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.InternalComposeUiApi
+import androidx.compose.ui.MotionDurationScale
+import androidx.compose.ui.platform.AndroidUiDispatcher
+import androidx.core.os.HandlerCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.coroutines.ContinuationInterceptor
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/** See [androidx.compose.ui.platform.WindowRecomposerPolicy] */
+@InternalComposeUiApi
+internal object SubspaceRecomposerPolicy {
+ private val factory = AtomicReference(SubspaceRecomposerFactory.LifecycleAware)
+
+ internal fun createAndInstallSubspaceRecomposer(
+ rootElement: AbstractComposeElement
+ ): Recomposer {
+ val newRecomposer = factory.get().createRecomposer(rootElement)
+ rootElement.compositionContext = newRecomposer
+
+ // If the Recomposer shuts down, unregister it so that a future request for a subspace
+ // recomposer will consult the factory for a new one.
+ // TODO: update to GlobalScope.launch(AndroidUiDispatcher.CurrentThread) when migrating
+ // to androidx-main
+ val scope = MainScope()
+ val unsetJob =
+ scope.launch {
+ try {
+ newRecomposer.join()
+ } finally {
+ // Unset if the element is detached. (See below for the attach state change
+ // listener). Since this is in a finally in this coroutine, even if this job is
+ // cancelled we will resume on the window's UI thread and perform this
+ // manipulation
+ // there.
+ if (rootElement.compositionContext === newRecomposer) {
+ rootElement.compositionContext = null
+ }
+ }
+ }
+
+ // If the root element is detached, cancel the await for recomposer shutdown above.
+ // This will also unset the tag reference to this recomposer during its cleanup.
+ rootElement.addOnAttachStateChangeListener(
+ object : SpatialElement.OnAttachStateChangeListener {
+ override fun onElementAttachedToSubspace(
+ spatialComposeScene: SpatialComposeScene
+ ) {}
+
+ override fun onElementDetachedFromSubspace(
+ spatialComposeScene: SpatialComposeScene
+ ) {
+ rootElement.removeOnAttachStateChangeListener(this)
+ // Cancel the job to clean up the composition context reference in the element.
+ unsetJob.cancel()
+
+ // Also cancel the recomposer as it is not shared with another tree.
+ newRecomposer.cancel()
+ }
+ }
+ )
+
+ return newRecomposer
+ }
+}
+
+/**
+ * A factory for creating an subspace-scoped [Recomposer].
+ *
+ * See [createRecomposer] for more info.
+ */
+@InternalComposeUiApi
+private fun interface SubspaceRecomposerFactory {
+ /**
+ * Creates a [Recomposer] for a subspace with [rootElement] being the root of the Compose
+ * hierarchy.
+ *
+ * The factory is responsible for establishing a policy for [shutting down][Recomposer.cancel]
+ * the returned [Recomposer]. [rootElement] will hold a hard reference to the returned
+ * [Recomposer] until it [joins][Recomposer.join] after shutting down.
+ */
+ fun createRecomposer(rootElement: AbstractComposeElement): Recomposer
+
+ companion object {
+ /**
+ * A [SubspaceRecomposerFactory] that creates **lifecycle-aware** [Recomposer]s.
+ *
+ * Returned [Recomposer]s will be bound to the [SpatialComposeScene] of the 'rootElement'
+ * argument of [createRecomposer] and will be destroyed once the [SpatialComposeScene]
+ * lifecycle ends.
+ *
+ * The recomposer will run [recomposition][Recomposer.runRecomposeAndApplyChanges] and
+ * composition effects on the [AndroidUiDispatcher.CurrentThread]. The associated
+ * [MonotonicFrameClock] will only produce frames when the [Lifecycle] is at least
+ * [Lifecycle.State.STARTED], causing animations and other uses of [MonotonicFrameClock]
+ * APIs to suspend until a **visible** frame will be produced.
+ */
+ @OptIn(ExperimentalComposeUiApi::class)
+ val LifecycleAware: SubspaceRecomposerFactory = SubspaceRecomposerFactory { rootElement ->
+ createLifecycleAwareSubspaceRecomposer(rootElement)
+ }
+ }
+}
+
+/**
+ * Create a [Lifecycle] and
+ * [subspace attachment][SpatialElement.isAttachedToSpatialComposeScene]-aware [Recomposer] for the
+ * [rootElement] with the same behavior as [SubspaceRecomposerFactory.LifecycleAware].
+ *
+ * [coroutineContext] will override any [CoroutineContext] elements from the default configuration
+ * normally used for this content element. The default [CoroutineContext] contains
+ * [AndroidUiDispatcher.CurrentThread];
+ *
+ * This function should only be called from the UI thread of this [SpatialElement] or its intended
+ * UI thread if it is currently detached. It must also only be called when the element is attached
+ * to a subspace, i.e., [SpatialElement.spatialComposeScene] must not be `null`. If the
+ * [SpatialElement.spatialComposeScene] is `null`, an [IllegalStateException] will be thrown.
+ *
+ * The returned [Recomposer] will be [cancelled][Recomposer.cancel] when the [rootElement] is
+ * detached from its subspace or if its determined that the subspace is destroyed and its
+ * [Lifecycle] has [ended][Lifecycle.Event.ON_DESTROY].
+ *
+ * Recomposition and associated [frame-based][MonotonicFrameClock] effects may be throttled or
+ * paused while the [Lifecycle] is not at least [Lifecycle.State.STARTED].
+ */
+@ExperimentalComposeUiApi
+private fun createLifecycleAwareSubspaceRecomposer(
+ rootElement: AbstractComposeElement,
+ coroutineContext: CoroutineContext = EmptyCoroutineContext,
+): Recomposer {
+ val subspace =
+ checkNotNull(rootElement.spatialComposeScene) {
+ "Element $rootElement is not attached to a subspace."
+ }
+
+ // Only access AndroidUiDispatcher.CurrentThread if we would use an element from it,
+ // otherwise prevent lazy initialization.
+ val baseContext =
+ if (
+ coroutineContext[ContinuationInterceptor] == null ||
+ coroutineContext[MonotonicFrameClock] == null
+ ) {
+ AndroidUiDispatcher.CurrentThread + coroutineContext
+ } else {
+ coroutineContext
+ }
+
+ val pausableClock =
+ baseContext[MonotonicFrameClock]?.let { PausableMonotonicFrameClock(it).apply { pause() } }
+
+ var systemDurationScaleSettingConsumer: MotionDurationScaleImpl? = null
+ val motionDurationScale =
+ baseContext[MotionDurationScale]
+ ?: MotionDurationScaleImpl().also { systemDurationScaleSettingConsumer = it }
+
+ val contextWithClockAndMotionScale =
+ baseContext + (pausableClock ?: EmptyCoroutineContext) + motionDurationScale
+ val recomposer =
+ Recomposer(contextWithClockAndMotionScale).also { it.pauseCompositionFrameClock() }
+ val runRecomposeScope = CoroutineScope(contextWithClockAndMotionScale)
+
+ // Removing the element that holds the spatial scene graph means it may never be reattached
+ // again.
+ // Since this factory function is used to create a new recomposer for each invocation and
+ // does not reuse a single instance like other factories might, shut it down whenever it
+ // becomes detached. This can easily happen as part of subspace content.
+ rootElement.onDetachedFromSubspaceOnce { recomposer.cancel() }
+
+ subspace.lifecycle.addObserver(
+ object : LifecycleEventObserver {
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+ val self = this
+ when (event) {
+ Lifecycle.Event.ON_CREATE -> {
+ // UNDISPATCHED launch since we've configured this scope to be on the UI
+ // thread.
+ runRecomposeScope.launch(start = CoroutineStart.UNDISPATCHED) {
+ var durationScaleJob: Job? = null
+ try {
+ durationScaleJob =
+ systemDurationScaleSettingConsumer?.let {
+ val durationScaleStateFlow =
+ getAnimationScaleFlowFor(
+ subspace.ownerActivity.applicationContext
+ )
+ it.scaleFactor = durationScaleStateFlow.value
+ launch {
+ durationScaleStateFlow.collect { scaleFactor ->
+ it.scaleFactor = scaleFactor
+ }
+ }
+ }
+ recomposer.runRecomposeAndApplyChanges()
+ } finally {
+ durationScaleJob?.cancel()
+ // If runRecomposeAndApplyChanges returns or this coroutine is
+ // cancelled
+ // it means we no longer care about this lifecycle. Clean up the
+ // dangling references tied to this observer.
+ source.lifecycle.removeObserver(self)
+ }
+ }
+ }
+ Lifecycle.Event.ON_START -> {
+ // The clock starts life as paused so resume it when starting. If it is
+ // already
+ // running (this ON_START is after an ON_STOP) then the resume is ignored.
+ pausableClock?.resume()
+
+ // Resumes the frame clock dispatching if this is an ON_START after an
+ // ON_STOP
+ // that paused it. If the recomposer is not paused calling
+ // `resumeFrameClock()`
+ // is ignored.
+ recomposer.resumeCompositionFrameClock()
+ }
+ Lifecycle.Event.ON_STOP -> {
+ // Pause the recomposer's frame clock which will pause all calls to
+ // `withFrameNanos` (e.g. animations) while the window is stopped.
+ recomposer.pauseCompositionFrameClock()
+ }
+ Lifecycle.Event.ON_DESTROY -> {
+ recomposer.cancel()
+ }
+ Lifecycle.Event.ON_PAUSE -> {
+ // Nothing
+ }
+ Lifecycle.Event.ON_RESUME -> {
+ // Nothing
+ }
+ Lifecycle.Event.ON_ANY -> {
+ // Nothing
+ }
+ }
+ }
+ }
+ )
+
+ return recomposer
+}
+
+private class MotionDurationScaleImpl : MotionDurationScale {
+ override var scaleFactor by mutableFloatStateOf(1f)
+}
+
+private val animationScale = mutableMapOf<Context, StateFlow<Float>>()
+
+// Callers of this function should pass an application context. Passing an activity context might
+// result in activity leaks.
+private fun getAnimationScaleFlowFor(applicationContext: Context): StateFlow<Float> {
+ return synchronized(animationScale) {
+ animationScale.getOrPut(applicationContext) {
+ val resolver = applicationContext.contentResolver
+ val animationScaleUri =
+ Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE)
+ val channel = Channel<Unit>(CONFLATED)
+ val contentObserver =
+ object : ContentObserver(HandlerCompat.createAsync(Looper.getMainLooper())) {
+ override fun onChange(selfChange: Boolean, uri: Uri?) {
+ @Suppress("UNUSED_VARIABLE") val unused = channel.trySend(Unit)
+ }
+ }
+
+ callbackFlow {
+ resolver.registerContentObserver(animationScaleUri, false, contentObserver)
+ try {
+ for (value in channel) {
+ val newValue =
+ Settings.Global.getFloat(
+ applicationContext.contentResolver,
+ Settings.Global.ANIMATOR_DURATION_SCALE,
+ 1f,
+ )
+ send(newValue)
+ }
+ } finally {
+ resolver.unregisterContentObserver(contentObserver)
+ }
+ }
+ .stateIn(
+ MainScope(),
+ SharingStarted.WhileSubscribed(),
+ Settings.Global.getFloat(
+ applicationContext.contentResolver,
+ Settings.Global.ANIMATOR_DURATION_SCALE,
+ 1f,
+ ),
+ )
+ }
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/WrappedComposition.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/WrappedComposition.kt
new file mode 100644
index 0000000..33b2a27
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/platform/WrappedComposition.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composition
+import androidx.compose.runtime.CompositionContext
+import androidx.compose.runtime.CompositionServiceKey
+import androidx.compose.runtime.CompositionServices
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.xr.compose.subspace.node.SubspaceNodeApplier
+
+/**
+ * A composition object that is tied to [owner]'s lifecycle and attachment state.
+ *
+ * See [androidx.compose.ui.platform.WrappedComposition]
+ */
+internal class WrappedComposition(
+ private val owner: AndroidComposeSpatialElement,
+ compositionContext: CompositionContext,
+) : Composition, LifecycleEventObserver, CompositionServices {
+
+ private val composition = Composition(SubspaceNodeApplier(owner.root), compositionContext)
+ private var lastContent: @Composable () -> Unit = {}
+ private var addedToLifecycle: Lifecycle? = null
+
+ override val hasInvalidations
+ get() = composition.hasInvalidations
+
+ override val isDisposed: Boolean
+ get() = composition.isDisposed
+
+ override fun setContent(content: @Composable () -> Unit) {
+ owner.setOnSubspaceAvailable {
+ if (isDisposed) return@setOnSubspaceAvailable
+
+ val lifecycle = it.lifecycle
+ lastContent = content
+
+ if (addedToLifecycle == null) {
+ addedToLifecycle = lifecycle
+ // this will call ON_CREATE synchronously if it is already past the created state.
+ lifecycle.addObserver(this)
+ } else if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
+ composition.setContent { ProvideCompositionLocals(owner, content) }
+ }
+ }
+
+ owner.onDetachedFromSubspaceOnce { dispose() }
+ }
+
+ override fun dispose() {
+ if (!isDisposed) {
+ owner.wrappedComposition = null
+ composition.dispose()
+ }
+ }
+
+ override fun <T> getCompositionService(key: CompositionServiceKey<T>): T? =
+ (composition as? CompositionServices)?.getCompositionService(key)
+
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+ if (event == Lifecycle.Event.ON_DESTROY) {
+ dispose()
+ } else if (event == Lifecycle.Event.ON_CREATE) {
+ if (!isDisposed) {
+ setContent(lastContent)
+ }
+ }
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/ConstrainToModifier.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/ConstrainToModifier.kt
new file mode 100644
index 0000000..ae0251f
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/ConstrainToModifier.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.spatial
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.unit.Constraints
+
+/** Constrain the layout to the provided Constraints. */
+internal fun Modifier.constrainTo(constraints: Constraints) = layout { measurable, _ ->
+ measurable.measure(constraints).run { layout(width, height) { place(0, 0) } }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/ElevatedPanel.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/ElevatedPanel.kt
new file mode 100644
index 0000000..ea0180f
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/ElevatedPanel.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.spatial
+
+import android.view.View
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.shape.ZeroCornerSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.xr.compose.platform.LocalPanelEntity
+import androidx.xr.compose.platform.LocalSession
+import androidx.xr.compose.subspace.layout.SpatialRoundedCornerShape
+import androidx.xr.compose.subspace.layout.SpatialShape
+import androidx.xr.compose.subspace.rememberComposeView
+import androidx.xr.compose.unit.Meter
+import androidx.xr.compose.unit.Meter.Companion.meters
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Vector3
+import androidx.xr.scenecore.Dimensions
+import androidx.xr.scenecore.PanelEntity
+
+internal object ElevatedPanelDefaults {
+ /** Default shape for a Spatial Panel. */
+ internal val shape: SpatialShape = SpatialRoundedCornerShape(ZeroCornerSize)
+}
+
+/**
+ * Defines the "root view" of this ElevatedPanel such that nested ElevatedPanels can all reference
+ * the root view that they are attached to.
+ */
+internal val LocalRootView = compositionLocalOf<View?> { null }
+
+/**
+ * This is the base panel underlying the implementations of ElevatedSurface, ElevatedPopup, and
+ * ElevatedDialog. It allows creating a panel at a specific size and offset.
+ */
+@Composable
+internal fun ElevatedPanel(
+ spatialElevationLevel: SpatialElevationLevel,
+ contentSize: IntSize,
+ shape: SpatialShape = ElevatedPanelDefaults.shape,
+ contentOffset: Offset? = null,
+ transitionSpec:
+ @Composable
+ Transition.Segment<SpatialElevationLevel>.() -> FiniteAnimationSpec<Float> =
+ {
+ spring()
+ },
+ content: @Composable () -> Unit,
+) {
+ val parentView = LocalRootView.current ?: LocalView.current
+ val zDepth by
+ updateTransition(targetState = spatialElevationLevel, label = "restingLevelTransition")
+ .animateFloat(transitionSpec = transitionSpec, label = "zDepth") { state ->
+ state.level
+ }
+ var parentViewSize by remember { mutableStateOf(parentView.size) }
+ DisposableEffect(parentView) {
+ val listener =
+ View.OnLayoutChangeListener { _, _, _, right, bottom, _, _, _, _ ->
+ parentViewSize = IntSize(right, bottom)
+ }
+ parentView.addOnLayoutChangeListener(listener)
+ onDispose { parentView.removeOnLayoutChangeListener(listener) }
+ }
+
+ ElevatedPanel(
+ contentSize = contentSize,
+ shape = shape,
+ pose =
+ contentOffset?.let { rememberCalculatePose(it, parentViewSize, contentSize, zDepth) },
+ content = content,
+ )
+}
+
+/**
+ * This is the base panel underlying the implementations of ElevatedSurface, ElevatedPopup, and
+ * ElevatedDialog. It allows creating a panel at a specific size and [Pose].
+ */
+@Composable
+internal fun ElevatedPanel(
+ contentSize: IntSize,
+ shape: SpatialShape = ElevatedPanelDefaults.shape,
+ pose: Pose? = null,
+ content: @Composable () -> Unit,
+) {
+ val parentView = LocalRootView.current ?: LocalView.current
+ val session = checkNotNull(LocalSession.current) { "session must be initialized" }
+ val parentPanelEntity = LocalPanelEntity.current ?: session.mainPanelEntity
+ val density = LocalDensity.current
+ var panelEntity by remember { mutableStateOf<PanelEntity?>(null) }
+ val meterSize = contentSize.toMeterSize(density)
+
+ val view = rememberComposeView {
+ CompositionLocalProvider(
+ LocalRootView provides parentView,
+ LocalPanelEntity provides panelEntity,
+ ) {
+ Box(Modifier.alpha(if (pose == null) 0.0f else 1.0f)) { content() }
+ }
+ }
+
+ DisposableEffect(Unit) {
+ panelEntity =
+ session.createPanelEntity(
+ view = view,
+ surfaceDimensionsPx = meterSize.toCorePixelDimensions(density),
+ dimensions = meterSize.toCoreMeterDimensions(),
+ name = "ElevatedSurface:${view.id}",
+ )
+ onDispose {
+ panelEntity?.dispose()
+ panelEntity = null
+ }
+ }
+
+ LaunchedEffect(pose) {
+ if (pose != null) {
+ panelEntity?.setPose(pose)
+ }
+ }
+
+ LaunchedEffect(contentSize) {
+ val width = contentSize.width.toFloat()
+ val height = contentSize.height.toFloat()
+
+ panelEntity?.setSize(Dimensions(width = width, height = height, depth = 0f))
+ if (shape is SpatialRoundedCornerShape) {
+ panelEntity?.setCornerRadius(
+ Meter.fromPixel(shape.computeCornerRadius(width, height, density), density).value
+ )
+ }
+ }
+
+ LaunchedEffect(parentPanelEntity) { panelEntity?.setParent(parentPanelEntity) }
+}
+
+/** A [Position] based on [Meter]s. */
+internal data class MeterPosition(
+ val x: Meter = 0.meters,
+ val y: Meter = 0.meters,
+ val z: Meter = 0.meters,
+) {
+ /**
+ * Adds this [MeterPosition] to the [other] one.
+ *
+ * @param other the other [MeterPosition] to add.
+ * @return a new [MeterPosition] representing the sum of the two positions.
+ */
+ public operator fun plus(other: MeterPosition) =
+ MeterPosition(x = x + other.x, y = y + other.y, z = z + other.z)
+
+ fun toVector3() = Vector3(x = x.toM(), y = y.toM(), z = z.toM())
+}
+
+/** Represents a 3D size using [Meter] units. */
+internal data class MeterSize(
+ public val width: Meter = 0.meters,
+ public val height: Meter = 0.meters,
+ public val depth: Meter = 0.meters,
+)
+
+private fun IntSize.toMeterSize(density: Density) =
+ MeterSize(
+ Meter.fromPixel(width.toFloat(), density),
+ Meter.fromPixel(height.toFloat(), density),
+ 0.meters,
+ )
+
+private fun MeterSize.toCoreMeterDimensions() = Dimensions(width.toM(), height.toM(), depth.toM())
+
+// TODO(b/355735174) Update to PixelDimensions when SceneCore provides a proper API surface.
+private fun MeterSize.toCorePixelDimensions(density: Density) =
+ Dimensions(width.toPx(density), height.toPx(density), depth.toPx(density))
+
+internal val View.size
+ get() = IntSize(width, height)
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/Orbiter.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/Orbiter.kt
new file mode 100644
index 0000000..3c24d7a
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/Orbiter.kt
@@ -0,0 +1,497 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.spatial
+
+import android.view.View
+import androidx.annotation.RestrictTo
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.shape.ZeroCornerSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.xr.compose.platform.LocalDialogManager
+import androidx.xr.compose.platform.LocalPanelEntity
+import androidx.xr.compose.platform.LocalSession
+import androidx.xr.compose.platform.LocalSpatialCapabilities
+import androidx.xr.compose.spatial.EdgeOffset.Companion.outer
+import androidx.xr.compose.subspace.layout.SpatialRoundedCornerShape
+import androidx.xr.compose.subspace.layout.SpatialShape
+import androidx.xr.scenecore.PixelDimensions
+
+/** Contains default values used by Orbiters. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public object OrbiterDefaults {
+
+ /** Default shape for an Orbiter. */
+ public val shape: SpatialShape = SpatialRoundedCornerShape(ZeroCornerSize)
+
+ /** Default settings for an Orbiter */
+ public val orbiterSettings: OrbiterSettings = OrbiterSettings()
+}
+
+/**
+ * Settings for an Orbiter.
+ *
+ * @property shouldRenderInNonSpatial controls whether the orbiter content should be rendered in the
+ * normal flow in non-spatial environments. If `true`, the content is rendered normally;
+ * otherwise, it's removed from the flow.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class OrbiterSettings(
+ @get:JvmName("shouldRenderInNonSpatial") public val shouldRenderInNonSpatial: Boolean = true
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is OrbiterSettings) return false
+
+ if (shouldRenderInNonSpatial != other.shouldRenderInNonSpatial) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return shouldRenderInNonSpatial.hashCode()
+ }
+
+ override fun toString(): String {
+ return "OrbiterSettings(shouldRenderInNonSpatial=$shouldRenderInNonSpatial)"
+ }
+
+ public fun copy(
+ shouldRenderInNonSpatial: Boolean = this.shouldRenderInNonSpatial
+ ): OrbiterSettings = OrbiterSettings(shouldRenderInNonSpatial = shouldRenderInNonSpatial)
+}
+
+/**
+ * A composable that creates an orbiter along the top or bottom edges of a view.
+ *
+ * @param position The edge of the orbiter. Use [OrbiterEdge.Top] or [OrbiterEdge.Bottom].
+ * @param offset The offset of the orbiter based on the outer edge of the orbiter.
+ * @param alignment The alignment of the orbiter. Use [Alignment.CenterHorizontally] or
+ * [Alignment.Start] or [Alignment.End].
+ * @param settings The settings for the orbiter.
+ * @param shape The shape of this Orbiter when it is rendered in 3D space.
+ * @param content The content of the orbiter.
+ *
+ * Example:
+ * ```
+ * Orbiter(position = OrbiterEdge.Top, offset = 10.dp) {
+ * Text("This is a top edge Orbiter")
+ * }
+ * ```
+ */
+@Composable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun Orbiter(
+ position: OrbiterEdge.Horizontal,
+ offset: Dp = 0.dp,
+ alignment: Alignment.Horizontal = Alignment.CenterHorizontally,
+ settings: OrbiterSettings = OrbiterDefaults.orbiterSettings,
+ shape: SpatialShape = OrbiterDefaults.shape,
+ content: @Composable () -> Unit,
+) {
+ Orbiter(
+ OrbiterData(
+ position = position,
+ horizontalAlignment = alignment,
+ offset = outer(offset),
+ settings = settings,
+ shape = shape,
+ content = content,
+ )
+ )
+}
+
+/**
+ * A composable that creates an orbiter along the top or bottom edges of a view.
+ *
+ * @param position The edge of the orbiter. Use [OrbiterEdge.Top] or [OrbiterEdge.Bottom].
+ * @param offset The offset of the orbiter based on the inner or outer edge of the orbiter. Use
+ * [EdgeOffset.outer] to create an [EdgeOffset] aligned to the outer edge of the orbiter or
+ * [innerEdge] or [EdgeOffset.overlap] to create an [EdgeOffset] aligned to the inner edge of the
+ * orbiter.
+ * @param alignment The alignment of the orbiter. Use [Alignment.CenterHorizontally] or
+ * [Alignment.Start] or [Alignment.End].
+ * @param settings The settings for the orbiter.
+ * @param shape The shape of this Orbiter when it is rendered in 3D space.
+ * @param content The content of the orbiter.
+ *
+ * Example:
+ * ```
+ * Orbiter(position = OrbiterEdge.Top, offset = outer(10.dp)) {
+ * Text("This is a top edge Orbiter")
+ * }
+ * ```
+ */
+@Composable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun Orbiter(
+ position: OrbiterEdge.Horizontal,
+ offset: EdgeOffset,
+ alignment: Alignment.Horizontal = Alignment.CenterHorizontally,
+ settings: OrbiterSettings = OrbiterDefaults.orbiterSettings,
+ shape: SpatialShape = OrbiterDefaults.shape,
+ content: @Composable () -> Unit,
+) {
+ Orbiter(
+ OrbiterData(
+ position = position,
+ horizontalAlignment = alignment,
+ offset = offset,
+ settings = settings,
+ shape = shape,
+ content = content,
+ )
+ )
+}
+
+/**
+ * A composable that creates an orbiter along the start or end edges of a view.
+ *
+ * @param position The edge of the orbiter. Use [OrbiterEdge.Start] or [OrbiterEdge.End].
+ * @param offset The offset of the orbiter based on the outer edge of the orbiter.
+ * @param alignment The alignment of the orbiter. Use [Alignment.CenterVertically] or
+ * [Alignment.Top] or [Alignment.Bottom].
+ * @param settings The settings for the orbiter.
+ * @param shape The shape of this Orbiter when it is rendered in 3D space.
+ * @param content The content of the orbiter.
+ *
+ * Example:
+ * ```
+ * Orbiter(position = OrbiterEdge.Start, offset = 10.dp) {
+ * Text("This is a start edge Orbiter")
+ * }
+ * ```
+ */
+@Composable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun Orbiter(
+ position: OrbiterEdge.Vertical,
+ offset: Dp = 0.dp,
+ alignment: Alignment.Vertical = Alignment.CenterVertically,
+ settings: OrbiterSettings = OrbiterDefaults.orbiterSettings,
+ shape: SpatialShape = OrbiterDefaults.shape,
+ content: @Composable () -> Unit,
+) {
+ Orbiter(
+ OrbiterData(
+ position = position,
+ verticalAlignment = alignment,
+ offset = outer(offset),
+ settings = settings,
+ shape = shape,
+ content = content,
+ )
+ )
+}
+
+/**
+ * A composable that creates an orbiter along the start or end edges of a view.
+ *
+ * @param position The edge of the orbiter. Use [OrbiterEdge.Start] or [OrbiterEdge.End].
+ * @param offset The offset of the orbiter based on the inner or outer edge of the orbiter. Use
+ * [EdgeOffset.outer] to create an [EdgeOffset] aligned to the outer edge of the orbiter or
+ * [innerEdge] or [EdgeOffset.overlap] to create an [EdgeOffset] aligned to the inner edge of the
+ * orbiter.
+ * @param alignment The alignment of the orbiter. Use [Alignment.CenterVertically] or
+ * [Alignment.Top] or [Alignment.Bottom].
+ * @param settings The settings for the orbiter.
+ * @param shape The shape of this Orbiter when it is rendered in 3D space.
+ * @param content The content of the orbiter.
+ *
+ * Example:
+ * ```
+ * Orbiter(position = OrbiterEdge.Start, offset = outer(10.dp)) {
+ * Text("This is a start edge Orbiter")
+ * }
+ * ```
+ */
+@Composable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun Orbiter(
+ position: OrbiterEdge.Vertical,
+ offset: EdgeOffset,
+ alignment: Alignment.Vertical = Alignment.CenterVertically,
+ settings: OrbiterSettings = OrbiterDefaults.orbiterSettings,
+ shape: SpatialShape = OrbiterDefaults.shape,
+ content: @Composable () -> Unit,
+) {
+ Orbiter(
+ OrbiterData(
+ position = position,
+ verticalAlignment = alignment,
+ offset = offset,
+ settings = settings,
+ shape = shape,
+ content = content,
+ )
+ )
+}
+
+@Composable
+private fun Orbiter(data: OrbiterData) {
+ if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
+ PositionedOrbiter(data)
+ } else if (data.settings.shouldRenderInNonSpatial) {
+ data.content()
+ }
+}
+
+@Composable
+internal fun PositionedOrbiter(data: OrbiterData) {
+ val view = LocalView.current
+ val session = checkNotNull(LocalSession.current) { "session must be initialized" }
+ val panelEntity = LocalPanelEntity.current ?: session.mainPanelEntity
+ var panelSize by remember { mutableStateOf(panelEntity.getPixelDimensions()) }
+ var contentSize: IntSize? by remember { mutableStateOf(null) }
+ val dialogManager = LocalDialogManager.current
+
+ DisposableEffect(view) {
+ val listener =
+ View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+ panelSize = panelEntity.getPixelDimensions()
+ }
+ view.addOnLayoutChangeListener(listener)
+ onDispose { view.removeOnLayoutChangeListener(listener) }
+ }
+
+ ElevatedPanel(
+ spatialElevationLevel = SpatialElevationLevel.Level1,
+ contentSize = contentSize ?: IntSize.Zero,
+ contentOffset = contentSize?.let { data.calculateOffset(panelSize, it) },
+ shape = data.shape,
+ ) {
+ Box(
+ modifier =
+ Modifier.constrainTo(Constraints(0, panelSize.width, 0, panelSize.height))
+ .onSizeChanged { contentSize = it }
+ ) {
+ data.content()
+ }
+ Box(
+ modifier =
+ Modifier.fillMaxSize()
+ .then(
+ if (dialogManager.isSpatialDialogActive.value) {
+ Modifier.background(Color.Black.copy(alpha = 0.2f)).pointerInput(Unit) {
+ detectTapGestures {
+ dialogManager.isSpatialDialogActive.value = false
+ }
+ }
+ } else {
+ Modifier
+ }
+ )
+ ) {}
+ }
+}
+
+/** An enum that represents the edges of a view where an orbiter can be placed. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public sealed interface OrbiterEdge {
+ @JvmInline
+ public value class Horizontal private constructor(private val value: Int) : OrbiterEdge {
+ public companion object {
+ public val Top: Horizontal = Horizontal(0)
+ public val Bottom: Horizontal = Horizontal(1)
+ }
+ }
+
+ /** Represents vertical edges (start or end). */
+ @JvmInline
+ public value class Vertical private constructor(private val value: Int) : OrbiterEdge {
+ public companion object {
+ public val Start: Vertical = Vertical(0)
+ public val End: Vertical = Vertical(1)
+ }
+ }
+
+ public companion object {
+ /** The top edge. */
+ public val Top: Horizontal = Horizontal.Top
+ /** The bottom edge. */
+ public val Bottom: Horizontal = Horizontal.Bottom
+ /** The start edge. */
+ public val Start: Vertical = Vertical.Start
+ /** The end edge. */
+ public val End: Vertical = Vertical.End
+ }
+}
+
+/** Represents the type of offset used for positioning an orbiter. */
+@JvmInline
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public value class OrbiterOffsetType private constructor(private val value: Int) {
+ public companion object {
+ /** Indicates that the offset is relative to the outer edge of the orbiter. */
+ public val OuterEdge: OrbiterOffsetType = OrbiterOffsetType(0)
+ /** Indicates that the offset is relative to the inner edge of the orbiter. */
+ public val InnerEdge: OrbiterOffsetType = OrbiterOffsetType(1)
+ }
+}
+
+/**
+ * Represents the offset of an orbiter from the main panel.
+ *
+ * @property amount the magnitude of the offset in pixels.
+ * @property type the type of offset ([OrbiterOffsetType.OuterEdge] or
+ * [OrbiterOffsetType.InnerEdge]).
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class EdgeOffset
+internal constructor(public val amount: Float, public val type: OrbiterOffsetType) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is EdgeOffset) return false
+
+ if (amount != other.amount) return false
+ if (type != other.type) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = amount.hashCode()
+ result = 31 * result + type.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "EdgeOffset(amount=$amount, type=$type)"
+ }
+
+ public fun copy(amount: Float = this.amount, type: OrbiterOffsetType = this.type): EdgeOffset =
+ EdgeOffset(amount = amount, type = type)
+
+ public companion object {
+ /**
+ * Creates an [EdgeOffset] representing an offset from the outer edge of an orbiter.
+ *
+ * An offset that represents the offset of an orbiter from the main panel relative to the
+ * outer edge of the orbiter. In outer edge alignment, the outer edge of the orbiter will be
+ * [offset] distance away from the edge of the main panel.
+ *
+ * @param offset the offset value in [Dp].
+ * @return an [EdgeOffset] with the specified offset and type [OrbiterOffsetType.OuterEdge].
+ */
+ @Composable
+ public fun outer(offset: Dp): EdgeOffset =
+ with(LocalDensity.current) { EdgeOffset(offset.toPx(), OrbiterOffsetType.OuterEdge) }
+
+ /**
+ * Creates an [EdgeOffset] representing an offset from the inner edge of an orbiter.
+ *
+ * An offset that represents the offset of an orbiter from the main panel relative to the
+ * inner edge of the orbiter. In inner edge alignment, the inner edge of the orbiter will be
+ * [offset] distance away from the edge of the main panel.
+ *
+ * @param offset the offset value in [Dp].
+ * @return an [EdgeOffset] with the specified offset and type [OrbiterOffsetType.InnerEdge].
+ */
+ @Composable
+ public fun inner(offset: Dp): EdgeOffset =
+ with(LocalDensity.current) { EdgeOffset(offset.toPx(), OrbiterOffsetType.InnerEdge) }
+
+ /**
+ * Creates an [EdgeOffset] representing an overlap of an orbiter into the main panel
+ * relative to the inner edge of the orbiter.
+ *
+ * In overlap alignment, the inner edge of the orbiter will be [offset] distance inset into
+ * the edge of the main panel.
+ *
+ * @param offset the amount of overlap, specified in [Dp].
+ * @return an [EdgeOffset] with the [offset]'s pixel value and
+ * [OrbiterOffsetType.InnerEdge].
+ */
+ @Composable
+ public fun overlap(offset: Dp): EdgeOffset =
+ with(LocalDensity.current) { EdgeOffset(-offset.toPx(), OrbiterOffsetType.InnerEdge) }
+ }
+}
+
+internal data class OrbiterData(
+ public val position: OrbiterEdge,
+ public val verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
+ public val horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
+ public val offset: EdgeOffset,
+ public val settings: OrbiterSettings = OrbiterDefaults.orbiterSettings,
+ public val content: @Composable () -> Unit,
+ public val shape: SpatialShape,
+)
+
+/**
+ * Calculates the offset that should be applied to the orbiter given its settings, the panel size,
+ * and the size of the orbiter content.
+ */
+private fun OrbiterData.calculateOffset(viewSize: PixelDimensions, contentSize: IntSize): Offset {
+ if (position is OrbiterEdge.Vertical) {
+ val y = verticalAlignment.align(contentSize.height, viewSize.height)
+
+ val xOffset =
+ when (offset.type) {
+ OrbiterOffsetType.OuterEdge -> -offset.amount
+ OrbiterOffsetType.InnerEdge -> -contentSize.width - offset.amount
+ else -> error("Unexpected OrbiterOffsetType: ${offset.type}")
+ }
+
+ val x =
+ when (position) {
+ OrbiterEdge.Start -> xOffset
+ OrbiterEdge.End -> viewSize.width - contentSize.width - xOffset
+ else -> error("Unexpected OrbiterEdge: $position")
+ }
+ return Offset(x, y.toFloat())
+ } else {
+ // It should be fine to use LTR layout direction here since we can use placeRelative to
+ // adjust
+ val x = horizontalAlignment.align(contentSize.width, viewSize.width, LayoutDirection.Ltr)
+
+ val yOffset =
+ when (offset.type) {
+ OrbiterOffsetType.OuterEdge -> -offset.amount
+ OrbiterOffsetType.InnerEdge -> -contentSize.height - offset.amount
+ else -> error("Unexpected OrbiterOffsetType: ${offset.type}")
+ }
+
+ val y =
+ when (position) {
+ OrbiterEdge.Top -> yOffset
+ OrbiterEdge.Bottom -> viewSize.height - contentSize.height - yOffset
+ else -> error("Unexpected OrbiterEdge: $position")
+ }
+ return Offset(x.toFloat(), y)
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/OutsideInputHandler.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/OutsideInputHandler.kt
new file mode 100644
index 0000000..dd7fd87
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/OutsideInputHandler.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.spatial
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.MotionEvent
+import android.view.View
+import android.view.WindowManager
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.RememberObserver
+import androidx.compose.runtime.State
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.updateLayoutParams
+import kotlin.math.roundToInt
+
+/** Handle clicks outside of the parent panel. */
+@Composable
+internal fun OutsideInputHandler(enabled: Boolean = true, onOutsideInput: () -> Unit) {
+ if (enabled) {
+ val view = LocalView.current
+ val currentOnOutsideInput = rememberUpdatedState(onOutsideInput)
+ remember(view) { InputCaptureView(view, currentOnOutsideInput) }
+ }
+}
+
+/**
+ * A View that captures input events and forwards pointer and touch events to a target view.
+ *
+ * This allows us to detect when touch events happen outside of the bounds of the target view.
+ *
+ * This default View constructor is used by tooling (as per a warning in Android Studio). In
+ * practice, use the constructor that takes a targetView and onOutsideInput.
+ */
+private class InputCaptureView private constructor(context: Context) :
+ View(context), RememberObserver, View.OnLayoutChangeListener {
+ constructor(targetView: View, onOutsideInput: State<() -> Unit>) : this(targetView.context) {
+ this.targetView = targetView
+ this.onOutsideInput = onOutsideInput
+ }
+
+ init {
+ // Assign the layout parameters for this view. See documentation for more details:
+ // https://developer.android.com/reference/android/view/View#setLayoutParams(android.view.ViewGroup.LayoutParams)
+ layoutParams =
+ WindowManager.LayoutParams().apply {
+ type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
+ flags = WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+ }
+ }
+
+ private val windowManager: WindowManager =
+ context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+
+ private var targetView: View? = null
+ set(value) {
+ if (field != value && value != null) {
+ field?.removeOnLayoutChangeListener(this)
+ value.addOnLayoutChangeListener(this)
+ updateLayoutParams<WindowManager.LayoutParams> {
+ // Get the Window token from the parent view
+ token = value.applicationWindowToken
+
+ // Match the size of the target view.
+ width = value.width
+ height = value.height
+ }
+ if (isAttachedToWindow) {
+ windowManager.updateViewLayout(this, layoutParams)
+ }
+ }
+ field = value
+ }
+
+ private var onOutsideInput: State<() -> Unit>? = null
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouchEvent(event: MotionEvent?): Boolean {
+ if (event == null || targetView == null) {
+ return super.onTouchEvent(event)
+ }
+ if (isMotionEventOutsideTargetView(event)) {
+ onOutsideInput?.value?.invoke()
+ } else {
+ // This click is inside of the target view bounds, so dispatch it to the target view.
+ targetView?.dispatchTouchEvent(event)
+ }
+ return true
+ }
+
+ private fun isMotionEventOutsideTargetView(event: MotionEvent): Boolean {
+ val view = this.targetView ?: return true
+ // If the action is ACTION_DOWN, we need to check if the touch event is outside of the view
+ // bounds.
+ if (event.action == MotionEvent.ACTION_DOWN) {
+ return event.x.roundToInt() !in view.left..view.right ||
+ event.y.roundToInt() !in view.top..view.bottom
+ }
+ return event.action == MotionEvent.ACTION_OUTSIDE
+ }
+
+ override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
+ targetView?.dispatchGenericMotionEvent(event)
+ return true
+ }
+
+ override fun onAbandoned() {
+ // Do nothing. Nothing is set up until onRemembered is called.
+ }
+
+ override fun onForgotten() {
+ targetView?.removeOnLayoutChangeListener(this)
+ windowManager.removeView(this)
+ }
+
+ override fun onRemembered() {
+ windowManager.addView(this, layoutParams)
+ }
+
+ /**
+ * Update the layout parameters of this view to match the size of the [targetView].
+ *
+ * This is called when the [targetView] is laid out.
+ */
+ override fun onLayoutChange(
+ v: View?,
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ oldLeft: Int,
+ oldTop: Int,
+ oldRight: Int,
+ oldBottom: Int,
+ ) {
+ updateLayoutParams<WindowManager.LayoutParams> {
+ width = right - left
+ height = bottom - top
+ }
+ if (isAttachedToWindow) {
+ windowManager.updateViewLayout(this, layoutParams)
+ }
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/RememberCalculatePose.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/RememberCalculatePose.kt
new file mode 100644
index 0000000..d159c06
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/RememberCalculatePose.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.spatial
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.xr.compose.unit.Meter
+import androidx.xr.compose.unit.Meter.Companion.meters
+import androidx.xr.runtime.math.Pose
+
+/** Calculate a [Pose] in 3D space based on the relative offset within the 2D space of a Panel. */
+@Composable
+internal fun rememberCalculatePose(
+ contentOffset: Offset,
+ parentViewSize: IntSize,
+ contentSize: IntSize,
+ zDepth: Float = 0f,
+): Pose {
+ val density = LocalDensity.current
+ return remember(contentOffset, parentViewSize, contentSize, zDepth) {
+ val meterPosition =
+ contentOffset.toMeterPosition(parentViewSize, contentSize, density) +
+ MeterPosition(z = zDepth.meters)
+ Pose(translation = meterPosition.toVector3())
+ }
+}
+
+/**
+ * Resolves the coordinate systems between 2D app pixel space and 3D meter space. In 2d space, views
+ * and composables are anchored at the top left corner; however, in 3D space they are anchored at
+ * the center. This fixes that by adjusting for the space size and the content's size so they are
+ * anchored in the top left corner in 3D space.
+ *
+ * This conversion requires that [density] be specified.
+ */
+private fun Offset.toMeterPosition(
+ parentViewSize: IntSize,
+ contentSize: IntSize,
+ density: Density,
+) =
+ MeterPosition(
+ Meter.fromPixel(x.scale(contentSize.width, parentViewSize.width), density),
+ Meter.fromPixel(-y.scale(contentSize.height, parentViewSize.height), density),
+ )
+
+private fun Float.scale(size: Int, space: Int) = this + (size - space) / 2.0f
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/SpatialDialog.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/SpatialDialog.kt
new file mode 100644
index 0000000..d62b0e0
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/SpatialDialog.kt
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.spatial
+
+import androidx.annotation.RestrictTo
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.animate
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import androidx.xr.compose.platform.LocalDialogManager
+import androidx.xr.compose.platform.LocalSession
+import androidx.xr.compose.platform.LocalSpatialCapabilities
+import androidx.xr.compose.unit.Meter
+import androidx.xr.compose.unit.Meter.Companion.meters
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Vector3
+import androidx.xr.scenecore.Session
+import kotlinx.coroutines.launch
+
+/**
+ * Properties for configuring a [SpatialDialog].
+ *
+ * @property dismissOnBackPress whether the dialog should be dismissed when the device's back button
+ * is pressed. Defaults to `true`.
+ * @property dismissOnClickOutside whether the dialog should be dismissed when the user touches
+ * outside of it. Defaults to `true`.
+ * @property usePlatformDefaultWidth whether the dialog should use the platform's default width.
+ * Defaults to `true`.
+ * @property restingLevelAnimationSpec the animation specification for the resting level.
+ * @property spatialElevationLevel the elevation level of the dialog. Defaults to
+ * [SpatialElevationLevel.DialogDefault].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SpatialDialogProperties(
+ @get:Suppress("GetterSetterNames") public val dismissOnBackPress: Boolean = true,
+ @get:Suppress("GetterSetterNames") public val dismissOnClickOutside: Boolean = true,
+ @get:Suppress("GetterSetterNames") public val usePlatformDefaultWidth: Boolean = true,
+ public val restingLevelAnimationSpec: FiniteAnimationSpec<Float> = spring(),
+ public val spatialElevationLevel: SpatialElevationLevel = SpatialElevationLevel.DialogDefault,
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is SpatialDialogProperties) return false
+
+ if (dismissOnBackPress != other.dismissOnBackPress) return false
+ if (dismissOnClickOutside != other.dismissOnClickOutside) return false
+ if (usePlatformDefaultWidth != other.usePlatformDefaultWidth) return false
+ if (restingLevelAnimationSpec != other.restingLevelAnimationSpec) return false
+ if (spatialElevationLevel != other.spatialElevationLevel) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = dismissOnBackPress.hashCode()
+ result = 31 * result + dismissOnClickOutside.hashCode()
+ result = 31 * result + usePlatformDefaultWidth.hashCode()
+ result = 31 * result + restingLevelAnimationSpec.hashCode()
+ result = 31 * result + spatialElevationLevel.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "SpatialDialogProperties(dismissOnBackPress=$dismissOnBackPress, dismissOnClickOutside=$dismissOnClickOutside, usePlatformDefaultWidth=$usePlatformDefaultWidth, restingLevelAnimationSpec=$restingLevelAnimationSpec, spatialElevationLevel=$spatialElevationLevel)"
+ }
+
+ public fun copy(
+ dismissOnBackPress: Boolean = this.dismissOnBackPress,
+ dismissOnClickOutside: Boolean = this.dismissOnClickOutside,
+ usePlatformDefaultWidth: Boolean = this.usePlatformDefaultWidth,
+ restingLevelAnimationSpec: FiniteAnimationSpec<Float> = this.restingLevelAnimationSpec,
+ spatialElevationLevel: SpatialElevationLevel = this.spatialElevationLevel,
+ ): SpatialDialogProperties =
+ SpatialDialogProperties(
+ dismissOnBackPress = dismissOnBackPress,
+ dismissOnClickOutside = dismissOnClickOutside,
+ usePlatformDefaultWidth = usePlatformDefaultWidth,
+ restingLevelAnimationSpec = restingLevelAnimationSpec,
+ spatialElevationLevel = spatialElevationLevel,
+ )
+}
+
+private fun SpatialDialogProperties.toBaseDialogProperties() =
+ DialogProperties(
+ dismissOnClickOutside = dismissOnClickOutside,
+ dismissOnBackPress = dismissOnBackPress,
+ usePlatformDefaultWidth = usePlatformDefaultWidth,
+ )
+
+/**
+ * [SpatialDialog] is a dialog that is elevated above the activity.
+ *
+ * @param onDismissRequest a callback to be invoked when the dialog should be dismissed.
+ * @param properties the dialog properties.
+ * @param content the content of the dialog.
+ */
+@Composable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SpatialDialog(
+ onDismissRequest: () -> Unit,
+ properties: SpatialDialogProperties = SpatialDialogProperties(),
+ content: @Composable () -> Unit,
+) {
+ if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
+ LayoutSpatialDialog(onDismissRequest, properties, content)
+ } else {
+ Dialog(
+ onDismissRequest = onDismissRequest,
+ properties = properties.toBaseDialogProperties(),
+ content = content,
+ )
+ }
+}
+
+@Composable
+private fun LayoutSpatialDialog(
+ onDismissRequest: () -> Unit,
+ properties: SpatialDialogProperties = SpatialDialogProperties(),
+ content: @Composable () -> Unit,
+) {
+ val view = LocalRootView.current ?: LocalView.current
+ val scope = rememberCoroutineScope()
+ val session = checkNotNull(LocalSession.current) { "session must be initialized" }
+ // Start elevation at Level0 to prevent effects where the dialog flashes behind its parent.
+ var spatialElevationLevel by remember { mutableStateOf(SpatialElevationLevel.Level0) }
+ val dialogManager = LocalDialogManager.current
+
+ DisposableEffect(Unit) {
+ scope.launch {
+ animate(
+ initialValue = SpatialElevationLevel.ActivityDefault.level,
+ targetValue = -properties.spatialElevationLevel.level,
+ animationSpec = properties.restingLevelAnimationSpec,
+ ) { value, _ ->
+ session.setActivitySpaceZDepth(value.meters)
+ }
+ }
+ dialogManager.isSpatialDialogActive.value = true
+ onDispose {
+ session.resetActivitySpaceZDepth()
+ dialogManager.isSpatialDialogActive.value = false
+ }
+ }
+
+ LaunchedEffect(Unit) { spatialElevationLevel = properties.spatialElevationLevel }
+
+ LaunchedEffect(dialogManager.isSpatialDialogActive.value) {
+ if (!dialogManager.isSpatialDialogActive.value) {
+ onDismissRequest()
+ }
+ }
+
+ // Paint the scrim on the parent panel and capture dismiss events.
+ Dialog(
+ onDismissRequest = {
+ scope.launch {
+ animate(
+ initialValue = -properties.spatialElevationLevel.level,
+ targetValue = SpatialElevationLevel.ActivityDefault.level,
+ animationSpec = properties.restingLevelAnimationSpec,
+ ) { value, _ ->
+ session.setActivitySpaceZDepth(value.meters)
+ }
+ }
+ dialogManager.isSpatialDialogActive.value = false
+ },
+ properties = properties.toBaseDialogProperties(),
+ ) {
+ // We need a very small (non-zero) content to fill the remaining space with the scrim.
+ Spacer(Modifier.size(1.dp))
+ }
+
+ var contentSize by remember { mutableStateOf(view.size) }
+
+ val zDepth by
+ updateTransition(targetState = spatialElevationLevel, label = "restingLevelTransition")
+ .animateFloat(
+ transitionSpec = { properties.restingLevelAnimationSpec },
+ label = "zDepth"
+ ) { state ->
+ state.level
+ }
+
+ ElevatedPanel(
+ contentSize = contentSize,
+ pose = Pose(translation = MeterPosition(z = zDepth.meters).toVector3()),
+ ) {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Box(modifier = Modifier.onSizeChanged { contentSize = it }) { content() }
+ }
+ }
+}
+
+private fun Session.setActivitySpaceZDepth(value: Meter) {
+ activitySpace.setPose(Pose(translation = Vector3(0f, 0f, value.toM())))
+}
+
+private fun Session.resetActivitySpaceZDepth() {
+ setActivitySpaceZDepth(SpatialElevationLevel.ActivityDefault.level.meters)
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/SpatialElevation.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/SpatialElevation.kt
new file mode 100644
index 0000000..6228322
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/SpatialElevation.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.spatial
+
+import androidx.annotation.RestrictTo
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.xr.compose.platform.LocalSpatialCapabilities
+
+/**
+ * Composable that creates a panel in 3D space when spatialization is enabled.
+ *
+ * [SpatialElevation] elevates content in-place. It uses the source position and constraints to
+ * determine the size and placement of the elevated panel while reserving space for the original
+ * element within the layout.
+ *
+ * In non-spatial environments, the content is rendered normally without elevation.
+ *
+ * @param spatialElevationLevel the desired elevation level for the panel in spatial environments.
+ * @param content the composable content to be displayed within the elevated panel.
+ */
+@Composable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SpatialElevation(
+ spatialElevationLevel: SpatialElevationLevel = SpatialElevationLevel.Level0,
+ content: @Composable () -> Unit,
+) {
+ if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
+ LayoutSpatialElevation(spatialElevationLevel, content)
+ } else {
+ content()
+ }
+}
+
+@Composable
+private fun LayoutSpatialElevation(
+ spatialElevationLevel: SpatialElevationLevel,
+ content: @Composable () -> Unit,
+) {
+ val bufferPadding = 1.dp
+ val bufferPaddingPx = with(LocalDensity.current) { bufferPadding.toPx() }
+ var contentSize by remember { mutableStateOf(IntSize.Zero) }
+ var contentOffset: Offset? by remember { mutableStateOf(null) }
+
+ // Reserve space for the content in the original view.
+ with(LocalDensity.current) {
+ Spacer(
+ Modifier.size(contentSize.width.toDp(), contentSize.height.toDp())
+ .onGloballyPositioned { contentOffset = it.positionInRoot() }
+ )
+ }
+
+ // It is important to use BoxWithConstraints here because the Layout within the ElevatedPanel
+ // does
+ // not know the constraints of the parent view.
+ BoxWithConstraints {
+ ElevatedPanel(
+ spatialElevationLevel = spatialElevationLevel,
+ contentSize = contentSize,
+ contentOffset = contentOffset,
+ ) {
+ // This padding prevents visual aberrations due to stretched panels. The panel is still
+ // being stretched in those cases (which will affect input tracking), but it will not be
+ // visible to the user.
+ // TODO(b/333074376): Remove this padding when the underlying bug is fixed.
+ Box(
+ Modifier.constrainTo(constraints)
+ .onSizeChanged {
+ check(it.width > bufferPaddingPx * 2 && it.height > bufferPaddingPx * 2) {
+ "Empty composables cannot be placed at a SpatialElevation. You may be trying" +
+ " to use a Popup or Dialog with a SpatialElevation, which is not supported."
+ }
+ contentSize = it
+ }
+ .padding(bufferPadding)
+ ) {
+ content()
+ }
+ }
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/SpatialElevationLevel.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/SpatialElevationLevel.kt
new file mode 100644
index 0000000..60dabb4
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/SpatialElevationLevel.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.spatial
+
+import androidx.annotation.RestrictTo
+import androidx.compose.ui.unit.dp
+import androidx.xr.compose.unit.toMeter
+
+/**
+ * Represents the resting elevation level for spatial elements.
+ *
+ * Elevation levels range from `Level0` (no elevation) to `Level5` (highest allowed elevation).
+ *
+ * NOTE: Level0 is not visually distinguishable from base-level content but is present to support
+ * smooth transitioning between elevation levels.
+ *
+ * Values are expressed in meters for consistency with spatial positioning.
+ */
+@JvmInline
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public value class SpatialElevationLevel internal constructor(public val level: Float) {
+ public companion object {
+ internal val ActivityDefault = SpatialElevationLevel(0f)
+ public val Level0: SpatialElevationLevel = SpatialElevationLevel(0.1.dp.toMeter().value)
+ public val Level1: SpatialElevationLevel = SpatialElevationLevel(15.dp.toMeter().value)
+ public val Level2: SpatialElevationLevel = SpatialElevationLevel(30.dp.toMeter().value)
+ public val Level3: SpatialElevationLevel = SpatialElevationLevel(60.dp.toMeter().value)
+ public val Level4: SpatialElevationLevel = SpatialElevationLevel(90.dp.toMeter().value)
+ public val Level5: SpatialElevationLevel = SpatialElevationLevel(120.dp.toMeter().value)
+ internal val DialogDefault = SpatialElevationLevel(125.dp.toMeter().value)
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/SpatialPopup.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/SpatialPopup.kt
new file mode 100644
index 0000000..64be95a
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/SpatialPopup.kt
@@ -0,0 +1,369 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.spatial
+
+import android.graphics.Rect
+import androidx.annotation.RestrictTo
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.layout.positionInWindow
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastRoundToInt
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupProperties
+import androidx.xr.compose.platform.LocalSpatialCapabilities
+
+/**
+ * [SpatialPopup] properties.
+ *
+ * @property focusable whether the popup is focusable. If `true`, it will handle IME events and key
+ * presses (e.g., back button). Defaults to `false`.
+ * @property dismissOnBackPress whether the popup can be dismissed by pressing the back button
+ * (Android) or escape key (desktop). Only effective if `focusable` is `true`. Defaults to `true`.
+ * @property dismissOnClickOutside whether the popup can be dismissed by clicking outside its
+ * bounds. If true, clicking outside the popup will call onDismissRequest. Defaults to `true`.
+ * @property clippingEnabled whether to allow the popup window to extend beyond the screen
+ * boundaries. Defaults to `true`. Setting this to false will allow windows to be accurately
+ * positioned.
+ * @property spatialElevationLevel the resting level of the elevated popup. Defaults to
+ * [SpatialElevationLevel.Level3].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SpatialPopupProperties(
+ @get:Suppress("GetterSetterNames") public val focusable: Boolean = false,
+ @get:Suppress("GetterSetterNames") public val dismissOnBackPress: Boolean = true,
+ @get:Suppress("GetterSetterNames") public val dismissOnClickOutside: Boolean = true,
+ @get:Suppress("GetterSetterNames") public val clippingEnabled: Boolean = true,
+ public val spatialElevationLevel: SpatialElevationLevel = SpatialElevationLevel.Level3,
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is SpatialPopupProperties) return false
+
+ if (focusable != other.focusable) return false
+ if (dismissOnBackPress != other.dismissOnBackPress) return false
+ if (dismissOnClickOutside != other.dismissOnClickOutside) return false
+ if (clippingEnabled != other.clippingEnabled) return false
+ if (spatialElevationLevel != other.spatialElevationLevel) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = focusable.hashCode()
+ result = 31 * result + dismissOnBackPress.hashCode()
+ result = 31 * result + dismissOnClickOutside.hashCode()
+ result = 31 * result + clippingEnabled.hashCode()
+ result = 31 * result + spatialElevationLevel.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "SpatialPopupProperties(focusable=$focusable, dismissOnBackPress=$dismissOnBackPress, dismissOnClickOutside=$dismissOnClickOutside, clippingEnabled=$clippingEnabled, spatialElevationLevel=$spatialElevationLevel)"
+ }
+
+ public fun copy(
+ focusable: Boolean = this.focusable,
+ dismissOnBackPress: Boolean = this.dismissOnBackPress,
+ dismissOnClickOutside: Boolean = this.dismissOnClickOutside,
+ clippingEnabled: Boolean = this.clippingEnabled,
+ spatialElevationLevel: SpatialElevationLevel = this.spatialElevationLevel,
+ ): SpatialPopupProperties =
+ SpatialPopupProperties(
+ focusable = focusable,
+ dismissOnBackPress = dismissOnBackPress,
+ dismissOnClickOutside = dismissOnClickOutside,
+ clippingEnabled = clippingEnabled,
+ spatialElevationLevel = spatialElevationLevel,
+ )
+}
+
+private fun SpatialPopupProperties.toPopupProperties() =
+ PopupProperties(
+ focusable = focusable,
+ dismissOnBackPress = dismissOnBackPress,
+ dismissOnClickOutside = dismissOnClickOutside,
+ clippingEnabled = clippingEnabled,
+ )
+
+/**
+ * A composable that creates a panel in 3D space to hoist Popup based composables.
+ *
+ * @param alignment the alignment of the popup relative to its parent.
+ * @param offset An offset from the original aligned position of the popup. Offset respects the
+ * Ltr/Rtl context, thus in Ltr it will be added to the original aligned position and in Rtl it
+ * will be subtracted from it.
+ * @param onDismissRequest callback invoked when the user requests to dismiss the popup (e.g., by
+ * clicking outside).
+ * @param properties [PopupProperties] configuration properties for further customization of this
+ * popup's behavior.
+ * @param content the composable content to be displayed within the popup.
+ */
+@Composable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SpatialPopup(
+ alignment: Alignment = Alignment.TopStart,
+ offset: IntOffset = IntOffset(0, 0),
+ onDismissRequest: (() -> Unit)? = null,
+ properties: SpatialPopupProperties = SpatialPopupProperties(),
+ content: @Composable () -> Unit,
+) {
+ if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
+ LayoutSpatialPopup(
+ alignment = alignment,
+ offset = offset,
+ onDismissRequest = onDismissRequest,
+ properties = properties,
+ content = content,
+ )
+ } else {
+ Popup(
+ alignment = alignment,
+ offset = offset,
+ onDismissRequest = onDismissRequest,
+ properties = properties.toPopupProperties(),
+ content = content,
+ )
+ }
+}
+
+/**
+ * Composable that creates a panel in 3d space to hoist Popup based composables.
+ *
+ * @param alignment The alignment relative to the parent.
+ * @param offset An offset from the original aligned position of the popup. Offset respects the
+ * Ltr/Rtl context, thus in Ltr it will be added to the original aligned position and in Rtl it
+ * will be subtracted from it.
+ * @param onDismissRequest Executes when the user clicks outside of the popup.
+ * @param properties [PopupProperties] for further customization of this popup's behavior.
+ * @param content The content to be displayed inside the popup.
+ */
+@Composable
+private fun LayoutSpatialPopup(
+ alignment: Alignment = Alignment.TopStart,
+ offset: IntOffset = IntOffset(0, 0),
+ onDismissRequest: (() -> Unit)? = null,
+ properties: SpatialPopupProperties = SpatialPopupProperties(),
+ content: @Composable () -> Unit,
+) {
+ val popupPositioner =
+ remember(alignment, offset) { AlignmentOffsetPositionProvider(alignment, offset) }
+ LayoutSpatialPopup(
+ popupPositionProvider = popupPositioner,
+ onDismissRequest = onDismissRequest,
+ properties = properties,
+ content = content,
+ )
+}
+
+/**
+ * Opens a popup with the given content.
+ *
+ * The popup is positioned using a custom [popupPositionProvider].
+ *
+ * @param popupPositionProvider Provides the screen position of the popup.
+ * @param onDismissRequest Executes when the user clicks outside of the popup.
+ * @param properties [SpatialPopupProperties] for further customization of this popup's behavior.
+ * @param content The content to be displayed inside the popup.
+ */
+@Composable
+private fun LayoutSpatialPopup(
+ popupPositionProvider: PopupPositionProvider,
+ onDismissRequest: (() -> Unit)? = null,
+ properties: SpatialPopupProperties = SpatialPopupProperties(),
+ content: @Composable () -> Unit,
+) {
+ val restingLevel by remember { mutableStateOf(properties.spatialElevationLevel) }
+ var contentSize: IntSize by remember { mutableStateOf(IntSize.Zero) }
+ var parentLayoutDirection = LocalLayoutDirection.current
+ var anchorBounds by remember { mutableStateOf(IntRect.Zero) }
+ val fullScreenRect = getWindowVisibleDisplayFrame()
+ val windowSize = IntSize(fullScreenRect.width(), fullScreenRect.height())
+
+ val popupOffset by remember {
+ derivedStateOf {
+ popupPositionProvider.calculatePosition(
+ anchorBounds,
+ windowSize,
+ parentLayoutDirection,
+ contentSize,
+ )
+ }
+ }
+
+ // The coordinates should be re-calculated on every layout to properly retrieve the absolute
+ // bounds for popup content offset calculation.
+ Layout(
+ content = {},
+ modifier =
+ Modifier.onGloballyPositioned { childCoordinates ->
+ val parentCoordinates = childCoordinates.parentLayoutCoordinates!!
+ val layoutSize = parentCoordinates.size
+ val position = parentCoordinates.positionInWindow()
+ val layoutPosition =
+ IntOffset(position.x.fastRoundToInt(), position.y.fastRoundToInt())
+
+ anchorBounds = IntRect(layoutPosition, layoutSize)
+ },
+ ) { _, _ ->
+ parentLayoutDirection = layoutDirection
+ layout(0, 0) {}
+ }
+
+ ElevatedPanel(
+ spatialElevationLevel = restingLevel,
+ contentSize = contentSize,
+ contentOffset = Offset(popupOffset.x.toFloat(), popupOffset.y.toFloat()),
+ ) {
+ OutsideInputHandler(enabled = properties.dismissOnClickOutside) {
+ onDismissRequest?.invoke()
+ }
+ Box(
+ Modifier.constrainTo(
+ Constraints(
+ minWidth = 0,
+ maxWidth = Constraints.Infinity,
+ minHeight = 0,
+ maxHeight = Constraints.Infinity,
+ )
+ )
+ .onSizeChanged { contentSize = it }
+ ) {
+ content()
+ }
+ }
+}
+
+// Get the visible display Rect for the current window of the device
+@Composable
+private fun getWindowVisibleDisplayFrame(): Rect {
+ return Rect().apply { LocalView.current.getWindowVisibleDisplayFrame(this) }
+}
+
+/**
+ * Opens a popup with the given content.
+ *
+ * @param spatialElevationLevel the resting elevation level of the popup.
+ * @param content the composable content to be displayed within the popup, along with a callback
+ * which is explicitly to be used for the [onGloballyPositioned] modifier of the Popup composable.
+ */
+@Composable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SpatialPopup(
+ spatialElevationLevel: SpatialElevationLevel = SpatialElevationLevel.Level0,
+ content: @Composable (onGloballyPositioned: (LayoutCoordinates) -> Unit) -> Unit,
+) {
+ var contentSize: IntSize by remember { mutableStateOf(IntSize.Zero) }
+ var contentOffset by remember { mutableStateOf(Offset.Zero) }
+
+ if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
+ ElevatedPanel(
+ spatialElevationLevel = spatialElevationLevel,
+ contentSize = contentSize,
+ contentOffset = contentOffset,
+ ) {
+ content { coordinates ->
+ contentSize = coordinates.size
+ contentOffset = coordinates.positionInRoot()
+ }
+ }
+ } else {
+ content {}
+ }
+}
+
+/** Calculates the position of a [Popup] on a screen. */
+@Immutable
+private interface PopupPositionProvider {
+ /**
+ * Calculates the position of a [Popup] on screen.
+ *
+ * The window size is useful in cases where the popup is meant to be positioned next to its
+ * anchor instead of inside of it. The size can be used to calculate available space around the
+ * parent to find a spot with enough clearance (e.g. when implementing a dropdown). Note that
+ * positioning the popup outside of the window bounds might prevent it from being visible.
+ *
+ * @param anchorBounds the bounds of the anchor layout relative to the window.
+ * @param windowSize The size of the window containing the anchor layout.
+ * @param layoutDirection The layout direction of the anchor layout.
+ * @param popupContentSize The size of the popup's content.
+ * @return The window relative position where the popup should be positioned.
+ */
+ fun calculatePosition(
+ anchorBounds: IntRect,
+ windowSize: IntSize,
+ layoutDirection: LayoutDirection,
+ popupContentSize: IntSize,
+ ): IntOffset
+}
+
+/** Calculates the position of a [Popup] on screen. */
+private class AlignmentOffsetPositionProvider(val alignment: Alignment, val offset: IntOffset) :
+ PopupPositionProvider {
+
+ /**
+ * Calculates the position of a [Popup] on screen.
+ *
+ * The window size is useful in cases where the popup is meant to be positioned next to its
+ * anchor instead of inside of it. The size can be used to calculate available space around the
+ * parent to find a spot with enough clearance (e.g. when implementing a dropdown). Note that
+ * positioning the popup outside of the window bounds might prevent it from being visible.
+ *
+ * @param anchorBounds The window relative bounds of the layout which this popup is anchored to.
+ * @param windowSize The size of the window containing the anchor layout.
+ * @param layoutDirection The layout direction of the anchor layout.
+ * @param popupContentSize The size of the popup's content.
+ * @return The window relative position where the popup should be positioned.
+ */
+ override fun calculatePosition(
+ anchorBounds: IntRect,
+ windowSize: IntSize,
+ layoutDirection: LayoutDirection,
+ popupContentSize: IntSize,
+ ): IntOffset {
+ val anchorAlignmentPoint = alignment.align(IntSize.Zero, anchorBounds.size, layoutDirection)
+ // Note the negative sign. Popup alignment point contributes negative offset.
+ val popupAlignmentPoint = -alignment.align(IntSize.Zero, popupContentSize, layoutDirection)
+ val resolvedUserOffset =
+ IntOffset(offset.x * (if (layoutDirection == LayoutDirection.Ltr) 1 else -1), offset.y)
+
+ return anchorBounds.topLeft +
+ anchorAlignmentPoint +
+ popupAlignmentPoint +
+ resolvedUserOffset
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/Subspace.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/Subspace.kt
new file mode 100644
index 0000000..6c56106
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/spatial/Subspace.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.spatial
+
+import androidx.activity.ComponentActivity
+import androidx.annotation.RestrictTo
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.compositionLocalWithComputedDefaultOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCompositionContext
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.IntSize
+import androidx.xr.compose.platform.LocalPanelEntity
+import androidx.xr.compose.platform.LocalSession
+import androidx.xr.compose.platform.LocalSpatialCapabilities
+import androidx.xr.compose.platform.SpatialComposeScene
+import androidx.xr.compose.platform.getActivity
+import androidx.xr.compose.subspace.SpatialBox
+import androidx.xr.compose.subspace.SpatialBoxScope
+import androidx.xr.compose.subspace.SubspaceComposable
+import androidx.xr.compose.subspace.layout.CoreContentlessEntity
+import androidx.xr.compose.subspace.layout.SubspaceLayout
+import androidx.xr.compose.unit.IntVolumeSize
+import androidx.xr.compose.unit.VolumeConstraints
+import androidx.xr.runtime.math.Pose
+
+private val LocalIsInApplicationSubspace: ProvidableCompositionLocal<Boolean> =
+ compositionLocalWithComputedDefaultOf {
+ LocalPanelEntity.currentValue != null
+ }
+
+/**
+ * Create a 3D area that the app can render spatial content into.
+ *
+ * If this is the topmost [Subspace] in the compose hierarchy then this will expand to fill all of
+ * the available space and will not be bound by its containing window.
+ *
+ * If this is nested within another [Subspace] then it will lay out its content in the X and Y
+ * directions according to the layout logic of its parent in 2D space. It will be constrained in the
+ * Z direction according to the constraints imposed by its containing [Subspace].
+ *
+ * This is a no-op and does not render anything in non-XR environments (i.e. Phone and Tablet).
+ *
+ * @param content The 3D content to render within this Subspace.
+ */
+@Composable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun Subspace(content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit) {
+ val activity = LocalContext.current.getActivity()
+
+ // TODO(b/369446163) Test the case where a NestedSubspace could be created outside of a Panel.
+ if (LocalSpatialCapabilities.current.isSpatialUiEnabled && activity is ComponentActivity) {
+ if (LocalIsInApplicationSubspace.current) {
+ NestedSubspace(activity, content = content)
+ } else {
+ ApplicationSubspace(activity, content = content)
+ }
+ }
+}
+
+/**
+ * Create a Subspace that is rooted in the application space.
+ *
+ * This is used as the top-level [Subspace] within the context of the default task window. Nested
+ * Subspaces should use their nearest Panel that contains the [Subspace] to determine the sizing
+ * constraints and position of the [Subspace].
+ */
+@Composable
+private fun ApplicationSubspace(
+ activity: ComponentActivity,
+ content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit,
+) {
+ val session = checkNotNull(LocalSession.current) { "session must be initialized" }
+ val compositionContext = rememberCompositionContext()
+ val scene = remember {
+ SpatialComposeScene(
+ ownerActivity = activity,
+ jxrSession = session,
+ parentCompositionContext = compositionContext,
+ )
+ }
+
+ DisposableEffect(session) {
+ session.mainPanelEntity.setHidden(true)
+ onDispose { session.mainPanelEntity.setHidden(false) }
+ }
+
+ SideEffect {
+ scene.setContent {
+ CompositionLocalProvider(LocalIsInApplicationSubspace provides true) {
+ SpatialBox(content = content)
+ }
+ }
+ }
+
+ DisposableEffect(scene) { onDispose { scene.dispose() } }
+}
+
+@Composable
+private fun NestedSubspace(
+ activity: ComponentActivity,
+ content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit,
+) {
+ val session = checkNotNull(LocalSession.current) { "session must be initialized" }
+ val compositionContext = rememberCompositionContext()
+ val panelEntity = LocalPanelEntity.current
+ // The subspace root node will be owned and manipulated by the containing composition, we need a
+ // container that we can manipulate at the Subspace level in order to position the entire
+ // subspace
+ // properly.
+ val subspaceRootContainer = remember {
+ session.createEntity("SubspaceRootContainer").apply { setParent(panelEntity) }
+ }
+ val scene = remember {
+ val subspaceRoot =
+ session.createEntity("SubspaceRoot").apply { setParent(subspaceRootContainer) }
+ SpatialComposeScene(
+ ownerActivity = activity,
+ jxrSession = session,
+ parentCompositionContext = compositionContext,
+ rootEntity = CoreContentlessEntity(subspaceRoot),
+ )
+ }
+ var measuredSize by remember { mutableStateOf(IntVolumeSize.Zero) }
+ var contentOffset by remember { mutableStateOf(Offset.Zero) }
+ val pose =
+ rememberCalculatePose(
+ contentOffset,
+ LocalView.current.size,
+ measuredSize.run { IntSize(width, height) },
+ )
+
+ LaunchedEffect(pose) { subspaceRootContainer.setPose(pose) }
+ DisposableEffect(Unit) { onDispose { scene.dispose() } }
+
+ Layout(modifier = Modifier.onGloballyPositioned { contentOffset = it.positionInRoot() }) {
+ _,
+ constraints ->
+ scene.setContent {
+ SubspaceLayout(content = { SpatialBox(content = content) }) { measurables, _ ->
+ val placeables =
+ measurables.map {
+ it.measure(
+ VolumeConstraints(
+ minWidth = constraints.minWidth,
+ maxWidth = constraints.maxWidth,
+ minHeight = constraints.minHeight,
+ maxHeight = constraints.maxHeight,
+ // TODO(b/366564066) Nested Subspaces should get their depth
+ // constraints from
+ // the parent Subspace
+ minDepth = 0,
+ maxDepth = Int.MAX_VALUE,
+ )
+ )
+ }
+ measuredSize =
+ IntVolumeSize(
+ width = placeables.maxOf { it.measuredWidth },
+ height = placeables.maxOf { it.measuredHeight },
+ depth = placeables.maxOf { it.measuredDepth },
+ )
+ layout(measuredSize.width, measuredSize.height, measuredSize.depth) {
+ placeables.forEach { it.place(Pose.Identity) }
+ }
+ }
+ }
+
+ layout(measuredSize.width, measuredSize.height) {}
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/RememberComposeView.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/RememberComposeView.kt
new file mode 100644
index 0000000..81dd734
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/RememberComposeView.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace
+
+import androidx.annotation.RestrictTo
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCompositionContext
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.findViewTreeViewModelStoreOwner
+import androidx.lifecycle.setViewTreeLifecycleOwner
+import androidx.lifecycle.setViewTreeViewModelStoreOwner
+import androidx.savedstate.findViewTreeSavedStateRegistryOwner
+import androidx.savedstate.setViewTreeSavedStateRegistryOwner
+import java.util.UUID
+
+/**
+ * Create a [ComposeView] that is applicable to the local context.
+ *
+ * This handles propagating the composition context and ensures that savable state is remembered.
+ */
+@Composable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun rememberComposeView(content: @Composable () -> Unit): ComposeView {
+ val localId = rememberSaveable { UUID.randomUUID() }
+ val context = LocalContext.current
+ val parentView = LocalView.current
+ val compositionContext = rememberCompositionContext()
+
+ return remember {
+ ComposeView(context).apply {
+ id = android.R.id.content
+ setViewTreeLifecycleOwner(parentView.findViewTreeLifecycleOwner())
+ setViewTreeViewModelStoreOwner(parentView.findViewTreeViewModelStoreOwner())
+ setViewTreeSavedStateRegistryOwner(parentView.findViewTreeSavedStateRegistryOwner())
+ // Dispose of the Composition when the view's LifecycleOwner is destroyed
+ setParentCompositionContext(compositionContext)
+ // Set unique id for AbstractComposeView. This allows state restoration for the
+ // state
+ // defined inside the ElevatedSurface via rememberSaveable()
+ setTag(
+ androidx.compose.ui.R.id.compose_view_saveable_id_tag,
+ "ComposeView:$localId"
+ )
+
+ // Enable children to draw their shadow by not clipping them
+ clipChildren = false
+ }
+ }
+ .apply {
+ setContent(content)
+ DisposableEffect(this) { onDispose { disposeComposition() } }
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/RememberCoreEntity.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/RememberCoreEntity.kt
new file mode 100644
index 0000000..1d1107c
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/RememberCoreEntity.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisallowComposableCalls
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
+import androidx.xr.compose.platform.LocalSession
+import androidx.xr.compose.subspace.layout.CoreContentlessEntity
+import androidx.xr.compose.subspace.layout.CorePanelEntity
+import androidx.xr.scenecore.BasePanelEntity
+import androidx.xr.scenecore.Entity
+import androidx.xr.scenecore.Session
+
+/**
+ * Creates a [CoreContentlessEntity] that is automatically disposed of when it leaves the
+ * composition.
+ */
+@Composable
+internal inline fun rememberCoreContentlessEntity(
+ crossinline entityFactory: @DisallowComposableCalls Session.() -> Entity
+): CoreContentlessEntity {
+ val session = checkNotNull(LocalSession.current) { "session must be initialized" }
+ val entity = remember { session.entityFactory() }
+
+ DisposableEffect(entity) { onDispose { entity.dispose() } }
+
+ return remember { CoreContentlessEntity(entity) }
+}
+
+/** Creates a [CorePanelEntity] that is automatically disposed of when it leaves the composition. */
+@Composable
+internal inline fun rememberCorePanelEntity(
+ crossinline entityFactory: @DisallowComposableCalls Session.() -> BasePanelEntity<*>
+): CorePanelEntity {
+ val session = checkNotNull(LocalSession.current) { "session must be initialized" }
+ val entity = remember { session.entityFactory() }
+
+ DisposableEffect(entity) { onDispose { entity.dispose() } }
+
+ return remember { CorePanelEntity(session, entity) }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/RowColumnImpl.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/RowColumnImpl.kt
new file mode 100644
index 0000000..9e3e1b5
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/RowColumnImpl.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace
+
+import androidx.xr.compose.subspace.layout.ParentLayoutParamsAdjustable
+import androidx.xr.compose.subspace.layout.ParentLayoutParamsModifier
+import androidx.xr.compose.subspace.layout.SpatialAlignment
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.node.SubspaceModifierElement
+
+internal class LayoutWeightElement(val weight: Float, val fill: Boolean) :
+ SubspaceModifierElement<LayoutWeightNode>() {
+ override fun create(): LayoutWeightNode = LayoutWeightNode(weight = weight, fill = fill)
+
+ override fun update(node: LayoutWeightNode) {
+ node.weight = weight
+ node.fill = fill
+ }
+
+ override fun hashCode(): Int {
+ var result = weight.hashCode()
+ result = 31 * result + fill.hashCode()
+ return result
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ val otherModifier = other as? LayoutWeightElement ?: return false
+ return weight == otherModifier.weight && fill == otherModifier.fill
+ }
+}
+
+internal class LayoutWeightNode(var weight: Float, var fill: Boolean) :
+ SubspaceModifier.Node(), ParentLayoutParamsModifier {
+ override fun adjustParams(params: ParentLayoutParamsAdjustable) {
+ if (params is RowColumnParentData) {
+ params.weight = weight
+ params.fill = fill
+ }
+ }
+}
+
+internal data class RowColumnParentData(var weight: Float = 0f, var fill: Boolean = true) :
+ ParentLayoutParamsAdjustable
+
+internal class RowColumnAlignElement(
+ val horizontalSpatialAlignment: SpatialAlignment.Horizontal? = null,
+ val verticalSpatialAlignment: SpatialAlignment.Vertical? = null,
+ val depthSpatialAlignment: SpatialAlignment.Depth? = null,
+) : SubspaceModifierElement<RowColumnAlignNode>() {
+ override fun create(): RowColumnAlignNode =
+ RowColumnAlignNode(
+ horizontalSpatialAlignment,
+ verticalSpatialAlignment,
+ depthSpatialAlignment
+ )
+
+ override fun update(node: RowColumnAlignNode) {
+ node.horizontalSpatialAlignment = horizontalSpatialAlignment
+ node.verticalSpatialAlignment = verticalSpatialAlignment
+ node.depthSpatialAlignment = depthSpatialAlignment
+ }
+
+ override fun hashCode(): Int {
+ var result = horizontalSpatialAlignment.hashCode()
+ result = 31 * result + verticalSpatialAlignment.hashCode()
+ result = 31 * result + depthSpatialAlignment.hashCode()
+ return result
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ val otherModifier = other as? RowColumnAlignElement ?: return false
+ return horizontalSpatialAlignment == otherModifier.horizontalSpatialAlignment &&
+ verticalSpatialAlignment == otherModifier.verticalSpatialAlignment &&
+ depthSpatialAlignment == otherModifier.depthSpatialAlignment
+ }
+}
+
+internal class RowColumnAlignNode(
+ var horizontalSpatialAlignment: SpatialAlignment.Horizontal? = null,
+ var verticalSpatialAlignment: SpatialAlignment.Vertical? = null,
+ var depthSpatialAlignment: SpatialAlignment.Depth? = null,
+) : SubspaceModifier.Node(), ParentLayoutParamsModifier {
+ override fun adjustParams(params: ParentLayoutParamsAdjustable) {
+ if (params !is RowColumnSpatialAlignmentParentData) return
+ if (horizontalSpatialAlignment != null) {
+ params.horizontalSpatialAlignment = horizontalSpatialAlignment
+ }
+ if (verticalSpatialAlignment != null) {
+ params.verticalSpatialAlignment = verticalSpatialAlignment
+ }
+ if (depthSpatialAlignment != null) {
+ params.depthSpatialAlignment = depthSpatialAlignment
+ }
+ }
+}
+
+internal data class RowColumnSpatialAlignmentParentData(
+ var horizontalSpatialAlignment: SpatialAlignment.Horizontal? = null,
+ var verticalSpatialAlignment: SpatialAlignment.Vertical? = null,
+ var depthSpatialAlignment: SpatialAlignment.Depth? = null,
+) : ParentLayoutParamsAdjustable
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/RowColumnMeasurePolicy.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/RowColumnMeasurePolicy.kt
new file mode 100644
index 0000000..9bbd6bb
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/RowColumnMeasurePolicy.kt
@@ -0,0 +1,353 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace
+
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.util.fastRoundToInt
+import androidx.xr.compose.subspace.LayoutOrientation.Horizontal
+import androidx.xr.compose.subspace.LayoutOrientation.Vertical
+import androidx.xr.compose.subspace.layout.Measurable
+import androidx.xr.compose.subspace.layout.MeasurePolicy
+import androidx.xr.compose.subspace.layout.MeasureResult
+import androidx.xr.compose.subspace.layout.MeasureScope
+import androidx.xr.compose.subspace.layout.Placeable
+import androidx.xr.compose.subspace.layout.SpatialAlignment
+import androidx.xr.compose.unit.IntVolumeSize
+import androidx.xr.compose.unit.VolumeConstraints
+import androidx.xr.compose.unit.constrainDepth
+import androidx.xr.compose.unit.constrainHeight
+import androidx.xr.compose.unit.constrainWidth
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Vector3
+import kotlin.math.cos
+import kotlin.math.sign
+import kotlin.math.sin
+
+/** Shared [MeasurePolicy] between [Row] and [Column]. */
+internal class RowColumnMeasurePolicy(
+ private val orientation: LayoutOrientation,
+ private val alignment: SpatialAlignment,
+ private val curveRadius: Dp,
+) : MeasurePolicy {
+ override fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: VolumeConstraints,
+ ): MeasureResult {
+ // Space taken up on the main axis by children with no weight modifier
+ var fixedSpace = 0
+
+ // The total amount of weight declared across all children
+ var totalWeight = 0f
+
+ val resolvedMeasurables = measurables.map { ResolvedMeasurable(it) }
+
+ // We will measure non-weighted children in this pass.
+ resolvedMeasurables.forEach { resolvedMeasurable ->
+ if (resolvedMeasurable.weightInfo.weight > 0f) {
+ // Children with weight will be measured after all others
+ totalWeight += resolvedMeasurable.weightInfo.weight
+ } else {
+ // Children without weight will be measured now
+ resolvedMeasurable.placeable =
+ resolvedMeasurable.measurable
+ .measure(constraints.plusMainAxis(-fixedSpace))
+ .also { fixedSpace += it.mainAxisSize() }
+ }
+ }
+
+ // Now we can measure the weighted children (if any).
+ if (totalWeight > 0f) {
+ // Amount of space this Row/Column wants to fill up
+ val targetSpace = constraints.mainAxisTargetSpace()
+ // Amount of space left (after the non-weighted children were measured)
+ val remainingToTarget = (targetSpace - fixedSpace).coerceAtLeast(0)
+ // Amount of space that would be given to a weighted child with `.weight(1f)`
+ val weightUnitSpace = remainingToTarget / totalWeight
+
+ // First pass through the weighted children, we just want to see what the remaining
+ // space is.
+ // Due to rounding, we may over/underfill the container.
+ //
+ // For example, if we have a 200dp row with 7 children, each with a weight of 1f:
+ // 200/7 ~= 28.57..., which gets rounded to 29. But 29*7 = 203, so we've overfilled our
+ // 200dp row by 3dp.
+ //
+ // The fix is to track this remainder N, and adjust the first N children by 1dp or -1dp
+ // so
+ // that the row/column will be the exact size we need.
+ var remainder = remainingToTarget
+ resolvedMeasurables.forEach { resolvedMeasurable ->
+ if (resolvedMeasurable.weightInfo.weight <= 0f) return@forEach
+ val weightedSize = resolvedMeasurable.weightInfo.weight * weightUnitSpace
+ remainder -= weightedSize.fastRoundToInt()
+ }
+
+ resolvedMeasurables.forEach { resolvedMeasurable ->
+ if (resolvedMeasurable.weightInfo.weight <= 0f) return@forEach
+ val childMainAxisSize = run {
+ val remainderUnit = remainder.sign
+ remainder -= remainderUnit
+ val weightedSize = resolvedMeasurable.weightInfo.weight * weightUnitSpace
+ weightedSize.fastRoundToInt() + remainderUnit
+ }
+ val childConstraints =
+ buildConstraints(
+ mainAxisMin =
+ if (resolvedMeasurable.weightInfo.fill) childMainAxisSize else 0,
+ mainAxisMax = childMainAxisSize,
+ crossAxisMin = constraints.crossAxisMin(),
+ crossAxisMax = constraints.crossAxisMax(),
+ minDepth = constraints.minDepth,
+ maxDepth = constraints.maxDepth,
+ )
+ resolvedMeasurable.placeable =
+ resolvedMeasurable.measurable.measure(childConstraints)
+ }
+ }
+
+ val contentSize = resolvedMeasurables.contentSize()
+
+ val containerSize =
+ IntVolumeSize(
+ width = constraints.constrainWidth(contentSize.width),
+ height = constraints.constrainHeight(contentSize.height),
+ depth = constraints.constrainDepth(contentSize.depth),
+ )
+
+ // Each child will have its main-axis offset adjusted, based on extra space available and
+ // the
+ // provided alignment.
+ val mainAxisOffset =
+ if (isHorizontal()) {
+ // `mainAxisOffset` represents the left edge of the content in the container space.
+ alignment.horizontalOffset(contentSize.width, containerSize.width) -
+ contentSize.width / 2
+ } else {
+ // `mainAxisOffset` represents the top edge of the content in the container space.
+ alignment.verticalOffset(contentSize.height, containerSize.height) +
+ contentSize.height / 2
+ }
+ resolvedMeasurables.forEach { resolvedMeasurable ->
+ // Adjust main-axis offset appropriately.
+ resolvedMeasurable.mainAxisPosition =
+ resolvedMeasurable.mainAxisPosition!! + mainAxisOffset
+
+ // Set child's cross-axis position based on its desired size + the container's
+ // size/alignment.
+ val crossAxisSize = resolvedMeasurable.placeable!!.crossAxisSize()
+ resolvedMeasurable.crossAxisPosition =
+ if (isHorizontal()) {
+ resolvedMeasurable.verticalOffset(
+ crossAxisSize,
+ containerSize.height,
+ alignment
+ )
+ } else {
+ resolvedMeasurable.horizontalOffset(
+ crossAxisSize,
+ containerSize.width,
+ alignment
+ )
+ }
+ }
+
+ return layout(containerSize.width, containerSize.height, containerSize.depth) {
+ resolvedMeasurables.forEach { resolvedMeasurable ->
+ val placeable = resolvedMeasurable.placeable!!
+ val mainAxisPosition = resolvedMeasurable.mainAxisPosition!!
+ val crossAxisPosition = resolvedMeasurable.crossAxisPosition!!
+ val depthPosition =
+ resolvedMeasurable.depthOffset(
+ placeable.measuredDepth,
+ containerSize.depth,
+ alignment
+ )
+ var position =
+ Vector3(
+ x =
+ if (isHorizontal()) mainAxisPosition.toFloat()
+ else crossAxisPosition.toFloat(),
+ y =
+ if (isHorizontal()) crossAxisPosition.toFloat()
+ else mainAxisPosition.toFloat(),
+ z = depthPosition.toFloat(),
+ )
+ var orientation = Quaternion.Identity
+
+ if (curveRadius != Dp.Infinity) {
+ val pixelsCurveRadius = curveRadius.toPx()
+
+ // NOTE: Orientation needs to be computed first, otherwise position
+ // gets overwritten with the new position which will lead to an
+ // incorrect orientation calculation.
+ orientation = getOrientationTangentToCircle(position, pixelsCurveRadius)
+ position = getPositionOnCircle(position, pixelsCurveRadius)
+ }
+
+ placeable.place(Pose(position, orientation))
+ }
+ }
+ }
+
+ private fun isHorizontal() = orientation == LayoutOrientation.Horizontal
+
+ private fun Placeable.mainAxisSize() = if (isHorizontal()) measuredWidth else measuredHeight
+
+ private fun Placeable.crossAxisSize() = if (isHorizontal()) measuredHeight else measuredWidth
+
+ private fun VolumeConstraints.plusMainAxis(addToMainAxis: Int): VolumeConstraints {
+ val newMainAxisValue =
+ if (isHorizontal()) {
+ maxWidth + addToMainAxis
+ } else {
+ maxHeight + addToMainAxis
+ }
+ return VolumeConstraints(
+ minWidth = 0,
+ maxWidth = if (isHorizontal()) newMainAxisValue else maxWidth,
+ minHeight = 0,
+ maxHeight = if (isHorizontal()) maxHeight else newMainAxisValue,
+ minDepth = 0,
+ maxDepth = maxDepth,
+ )
+ }
+
+ private fun buildConstraints(
+ mainAxisMin: Int,
+ mainAxisMax: Int,
+ crossAxisMin: Int,
+ crossAxisMax: Int,
+ minDepth: Int,
+ maxDepth: Int,
+ ): VolumeConstraints {
+ val isHorizontal = isHorizontal()
+ return VolumeConstraints(
+ minWidth = if (isHorizontal) mainAxisMin else crossAxisMin,
+ maxWidth = if (isHorizontal) mainAxisMax else crossAxisMax,
+ minHeight = if (isHorizontal) crossAxisMin else mainAxisMin,
+ maxHeight = if (isHorizontal) crossAxisMax else mainAxisMax,
+ minDepth = minDepth,
+ maxDepth = maxDepth,
+ )
+ }
+
+ private fun List<ResolvedMeasurable>.contentSize(): IntVolumeSize {
+ // Content's main-axis size is the sum of all children's main-axis sizes
+ var mainAxisSize = 0
+ val mainAxisMultiplier = if (isHorizontal()) 1 else -1
+ // Content's cross-axis and depth size are the max measured value of all children
+ var crossAxisSize = 0
+ var depthSize = 0
+ this.forEach { resolvedMeasurable ->
+ val placeable = resolvedMeasurable.placeable!!
+ resolvedMeasurable.mainAxisPosition =
+ ((mainAxisSize + placeable.mainAxisSize() / 2.0f) * mainAxisMultiplier)
+ .fastRoundToInt()
+ mainAxisSize += placeable.mainAxisSize()
+ crossAxisSize = maxOf(crossAxisSize, placeable.crossAxisSize())
+ depthSize = maxOf(depthSize, placeable.measuredDepth)
+ }
+ return IntVolumeSize(
+ width = if (isHorizontal()) mainAxisSize else crossAxisSize,
+ height = if (isHorizontal()) crossAxisSize else mainAxisSize,
+ depth = depthSize,
+ )
+ }
+
+ private fun VolumeConstraints.mainAxisTargetSpace(): Int {
+ val mainAxisMax = if (isHorizontal()) maxWidth else maxHeight
+ return if (mainAxisMax != VolumeConstraints.INFINITY) {
+ mainAxisMax
+ } else {
+ if (isHorizontal()) minWidth else minHeight
+ }
+ }
+
+ private fun VolumeConstraints.crossAxisMin(): Int = if (isHorizontal()) minHeight else minWidth
+
+ private fun VolumeConstraints.crossAxisMax(): Int = if (isHorizontal()) maxHeight else maxWidth
+}
+
+// [radius], like [position], should be in pixels.
+private fun getPositionOnCircle(position: Vector3, radius: Float): Vector3 {
+ // NOTE: This method is hard coded to work with rows. Needs to be made
+ // slightly more general to work with columns.
+ val arclength = position.x // Signed, negative means arc extends to left.
+ val theta = arclength / radius
+ val x = radius * sin(theta)
+ val y = position.y
+ val z = radius * (1.0f - cos(theta)) + position.z
+ return Vector3(x.toInt().toFloat(), y.toInt().toFloat(), z.toInt().toFloat())
+}
+
+// [radius], like [position], should be in pixels.
+private fun getOrientationTangentToCircle(position: Vector3, radius: Float): Quaternion {
+ // NOTE: This method is hard coded to work with rows. Needs to be made
+ // slightly more general to work with columns.
+ val arclength = position.x // Signed, negative means arc extends to left.
+ val theta = arclength / radius
+
+ // We need to rotate by negative theta (clockwise) around the Y axis.
+ val qX = 0.0f
+ val qY = sin(-theta * 0.5f)
+ val qZ = 0.0f
+ val qW = cos(-theta * 0.5f)
+
+ return Quaternion(qX, qY, qZ, qW)
+}
+
+/** A [Measurable] and all associated information computed in [RowColumnMeasurePolicy.measure]. */
+private class ResolvedMeasurable(val measurable: Measurable) {
+ /** Parameters set by the [RowScope.weight] and [ColumnScope.weight] modifiers. */
+ val weightInfo: RowColumnParentData = RowColumnParentData().also { measurable.adjustParams(it) }
+
+ /** Parameters set by the [RowScope.align] and [ColumnScope.align] modifiers. */
+ val alignment: RowColumnSpatialAlignmentParentData =
+ RowColumnSpatialAlignmentParentData().also { measurable.adjustParams(it) }
+
+ /** A measured placeable, only present once [Measurable.measure] is called on [measurable]. */
+ var placeable: Placeable? = null
+
+ /** The main-axis position of this child in its parent; set after all children are measured. */
+ var mainAxisPosition: Int? = null
+
+ /** The cross-axis position of this child in its parent; set after all children are measured. */
+ var crossAxisPosition: Int? = null
+
+ fun horizontalOffset(width: Int, space: Int, parentSpatialAlignment: SpatialAlignment): Int =
+ alignment.horizontalSpatialAlignment?.offset(width, space)
+ ?: parentSpatialAlignment.horizontalOffset(width, space)
+
+ fun verticalOffset(height: Int, space: Int, parentSpatialAlignment: SpatialAlignment): Int =
+ alignment.verticalSpatialAlignment?.offset(height, space)
+ ?: parentSpatialAlignment.verticalOffset(height, space)
+
+ fun depthOffset(depth: Int, space: Int, parentSpatialAlignment: SpatialAlignment): Int =
+ alignment.depthSpatialAlignment?.offset(depth, space)
+ ?: parentSpatialAlignment.depthOffset(depth, space)
+
+ override fun toString(): String {
+ return measurable.toString()
+ }
+}
+
+/** [Row] is [Horizontal], [Column] is [Vertical]. */
+internal enum class LayoutOrientation {
+ Horizontal,
+ Vertical,
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialBox.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialBox.kt
new file mode 100644
index 0000000..2223978
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialBox.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace
+
+import androidx.annotation.RestrictTo
+import androidx.compose.foundation.layout.LayoutScopeMarker
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.util.fastForEachIndexed
+import androidx.xr.compose.subspace.layout.Measurable
+import androidx.xr.compose.subspace.layout.MeasurePolicy
+import androidx.xr.compose.subspace.layout.MeasureResult
+import androidx.xr.compose.subspace.layout.MeasureScope
+import androidx.xr.compose.subspace.layout.ParentLayoutParamsAdjustable
+import androidx.xr.compose.subspace.layout.ParentLayoutParamsModifier
+import androidx.xr.compose.subspace.layout.Placeable
+import androidx.xr.compose.subspace.layout.SpatialAlignment
+import androidx.xr.compose.subspace.layout.SubspaceLayout
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.node.SubspaceModifierElement
+import androidx.xr.compose.unit.IntVolumeSize
+import androidx.xr.compose.unit.VolumeConstraints
+import androidx.xr.runtime.math.Pose
+import kotlin.math.max
+
+/**
+ * A layout composable that sizes itself to fit its content, subject to incoming constraints.
+ *
+ * A layout composable with [content]. The [SpatialBox] will size itself to fit the content, subject
+ * to the incoming constraints. When children are smaller than the parent, by default they will be
+ * positioned inside the [SpatialBox] according to the [alignment]. For individually specifying the
+ * alignments of the children layouts, use the [SpatialBoxScope.align] modifier. By default, the
+ * content will be measured without the [SpatialBox]'s incoming min constraints. If
+ * [propagateMinConstraints] is set to `true`, the min size set on the [SpatialBox] will also be
+ * applied to the content.
+ *
+ * Note: If the content has multiple children, they might overlap depending on their positioning.
+ *
+ * @param modifier The modifier to be applied to the layout.
+ * @param alignment The default alignment of children within the [SpatialBox].
+ * @param propagateMinConstraints Whether the incoming min constraints should be passed to content.
+ * @param name The name for the [SpatialBox].
+ * @param content The content of the [SpatialBox].
+ */
+@Composable
+@SubspaceComposable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SpatialBox(
+ modifier: SubspaceModifier = SubspaceModifier,
+ alignment: SpatialAlignment = SpatialAlignment.Center,
+ propagateMinConstraints: Boolean = false,
+ name: String = defaultSpatialBoxName(),
+ content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit,
+) {
+ SubspaceLayout(
+ modifier = modifier,
+ content = { SpatialBoxScopeInstance.content() },
+ measurePolicy = SpatialBoxMeasurePolicy(alignment, propagateMinConstraints),
+ name = name,
+ )
+}
+
+private var spatialBoxNamePart: Int = 0
+
+private fun defaultSpatialBoxName(): String {
+ return "SpatialBox-${spatialBoxNamePart++}"
+}
+
+/** [MeasurePolicy] for [SpatialBox]. */
+internal class SpatialBoxMeasurePolicy(
+ private val alignment: SpatialAlignment,
+ private val propagateMinConstraints: Boolean,
+) : MeasurePolicy {
+ override fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: VolumeConstraints,
+ ): MeasureResult {
+ if (measurables.isEmpty()) {
+ return layout(constraints.minWidth, constraints.minHeight, constraints.minDepth) {}
+ }
+
+ val contentConstraints =
+ if (propagateMinConstraints) {
+ constraints
+ } else {
+ constraints.copy(minWidth = 0, minHeight = 0, minDepth = 0)
+ }
+
+ val placeables = arrayOfNulls<Placeable>(measurables.size)
+ var boxWidth = constraints.minWidth
+ var boxHeight = constraints.minHeight
+ var boxDepth = constraints.minDepth
+ measurables.fastForEachIndexed { index, measurable ->
+ val placeable = measurable.measure(contentConstraints)
+ placeables[index] = placeable
+ boxWidth = max(boxWidth, placeable.measuredWidth)
+ boxHeight = max(boxHeight, placeable.measuredHeight)
+ boxDepth = max(boxDepth, placeable.measuredDepth)
+ }
+
+ return layout(boxWidth, boxHeight, boxDepth) {
+ val space = IntVolumeSize(boxWidth, boxHeight, boxDepth)
+ placeables.forEachIndexed { index, placeable ->
+ placeable as Placeable
+ val measurable = measurables[index]
+ val childSpatialAlignment =
+ SpatialBoxParentData(alignment).also { measurable.adjustParams(it) }.alignment
+ placeable.place(Pose(childSpatialAlignment.position(placeable.size(), space)))
+ }
+ }
+ }
+
+ private fun Placeable.size() = IntVolumeSize(measuredWidth, measuredHeight, measuredDepth)
+}
+
+/** Scope for the children of [SpatialBox]. */
+@LayoutScopeMarker
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface SpatialBoxScope {
+ /**
+ * Positions the content element at a specific [SpatialAlignment] within the [SpatialBox]. This
+ * alignment overrides the default [alignment] of the [SpatialBox].
+ *
+ * @param alignment The desired alignment for the content.
+ * @return The modified SubspaceModifier.
+ */
+ public fun SubspaceModifier.align(alignment: SpatialAlignment): SubspaceModifier
+}
+
+internal object SpatialBoxScopeInstance : SpatialBoxScope {
+ override fun SubspaceModifier.align(alignment: SpatialAlignment): SubspaceModifier {
+ return this then LayoutAlignElement(alignment = alignment)
+ }
+}
+
+private class LayoutAlignElement(val alignment: SpatialAlignment) :
+ SubspaceModifierElement<LayoutAlignNode>() {
+ override fun create(): LayoutAlignNode = LayoutAlignNode(alignment)
+
+ override fun update(node: LayoutAlignNode) {
+ node.alignment = alignment
+ }
+
+ override fun hashCode(): Int {
+ return alignment.hashCode()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ val otherModifier = other as? LayoutAlignElement ?: return false
+ return alignment == otherModifier.alignment
+ }
+}
+
+private class LayoutAlignNode(var alignment: SpatialAlignment) :
+ SubspaceModifier.Node(), ParentLayoutParamsModifier {
+ override fun adjustParams(params: ParentLayoutParamsAdjustable) {
+ if (params is SpatialBoxParentData) {
+ params.alignment = alignment
+ }
+ }
+}
+
+private data class SpatialBoxParentData(var alignment: SpatialAlignment) :
+ ParentLayoutParamsAdjustable
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialColumn.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialColumn.kt
new file mode 100644
index 0000000..b310f43
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialColumn.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace
+
+import androidx.annotation.FloatRange
+import androidx.annotation.RestrictTo
+import androidx.compose.foundation.layout.LayoutScopeMarker
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.Dp
+import androidx.xr.compose.subspace.layout.SpatialAlignment
+import androidx.xr.compose.subspace.layout.SubspaceLayout
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.runtime.math.Pose
+
+/**
+ * A layout composable that arranges its children in a vertical sequence.
+ *
+ * For arranging children horizontally, see [SpatialRow].
+ *
+ * @param modifier Modifiers to apply to the layout.
+ * @param alignment The default alignment for child elements within the column.
+ * @param name The name of the layout.
+ * @param content The composable content to be laid out vertically.
+ */
+@Composable
+@SubspaceComposable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SpatialColumn(
+ modifier: SubspaceModifier = SubspaceModifier,
+ alignment: SpatialAlignment = SpatialAlignment.Center,
+ name: String = defaultSpatialColumnName(),
+ content: @Composable @SubspaceComposable SpatialColumnScope.() -> Unit,
+) {
+ SubspaceLayout(
+ modifier = modifier,
+ content = { SpatialColumnScopeInstance.content() },
+ coreEntity =
+ rememberCoreContentlessEntity { createEntity(name = name, pose = Pose.Identity) },
+ name = name,
+ measurePolicy =
+ RowColumnMeasurePolicy(
+ orientation = LayoutOrientation.Vertical,
+ alignment = alignment,
+ curveRadius = Dp.Infinity,
+ ),
+ )
+}
+
+/** Scope for customizing the layout of children within a [SpatialColumn]. */
+@LayoutScopeMarker
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface SpatialColumnScope {
+ /**
+ * Sizes the element's height proportionally to its [weight] relative to other weighted sibling
+ * elements in the [SpatialColumn].
+ *
+ * The parent divides the remaining vertical space after measuring unweighted children and
+ * distributes it according to the weights.
+ *
+ * If [fill] is true, the element will occupy its entire allocated height. Otherwise, it can be
+ * smaller, potentially making the [SpatialColumn] smaller as unused space isn't redistributed.
+ *
+ * @param weight The proportional height for this element relative to other weighted siblings.
+ * Must be positive.
+ * @param fill Whether the element should fill its entire allocated height.
+ * @return The modified [SubspaceModifier].
+ */
+ public fun SubspaceModifier.weight(
+ @FloatRange(from = 0.0, fromInclusive = false) weight: Float,
+ fill: Boolean = true,
+ ): SubspaceModifier
+
+ /**
+ * Aligns the element within the [SpatialColumn] horizontally. This will override the horizontal
+ * alignment value passed to the [SpatialColumn].
+ *
+ * @param alignment The horizontal alignment to apply.
+ * @return The modified [SubspaceModifier].
+ */
+ public fun SubspaceModifier.align(alignment: SpatialAlignment.Horizontal): SubspaceModifier
+
+ /**
+ * Aligns the element within the [SpatialColumn] depthwise. This will override the depth
+ * alignment value passed to the [SpatialColumn].
+ *
+ * @param alignment The depth alignment to use for the element.
+ * @return The modified [SubspaceModifier].
+ */
+ public fun SubspaceModifier.align(alignment: SpatialAlignment.Depth): SubspaceModifier
+}
+
+internal object SpatialColumnScopeInstance : SpatialColumnScope {
+ override fun SubspaceModifier.weight(weight: Float, fill: Boolean): SubspaceModifier {
+ require(weight > 0.0) { "invalid weight $weight; must be greater than zero" }
+ return this then
+ LayoutWeightElement(
+ // Coerce Float.POSITIVE_INFINITY to Float.MAX_VALUE to avoid errors
+ weight = weight.coerceAtMost(Float.MAX_VALUE),
+ fill = fill,
+ )
+ }
+
+ override fun SubspaceModifier.align(alignment: SpatialAlignment.Horizontal): SubspaceModifier {
+ return this then RowColumnAlignElement(horizontalSpatialAlignment = alignment)
+ }
+
+ override fun SubspaceModifier.align(alignment: SpatialAlignment.Depth): SubspaceModifier {
+ return this then RowColumnAlignElement(depthSpatialAlignment = alignment)
+ }
+}
+
+private var spatialColumnNamePart: Int = 0
+
+private fun defaultSpatialColumnName(): String {
+ return "SpatialColumn-${spatialColumnNamePart++}"
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialLayoutSpacer.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialLayoutSpacer.kt
new file mode 100644
index 0000000..73f901c
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialLayoutSpacer.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace
+
+import androidx.annotation.RestrictTo
+import androidx.compose.runtime.Composable
+import androidx.xr.compose.subspace.layout.Measurable
+import androidx.xr.compose.subspace.layout.MeasurePolicy
+import androidx.xr.compose.subspace.layout.MeasureResult
+import androidx.xr.compose.subspace.layout.MeasureScope
+import androidx.xr.compose.subspace.layout.SubspaceLayout
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.layout.height
+import androidx.xr.compose.subspace.layout.width
+import androidx.xr.compose.unit.VolumeConstraints
+
+/**
+ * A composable that represents an empty space layout. Its size can be controlled using modifiers
+ * like [SubspaceModifier.width], [SubspaceModifier.height], etc.
+ *
+ * @param modifier Modifiers to apply to the spacer.
+ */
+@Composable
+@SubspaceComposable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SpatialLayoutSpacer(modifier: SubspaceModifier = SubspaceModifier) {
+ SubspaceLayout(
+ name = defaultSpatialLayoutSpacerName(),
+ modifier = modifier,
+ measurePolicy = SpacerMeasurePolicy,
+ )
+}
+
+/**
+ * A composable that represents an empty space layout. Its size can be controlled using modifiers
+ * like [SubspaceModifier.width], [SubspaceModifier.height], etc.
+ *
+ * @param modifier Modifiers to apply to this spacer.
+ * @param name The name of this SpatialLayoutSpacer element.
+ */
+@Composable
+@SubspaceComposable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SpatialLayoutSpacer(
+ modifier: SubspaceModifier = SubspaceModifier,
+ name: String = defaultSpatialLayoutSpacerName(),
+) {
+ SubspaceLayout(name = name, modifier = modifier, measurePolicy = SpacerMeasurePolicy)
+}
+
+private object SpacerMeasurePolicy : MeasurePolicy {
+ override fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: VolumeConstraints,
+ ): MeasureResult {
+ return with(constraints) {
+ val width = if (hasBoundedWidth) maxWidth else 0
+ val height = if (hasBoundedHeight) maxHeight else 0
+ val depth = if (hasBoundedDepth) maxDepth else 0
+ layout(width, height, depth) {}
+ }
+ }
+}
+
+private var spatialLayoutSpacerNamePart: Int = 0
+
+private fun defaultSpatialLayoutSpacerName(): String {
+ return "Spacer-${spatialLayoutSpacerNamePart++}"
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialPanel.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialPanel.kt
new file mode 100644
index 0000000..298715c
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialPanel.kt
@@ -0,0 +1,305 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace
+
+import android.content.Intent
+import android.graphics.Color
+import android.graphics.Rect
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.annotation.RestrictTo
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.UiComposable
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
+import androidx.xr.compose.platform.LocalDialogManager
+import androidx.xr.compose.platform.LocalPanelEntity
+import androidx.xr.compose.platform.LocalSession
+import androidx.xr.compose.subspace.layout.CorePanelEntity
+import androidx.xr.compose.subspace.layout.SpatialRoundedCornerShape
+import androidx.xr.compose.subspace.layout.SpatialShape
+import androidx.xr.compose.subspace.layout.SubspaceLayout
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.unit.IntVolumeSize
+import androidx.xr.runtime.math.Pose
+import androidx.xr.scenecore.Dimensions
+import androidx.xr.scenecore.PanelEntity
+
+private const val DEFAULT_SIZE_PX = 400
+
+/** Contains default values used by spatial panels. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public object SpatialPanelDefaults {
+
+ /** Default shape for a Spatial Panel. */
+ public val shape: SpatialShape = SpatialRoundedCornerShape(CornerSize(32.dp))
+}
+
+/**
+ * Creates a [SpatialPanel] representing a 2D plane in 3D space in which an application can fill
+ * content.
+ *
+ * @param view Content view to be displayed within the SpatialPanel.
+ * @param modifier SubspaceModifiers to apply to the SpatialPanel.
+ * @param name A name for the SpatialPanel, useful for debugging.
+ * @param shape The shape of this Spatial Panel.
+ */
+@Composable
+@SubspaceComposable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SpatialPanel(
+ view: View,
+ modifier: SubspaceModifier = SubspaceModifier,
+ name: String = defaultSpatialPanelName(),
+ shape: SpatialShape = SpatialPanelDefaults.shape,
+) {
+ SpatialPanel(modifier, name, view, shape) {}
+}
+
+/**
+ * Creates a [SpatialPanel] representing a 2D plane in 3D space in which an application can fill
+ * content.
+ *
+ * @param modifier SubspaceModifiers to apply to the SpatialPanel.
+ * @param name A name for the SpatialPanel, useful for debugging.
+ * @param shape The shape of this Spatial Panel.
+ * @param content The composable content to render within the SpatialPanel.
+ */
+@Composable
+@SubspaceComposable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SpatialPanel(
+ modifier: SubspaceModifier = SubspaceModifier,
+ name: String = defaultSpatialPanelName(),
+ shape: SpatialShape = SpatialPanelDefaults.shape,
+ content: @Composable @UiComposable () -> Unit,
+) {
+ var panelEntity by remember { mutableStateOf<PanelEntity?>(null) }
+
+ SpatialPanel(
+ modifier = modifier,
+ name = name,
+ view =
+ rememberComposeView {
+ CompositionLocalProvider(LocalPanelEntity provides panelEntity, content = content)
+ },
+ shape = shape,
+ onPanelEntityCreated = { panelEntity = it },
+ )
+}
+
+/**
+ * Creates a [SpatialPanel] backed by the main Window content.
+ *
+ * This panel requires the following specific configuration in the Android Manifest for proper
+ * sizing/resizing behavior:
+ * ```
+ * <activity
+ * android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize>
+ * <!--suppress AndroidElementNotAllowed -->
+ * <layout android:defaultWidth="50dp" android:defaultHeight="50dp" android:minHeight="50dp"
+ * android:minWidth="50dp"/>
+ * </activity>
+ * ```
+ *
+ * @param modifier SubspaceModifier to apply to the MainPanel.
+ * @param shape The shape of this Spatial Panel.
+ */
+@Composable
+@SubspaceComposable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun MainPanel(
+ modifier: SubspaceModifier = SubspaceModifier,
+ shape: SpatialShape = SpatialPanelDefaults.shape,
+) {
+ val session = checkNotNull(LocalSession.current) { "session must be initialized" }
+
+ DisposableEffect(Unit) {
+ session.mainPanelEntity.setHidden(false)
+ onDispose { session.mainPanelEntity.setHidden(true) }
+ }
+
+ LayoutPanelEntity(
+ // Do not use rememberCorePanelEntity since we do not want to dispose the main panel entity.
+ remember { CorePanelEntity(session, session.mainPanelEntity) },
+ "MainPanel",
+ shape,
+ modifier,
+ )
+}
+
+/**
+ * Creates a [SpatialPanel] and launches an Activity within it.
+ *
+ * @param intent The intent of an Activity to launch within this panel.
+ * @param modifier SubspaceModifiers to apply to the SpatialPanel.
+ * @param name A name for the SpatialPanel, useful for debugging.
+ * @param shape The shape of this Spatial Panel.
+ */
+@Composable
+@SubspaceComposable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SpatialPanel(
+ intent: Intent,
+ modifier: SubspaceModifier = SubspaceModifier,
+ name: String = "ActivityPanel-${intent.action}",
+ shape: SpatialShape = SpatialPanelDefaults.shape,
+) {
+ LayoutPanelEntity(
+ rememberCorePanelEntity {
+ val rect = Rect(0, 0, DEFAULT_SIZE_PX, DEFAULT_SIZE_PX)
+ createActivityPanelEntity(rect, name).also { it.launchActivity(intent) }
+ },
+ name,
+ shape,
+ modifier,
+ )
+}
+
+/**
+ * Private [SpatialPanel] implementation that reports its created PanelEntity.
+ *
+ * @param modifier SubspaceModifiers.
+ * @param view content view to render inside the SpatialPanel
+ * @param shape The shape of this Spatial Panel.
+ * @param onPanelEntityCreated callback to consume the [PanelEntity] when it is created
+ */
+@Composable
+@SubspaceComposable
+private fun SpatialPanel(
+ modifier: SubspaceModifier = SubspaceModifier,
+ name: String,
+ view: View,
+ shape: SpatialShape,
+ onPanelEntityCreated: (PanelEntity) -> Unit,
+) {
+ val minimumPanelDimension = Dimensions(10f, 10f, 10f)
+
+ val frameLayout = remember {
+ FrameLayout(view.context).also {
+ if (view.parent != it) {
+ val parent = view.parent as? ViewGroup
+ parent?.removeView(view)
+ it.addView(view)
+ }
+ }
+ }
+
+ val scrim = remember { View(view.context) }
+ val dialogManager = LocalDialogManager.current
+ LaunchedEffect(dialogManager.isSpatialDialogActive.value) {
+ if (dialogManager.isSpatialDialogActive.value) {
+ scrim.setBackgroundColor(Color.argb(90, 0, 0, 0))
+ val scrimLayoutParams =
+ FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ )
+
+ if (scrim.parent == null) {
+ frameLayout.addView(scrim, scrimLayoutParams)
+ }
+
+ scrim.setOnClickListener { dialogManager.isSpatialDialogActive.value = false }
+ } else {
+ frameLayout.removeView(scrim)
+ }
+ }
+
+ LayoutPanelEntity(
+ rememberCorePanelEntity {
+ createPanelEntity(
+ view = frameLayout,
+ surfaceDimensionsPx = minimumPanelDimension,
+ dimensions = minimumPanelDimension,
+ name = name,
+ pose = Pose.Identity,
+ )
+ .also(onPanelEntityCreated)
+ },
+ name,
+ shape,
+ modifier,
+ )
+}
+
+/**
+ * Lay out the SpatialPanel using the provided [CorePanelEntity].
+ *
+ * @param coreEntity The [CorePanelEntity] associated with this SpatialPanel. It should be based on
+ * a SceneCore panel entity.
+ * @param name The name of the panel for debugging purposes.
+ * @param shape The shape of this Spatial Panel.
+ * @param modifier The [SubspaceModifier] attached to this compose node.
+ */
+@Composable
+private fun LayoutPanelEntity(
+ coreEntity: CorePanelEntity,
+ name: String,
+ shape: SpatialShape,
+ modifier: SubspaceModifier,
+) {
+ val density = LocalDensity.current
+ SubspaceLayout(modifier = modifier, coreEntity = coreEntity, name = name) {
+ measurables,
+ constraints ->
+ val initialWidth = DEFAULT_SIZE_PX.coerceIn(constraints.minWidth, constraints.maxWidth)
+ val initialHeight = DEFAULT_SIZE_PX.coerceIn(constraints.minHeight, constraints.maxHeight)
+ val initialDepth = DEFAULT_SIZE_PX.coerceIn(constraints.minDepth, constraints.maxDepth)
+
+ val placeables = measurables.map { it.measure(constraints) }
+
+ val maxSize =
+ placeables.fold(IntVolumeSize(initialWidth, initialHeight, initialDepth)) {
+ currentMax,
+ placeable ->
+ IntVolumeSize(
+ width = maxOf(currentMax.width, placeable.measuredWidth),
+ height = maxOf(currentMax.height, placeable.measuredHeight),
+ depth = maxOf(currentMax.depth, placeable.measuredDepth),
+ )
+ }
+
+ val maxWidth = maxSize.width
+ val maxHeight = maxSize.height
+
+ if (shape is SpatialRoundedCornerShape) {
+ coreEntity.setCornerRadius(
+ shape.computeCornerRadius(maxWidth.toFloat(), maxHeight.toFloat(), density),
+ density,
+ )
+ }
+
+ // Reserve space in the original composition
+ layout(maxWidth, maxHeight, maxSize.depth) { placeables.forEach { it.place(Pose()) } }
+ }
+}
+
+private var spatialPanelNamePart: Int = 0
+
+private fun defaultSpatialPanelName(): String {
+ return "Panel-${spatialPanelNamePart++}"
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialRow.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialRow.kt
new file mode 100644
index 0000000..66af29e
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialRow.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace
+
+import androidx.annotation.FloatRange
+import androidx.annotation.RestrictTo
+import androidx.compose.foundation.layout.LayoutScopeMarker
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.xr.compose.subspace.layout.SpatialAlignment
+import androidx.xr.compose.subspace.layout.SubspaceLayout
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.runtime.math.Pose
+
+/**
+ * A layout composable that arranges its children in a horizontal sequence. For arranging children
+ * vertically, see [SpatialColumn].
+ *
+ * @param modifier Appearance modifiers to apply to this Composable.
+ * @param alignment The default alignment for child elements within the row.
+ * @param curveRadius The radial distance (in Dp) of the polar coordinate system of this row. It is
+ * a positive value. Setting this value to Dp.Infinity or a non-positive value will flatten the
+ * row. When a row is curved, its elements will be oriented so that they lie tangent to the curved
+ * row. A typical curved row has a curve radius of 825.dp.
+ * @param name A string name to associated with the SpatialRow. This can be useful identifying the
+ * SpatialRow when debugging spatial applications.
+ * @param content The composable content to be laid out horizontally in the row.
+ */
+@Composable
+@SubspaceComposable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SpatialRow(
+ modifier: SubspaceModifier = SubspaceModifier,
+ alignment: SpatialAlignment = SpatialAlignment.Center,
+ curveRadius: Dp = Dp.Infinity,
+ name: String = defaultSpatialRowName(),
+ content: @Composable @SubspaceComposable SpatialRowScope.() -> Unit,
+) {
+ SubspaceLayout(
+ modifier = modifier,
+ content = { SpatialRowScopeInstance.content() },
+ coreEntity =
+ rememberCoreContentlessEntity { createEntity(name = name, pose = Pose.Identity) },
+ name = name,
+ measurePolicy =
+ RowColumnMeasurePolicy(
+ orientation = LayoutOrientation.Horizontal,
+ alignment = alignment,
+ curveRadius = if (curveRadius > 0.dp) curveRadius else Dp.Infinity,
+ ),
+ )
+}
+
+/** Scope for customizing the layout of children within a [SpatialRow]. */
+@LayoutScopeMarker
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface SpatialRowScope {
+ /**
+ * Sizes the element's width proportionally to its [weight] relative to other weighted sibling
+ * elements in the [SpatialRow].
+ *
+ * The parent divides the remaining horizontal space after measuring unweighted children and
+ * distributes it according to the weights.
+ *
+ * If [fill] is true, the element will occupy its entire allocated width. Otherwise, it can be
+ * smaller, potentially making the [SpatialRow] smaller as unused space isn't redistributed.
+ *
+ * @param weight The proportional width for this element relative to other weighted siblings.
+ * Must be positive.
+ * @param fill Whether the element should fill its entire allocated width.
+ * @return The modified [SubspaceModifier].
+ */
+ public fun SubspaceModifier.weight(
+ @FloatRange(from = 0.0, fromInclusive = false) weight: Float,
+ fill: Boolean = true,
+ ): SubspaceModifier
+
+ /**
+ * Aligns the element vertically within the [SpatialRow], overriding the row's default vertical
+ * alignment.
+ *
+ * @param alignment The vertical alignment to apply.
+ * @return The modified [SubspaceModifier].
+ */
+ public fun SubspaceModifier.align(alignment: SpatialAlignment.Vertical): SubspaceModifier
+
+ /**
+ * Aligns the element depthwise within the [SpatialRow], overriding the row's default depth
+ * alignment.
+ *
+ * @param alignment The depth alignment to apply.
+ * @return The modified [SubspaceModifier].
+ */
+ public fun SubspaceModifier.align(alignment: SpatialAlignment.Depth): SubspaceModifier
+}
+
+internal object SpatialRowScopeInstance : SpatialRowScope {
+ override fun SubspaceModifier.weight(weight: Float, fill: Boolean): SubspaceModifier {
+ require(weight > 0.0) { "invalid weight $weight; must be greater than zero" }
+ return this then
+ LayoutWeightElement(
+ // Coerce Float.POSITIVE_INFINITY to Float.MAX_VALUE to avoid errors
+ weight = weight.coerceAtMost(Float.MAX_VALUE),
+ fill = fill,
+ )
+ }
+
+ override fun SubspaceModifier.align(alignment: SpatialAlignment.Vertical): SubspaceModifier {
+ return this then RowColumnAlignElement(verticalSpatialAlignment = alignment)
+ }
+
+ override fun SubspaceModifier.align(alignment: SpatialAlignment.Depth): SubspaceModifier {
+ return this then RowColumnAlignElement(depthSpatialAlignment = alignment)
+ }
+}
+
+private var spatialRowNamePart: Int = 0
+
+private fun defaultSpatialRowName(): String {
+ return "SpatialRow-${spatialRowNamePart++}"
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SubspaceComposable.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SubspaceComposable.kt
new file mode 100644
index 0000000..231e96d
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SubspaceComposable.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace
+
+import androidx.annotation.RestrictTo
+import androidx.compose.runtime.ComposableTargetMarker
+
+/**
+ * Marks a composable function or other code element as intended for use within the context of
+ * [SubspaceComposable] functions.
+ */
+@Retention(AnnotationRetention.BINARY)
+@ComposableTargetMarker(description = "Subspace Composable")
+@Target(
+ AnnotationTarget.FILE,
+ AnnotationTarget.FUNCTION,
+ AnnotationTarget.PROPERTY_GETTER,
+ AnnotationTarget.TYPE,
+ AnnotationTarget.TYPE_PARAMETER,
+)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public annotation class SubspaceComposable
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/Volume.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/Volume.kt
new file mode 100644
index 0000000..92e5aae
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/Volume.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace
+
+import androidx.annotation.RestrictTo
+import androidx.compose.runtime.Composable
+import androidx.xr.compose.subspace.layout.SubspaceLayout
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.unit.IntVolumeSize
+import androidx.xr.runtime.math.Pose
+import androidx.xr.scenecore.Entity
+
+/**
+ * A composable that represents a 3D volume of space within which an application can fill content.
+ *
+ * This composable provides a [Entity] through the [onVolumeEntity] lambda, allowing the caller to
+ * attach child Jetpack XR Entities to it.
+ *
+ * @param modifier SubspaceModifiers to apply to the Volume.
+ * @param name A name associated with this Volume entity, useful for debugging.
+ * @param onVolumeEntity A lambda function that will be invoked when the [Entity] becomes available.
+ */
+@Composable
+@SubspaceComposable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun Volume(
+ modifier: SubspaceModifier = SubspaceModifier,
+ name: String = defaultVolumeName(),
+ onVolumeEntity: (Entity) -> Unit,
+) {
+ val defaultWidthPx = 400
+ val defaultHeightPx = 400
+ val defaultDepthPx = 400
+
+ SubspaceLayout(
+ modifier = modifier,
+ coreEntity =
+ rememberCoreContentlessEntity {
+ createEntity(name = name, pose = Pose.Identity).apply(onVolumeEntity)
+ },
+ name = name,
+ ) { measurables, constraints ->
+ val initialWidth = defaultWidthPx.coerceIn(constraints.minWidth, constraints.maxWidth)
+ val initialHeight = defaultHeightPx.coerceIn(constraints.minHeight, constraints.maxHeight)
+ val initialDepth = defaultDepthPx.coerceIn(constraints.minDepth, constraints.maxDepth)
+
+ val placeables = measurables.map { it.measure(constraints) }
+
+ val maxSize =
+ placeables.fold(IntVolumeSize(initialWidth, initialHeight, initialDepth)) {
+ currentMax,
+ placeable ->
+ IntVolumeSize(
+ width = maxOf(currentMax.width, placeable.measuredWidth),
+ height = maxOf(currentMax.height, placeable.measuredHeight),
+ depth = maxOf(currentMax.depth, placeable.measuredDepth),
+ )
+ }
+
+ // Reserve space in the original composition
+ layout(maxSize.width, maxSize.height, maxSize.depth) {
+ placeables.forEach { it.place(Pose()) }
+ }
+ }
+}
+
+private var volumeNamePart: Int = 0
+
+private fun defaultVolumeName(): String {
+ return "Volume-${volumeNamePart++}"
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Alpha.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Alpha.kt
new file mode 100644
index 0000000..a070d0e
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Alpha.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.FloatRange
+import androidx.annotation.RestrictTo
+import androidx.xr.compose.subspace.node.SubspaceModifierElement
+
+/**
+ * Sets the opacity of this element (and its children) to a value between [0..1]. An alpha value of
+ * 0.0f means fully transparent while a value of 1.0f is completely opaque. Elements with
+ * semi-transparent alpha values (> 0.0 but < 1.0f) will be rendered using alpha-blending.
+ *
+ * @param alpha - Opacity of this element (and its children). Must be between `0` and `1`,
+ * inclusive. Values < `0` or > `1` will be clamped.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.alpha(
+ @FloatRange(from = 0.0, to = 1.0) alpha: Float
+): SubspaceModifier = this.then(AlphaElement(alpha))
+
+private class AlphaElement(private var alpha: Float) : SubspaceModifierElement<AlphaNode>() {
+ init {
+ alpha = alpha.coerceIn(0.0f, 1.0f)
+ }
+
+ override fun create(): AlphaNode = AlphaNode(alpha)
+
+ override fun update(node: AlphaNode) {
+ node.alpha = alpha
+ }
+
+ override fun hashCode(): Int {
+ return alpha.hashCode()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is AlphaElement) return false
+
+ return alpha == other.alpha
+ }
+}
+
+private class AlphaNode(public var alpha: Float) : SubspaceModifier.Node(), CoreEntityNode {
+ override fun modifyCoreEntity(coreEntity: CoreEntity) {
+ coreEntity.setAlpha(alpha)
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/CoreEntity.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/CoreEntity.kt
new file mode 100644
index 0000000..34e33d00
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/CoreEntity.kt
@@ -0,0 +1,569 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import android.content.res.Resources
+import android.util.Log
+import androidx.compose.ui.unit.Density
+import androidx.xr.compose.subspace.node.SubspaceLayoutModifierNode
+import androidx.xr.compose.subspace.node.SubspaceLayoutNode
+import androidx.xr.compose.subspace.node.coordinator
+import androidx.xr.compose.unit.IntVolumeSize
+import androidx.xr.compose.unit.Meter
+import androidx.xr.compose.unit.toDimensionsInMeters
+import androidx.xr.compose.unit.toIntVolumeSize
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Ray
+import androidx.xr.scenecore.BasePanelEntity
+import androidx.xr.scenecore.ContentlessEntity
+import androidx.xr.scenecore.Dimensions
+import androidx.xr.scenecore.Entity
+import androidx.xr.scenecore.MovableComponent
+import androidx.xr.scenecore.MoveListener
+import androidx.xr.scenecore.PixelDimensions
+import androidx.xr.scenecore.ResizableComponent
+import androidx.xr.scenecore.ResizeListener
+import androidx.xr.scenecore.Session
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+
+/**
+ * Wrapper class for Entities from SceneCore to provide convenience methods for working with
+ * Entities from SceneCore.
+ */
+internal sealed class CoreEntity(public val entity: Entity) : SubspaceLayoutCoordinates {
+
+ protected var movable: Movable? = null
+ protected var resizable: Resizable? = null
+
+ internal var layout: SubspaceLayoutNode.MeasurableLayout? = null
+ set(value) {
+ field = value
+ applyLayoutChanges()
+ }
+
+ internal fun applyLayoutChanges() {
+ // Compose XR uses pixels, SceneCore uses meters.
+ val corePose =
+ (layout?.poseInParentEntity ?: Pose.Identity).convertPixelsToMeters(DEFAULT_DENSITY)
+ if (entity.getPose() != corePose) {
+ entity.setPose(corePose)
+ }
+ }
+
+ override val pose: Pose
+ get() = movable?.userPose ?: Pose.Identity
+
+ override val poseInRoot: Pose
+ get() = pose.translate(sourcePoseInRoot.translation).rotate(sourcePoseInRoot.rotation)
+
+ private val sourcePoseInRoot: Pose
+ get() = coordinatesInRoot?.poseInRoot ?: Pose.Identity
+
+ private val coordinatesInRoot: SubspaceLayoutCoordinates?
+ get() =
+ layout
+ ?.tail
+ ?.traverseSelfThenAncestors()
+ ?.findInstance<SubspaceLayoutModifierNode>()
+ ?.coordinator ?: layout?.parentCoordinatesInRoot
+
+ override val poseInParentEntity: Pose
+ get() =
+ pose
+ .translate(sourcePoseInParentEntity.translation)
+ .rotate(sourcePoseInParentEntity.rotation)
+
+ private val sourcePoseInParentEntity: Pose
+ get() = coordinatesInParentEntity?.poseInParentEntity ?: Pose.Identity
+
+ private val coordinatesInParentEntity: SubspaceLayoutCoordinates?
+ get() =
+ layout
+ ?.tail
+ ?.traverseSelfThenAncestors()
+ ?.findInstance<SubspaceLayoutModifierNode>()
+ ?.coordinator ?: layout?.parentCoordinatesInParentEntity
+
+ override var size: IntVolumeSize = IntVolumeSize.Zero
+ set(value) {
+ val proposedSize = resizable?.userSize ?: value
+ if (field == proposedSize) {
+ return
+ }
+
+ setEntitySize(proposedSize)
+ field = proposedSize
+
+ movable?.setComponentSize(proposedSize)
+ resizable?.setComponentSize(proposedSize)
+ }
+
+ protected open fun setEntitySize(size: IntVolumeSize) {
+ entity.setSize(
+ Dimensions(size.width.toFloat(), size.height.toFloat(), size.depth.toFloat())
+ )
+ }
+
+ public var parent: CoreEntity? = null
+ set(value) {
+ field = value
+
+ // Leave SceneCore's parent as-is if we're trying to clear it out. SceneCore
+ // parents all
+ // newly-created non-Anchor entities under a world space point of reference for the
+ // activity
+ // space, but we don't have access to it. To maintain this parent-is-not-null property,
+ // we use
+ // this hack to keep the original parent, even if it's not technically correct when
+ // we're
+ // trying to reparent a node. The correct parent will be set on the "set" part of the
+ // reparent.
+ //
+ // TODO(b/356952297): Remove this hack once we can save and restore the original parent.
+ if (value == null) return
+
+ entity.setParent(value.entity)
+ }
+
+ internal fun applyModifiers(nodes: Sequence<SubspaceModifier.Node>) {
+ movable?.applyModifiers(nodes)
+ resizable?.applyModifiers(nodes)
+
+ // Apply any CoreEntityNode modifiers to [entity].
+ for (node in nodes.filterIsInstance<CoreEntityNode>()) {
+ node.modifyCoreEntity(this)
+ }
+ }
+
+ /**
+ * The scale of this entity relative to its parent. This value will affect the rendering of this
+ * Entity's children. As the scale increases, this will uniformly stretch the content of the
+ * Entity. This does not affect layout and other content will be laid out according to the
+ * original scale of the entity.
+ */
+ public var scale: Float
+ get() = entity.getScale()
+ set(value) = entity.setScale(value)
+
+ /**
+ * Sets the opacity of this entity (and its children) to a value between [0..1]. An alpha value
+ * of 0.0f means fully transparent while the value of 1.0f means fully opaque.
+ *
+ * @param alpha The opacity of this entity.
+ */
+ public fun setAlpha(alpha: Float) {
+ entity.setAlpha(alpha)
+ }
+
+ public companion object {
+ protected val LocalExecutor: Executor = Executors.newSingleThreadExecutor()
+
+ // TODO(djeu): Figure out if there's a better way here where there is no context.
+ private val DEFAULT_DENSITY: Density =
+ Density(
+ density = Resources.getSystem().displayMetrics.density,
+ fontScale = Resources.getSystem().configuration.fontScale,
+ )
+ }
+
+ protected inner class Movable(private val session: Session) {
+ /**
+ * The node should be movable.
+ *
+ * Right now this only affects the initial attachment of the component to the Core entity;
+ * however, this may change to be more reactive.
+ *
+ * TODO(b/358678496): Revisit this now that Movable cannot by disabled, except via the
+ * 'enabled' bit in the Node.
+ */
+ public var isEnabled: Boolean = true
+
+ /** Pose based on user adjustments from MoveEvents from SceneCore. */
+ public var userPose: Pose? = null
+ set(value) {
+ field = value
+ applyLayoutChanges()
+ }
+
+ private var initialOffset: Pose = Pose.Identity
+
+ /**
+ * Update the current state based on the modifiers applied to this [CoreEntity]. If a
+ * [MovableNode] is present then attach the component and apply its properties. Otherwise,
+ * detach the component. The last matching modifier is used, and earlier modifiers are
+ * ignored.
+ */
+ public fun applyModifiers(nodes: Sequence<SubspaceModifier.Node>) {
+ updateState(nodes.filterIsInstance<MovableNode>().lastOrNull())
+ }
+
+ /** Sets the size of the SysUI movable affordance. */
+ public fun setComponentSize(size: IntVolumeSize) {
+ if (isAttached) {
+ component.size = size.toDimensionsInMeters(DEFAULT_DENSITY)
+ }
+ }
+
+ /** All Compose XR params for the Movable modifier for this CoreEntity. */
+ private var movableNode: MovableNode? = null
+
+ /** Whether the movableComponent is attached to the entity. */
+ private var isAttached: Boolean = false
+
+ private val component: MovableComponent by lazy {
+ // Here we create the component and pass in false to systemMovable since Compose is
+ // going to
+ // handle the move events.
+ session.createMovableComponent(systemMovable = false)
+ }
+
+ /**
+ * Updates the movable state of this CoreEntity. Only update movable state if [Movable] is
+ * enabled.
+ *
+ * @param node The Movable modifier for this CoreEntity.
+ */
+ private fun updateState(node: MovableNode?) {
+ if (!isEnabled) {
+ if (node != null && node.enabled) logEnabledCheck()
+ return
+ }
+
+ movableNode = node
+ if (node != null && node.enabled) {
+ enableComponent()
+ } else {
+ disableComponent()
+ }
+ }
+
+ /** Enables the MovableComponent for this CoreEntity. */
+ private fun enableComponent() {
+ if (!isAttached) {
+ check(entity.addComponent(component)) {
+ "Could not add MovableComponent to Core Entity"
+ }
+ component.addMoveListener(
+ LocalExecutor,
+ object : MoveListener {
+ override fun onMoveStart(
+ entity: Entity,
+ initialInputRay: Ray,
+ initialPose: Pose,
+ initialScale: Float,
+ initialParent: Entity,
+ ) {
+ // updatePoseOnMove() not called because initialPose should be the same
+ // as the current
+ // pose.
+ initialOffset = sourcePoseInParentEntity
+ }
+
+ override fun onMoveUpdate(
+ entity: Entity,
+ currentInputRay: Ray,
+ currentPose: Pose,
+ currentScale: Float,
+ ) {
+ updatePoseOnMove(currentPose)
+ }
+
+ override fun onMoveEnd(
+ entity: Entity,
+ finalInputRay: Ray,
+ finalPose: Pose,
+ finalScale: Float,
+ updatedParent: Entity?,
+ ) {
+ updatePoseOnMove(finalPose)
+ initialOffset = Pose.Identity
+ }
+ },
+ )
+ // Ensure size is correct, since we do not update the size
+ // when the component is detached.
+ setComponentSize(size)
+ isAttached = true
+ }
+
+ // If the MovableComponent gets more internal state, copy it over
+ // from the modifier node here.
+ }
+
+ /**
+ * Disables the MovableComponent for this CoreEntity. Takes care of life cycle tasks for the
+ * underlying component in SceneCore.
+ */
+ private fun disableComponent() {
+ if (isAttached) {
+ entity.removeComponent(component)
+ isAttached = false
+ if (movableNode?.stickyPose != true) {
+ userPose = null
+ }
+ }
+ }
+
+ /** Called every time there is a MoveEvent in SceneCore, if this CoreEntity is movable. */
+ private fun updatePoseOnMove(pose: Pose) {
+ if (movableNode?.enabled == false) {
+ return
+ }
+ val node = movableNode ?: return
+
+ // SceneCore uses meters, Compose XR uses pixels.
+ val corePose = pose.convertMetersToPixels(DEFAULT_DENSITY)
+
+ // Find the delta from when the move event started.
+ val coreDeltaPose =
+ Pose(
+ corePose.translation - initialOffset.translation,
+ initialOffset.rotation.inverse * corePose.rotation,
+ )
+ if (node.onPoseChange(corePose)) {
+ // We're done, the user app will handle the event.
+ return
+ }
+ userPose = coreDeltaPose
+ }
+
+ /** Flag to enforce single logging of Entity Component update error. */
+ private var shouldLogEnabledCheck: Boolean = true
+
+ /** Log enabled check error if first time occurring. */
+ private fun logEnabledCheck() {
+ if (shouldLogEnabledCheck) {
+ Log.i(
+ "CoreEntity",
+ "Not attempting to update Components, functionality is not enabled."
+ )
+ shouldLogEnabledCheck = false
+ }
+ }
+ }
+
+ protected inner class Resizable(private val session: Session) {
+ /**
+ * The node should be resizable.
+ *
+ * Right now this only affects the initial attachment of the component to the Core entity;
+ * however, this may change to be more reactive.
+ *
+ * TODO(b/358678496): Revisit this now that Resizable cannot by disabled, except via the
+ * 'enabled' bit in the Node.
+ */
+ public var isEnabled: Boolean = true
+
+ /** Size based on user adjustments from ResizeEvents from SceneCore. */
+ public var userSize: IntVolumeSize? = null
+ private set(value) {
+ field = value
+ if (value != null) {
+ // The user size takes priority. Set the current size to the user provided size.
+ size = value
+ }
+ }
+
+ /**
+ * Update the current state based on the modifiers applied to this [CoreEntity]. If a
+ * [ResizableNode] is present then attach the component and apply its properties. Otherwise,
+ * detach the component. The last matching modifier is used, and earlier modifiers are
+ * ignored.
+ */
+ public fun applyModifiers(nodes: Sequence<SubspaceModifier.Node>) {
+ updateState(nodes.filterIsInstance<ResizableNode>().lastOrNull())
+ }
+
+ /** Sets the size of the SysUI resizable affordance. */
+ public fun setComponentSize(size: IntVolumeSize) {
+ if (isAttached) {
+ component.size = size.toDimensionsInMeters(DEFAULT_DENSITY)
+ }
+ }
+
+ /** All Compose XR params for the Resizable modifier for this CoreEntity. */
+ private var resizableNode: ResizableNode? = null
+
+ /** Whether the resizableComponent is attached to the entity. */
+ private var isAttached: Boolean = false
+
+ private val component: ResizableComponent by lazy {
+ session.createResizableComponent().apply {
+ addResizeListener(
+ LocalExecutor,
+ object : ResizeListener {
+ override fun onResizeStart(entity: Entity, originalSize: Dimensions) {
+ resizeListener(originalSize)
+ }
+
+ // Compose does not need to handle the onResizeUpdate event since Core is
+ // handling the
+ // UI affordance change and adding the update would make it so we update the
+ // size twice.
+ override fun onResizeEnd(entity: Entity, finalSize: Dimensions) {
+ resizeListener(finalSize)
+ }
+ },
+ )
+ }
+ }
+
+ /**
+ * Updates the resizable state of this CoreEntity. Only update resizable state if
+ * [Resizable] is enabled.
+ *
+ * @param node The Resizable modifier for this CoreEntity.
+ */
+ private fun updateState(node: ResizableNode?) {
+ if (!isEnabled) {
+ if (node != null && node.enabled) logEnabledCheck()
+ return
+ }
+
+ resizableNode = node
+ if (node != null && node.enabled) {
+ enableAndUpdateComponent(node)
+ } else {
+ disableComponent()
+ }
+ }
+
+ /** Flag to enforce single logging of Entity Component update error. */
+ private var shouldLogEnabledCheck: Boolean = true
+
+ /** Log enabled check error if first time occurring. */
+ private fun logEnabledCheck() {
+ if (shouldLogEnabledCheck) {
+ Log.i(
+ "CoreEntity",
+ "Not attempting to update Components, entity type is not interactive."
+ )
+ shouldLogEnabledCheck = false
+ }
+ }
+
+ /**
+ * Enables the ResizableComponent for this CoreEntity. Also, updates the component using
+ * [node]'s values.
+ *
+ * @param node The Resizable modifier for this CoreEntity.
+ */
+ private fun enableAndUpdateComponent(node: ResizableNode) {
+ if (!isAttached) {
+ check(entity.addComponent(component)) {
+ "Could not add ResizableComponent to Core Entity"
+ }
+ // Ensure size is correct, since we do not update the size
+ // when the component is detached.
+ setComponentSize(size)
+ isAttached = true
+ }
+
+ component.minimumSize = node.minimumSize.toDimensionsInMeters()
+ component.maximumSize = node.maximumSize.toDimensionsInMeters()
+
+ component.fixedAspectRatio = if (node.maintainAspectRatio) getAspectRatioY() else 0.0f
+ }
+
+ /** Returns 0.0f if the aspect ratio of x to y is not well defined. */
+ private fun getAspectRatioY(): Float {
+ if (size.width == 0 || size.height == 0) return 0.0f
+ return size.width.toFloat() / size.height.toFloat()
+ }
+
+ /**
+ * Disables the ResizableComponent for this CoreEntity. Takes care of life cycle tasks for
+ * the underlying component in SceneCore.
+ */
+ private fun disableComponent() {
+ if (isAttached) {
+ entity.removeComponent(component)
+ isAttached = false
+ }
+ }
+
+ /**
+ * Called every time there is an onResizeEnd event in SceneCore, if this CoreEntity is
+ * resizable.
+ */
+ private fun resizeListener(newSize: Dimensions) {
+ val node = resizableNode ?: return
+ if (node.onSizeChange(newSize.toIntVolumeSize(DEFAULT_DENSITY))) {
+ // We're done, the user app will handle the event.
+ return
+ }
+ userSize = newSize.toIntVolumeSize(DEFAULT_DENSITY)
+ }
+ }
+}
+
+/**
+ * A [CoreEntityNode] will apply itself to the entity in question as part of the [CoreEntity]'s
+ * application of modifiers. A [CoreEntityNode] should be applicable to all Entity types.
+ *
+ * TODO(b/374774812)
+ */
+internal interface CoreEntityNode {
+ public fun modifyCoreEntity(coreEntity: CoreEntity)
+}
+
+/** Wrapper class for contentless entities from SceneCore. */
+internal class CoreContentlessEntity(entity: Entity) : CoreEntity(entity) {
+ init {
+ require(entity is ContentlessEntity) {
+ "Entity passed to CoreContentlessEntity should be a ContentlessEntity."
+ }
+ }
+}
+
+/**
+ * Wrapper class for [BasePanelEntity] to provide convenience methods for working with panel
+ * entities from SceneCore.
+ */
+internal class CorePanelEntity(session: Session, private val panelEntity: BasePanelEntity<*>) :
+ CoreEntity(panelEntity) {
+
+ init {
+ movable = Movable(session)
+ resizable = Resizable(session)
+ }
+
+ override fun setEntitySize(size: IntVolumeSize) {
+ panelEntity.setPixelDimensions(PixelDimensions(size.width, size.height))
+ }
+
+ /**
+ * Sets a corner radius on all four corners of this PanelEntity.
+ *
+ * @param radius The radius of the corners, in pixels.
+ * @param density The panel pixel density.
+ * @throws IllegalArgumentException if radius is <= 0.0f.
+ */
+ internal fun setCornerRadius(radius: Float, density: Density) {
+ panelEntity.setCornerRadius(Meter.fromPixel(radius, density).value)
+ }
+
+ /**
+ * Returns the corner radius of this PanelEntity, in pixels.
+ *
+ * @param density The panel pixel density.
+ */
+ internal fun getCornerRadius(density: Density): Float {
+ return Meter(panelEntity.getCornerRadius()).toPx(density)
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/MathExt.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/MathExt.kt
new file mode 100644
index 0000000..75d56f5
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/MathExt.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.compose.ui.unit.Density
+import androidx.xr.compose.unit.Meter
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Vector3
+
+/** Converts the translation from pixels to meters, taking into account [density]. */
+internal fun Pose.convertPixelsToMeters(density: Density): Pose =
+ Pose(translation = translation.convertPixelsToMeters(density), rotation = rotation)
+
+/** Converts the translation from meters to pixels, taking into account [density]. */
+internal fun Pose.convertMetersToPixels(density: Density): Pose =
+ Pose(translation = translation.convertMetersToPixels(density), rotation = rotation)
+
+/** Converts values from pixels to meters, taking into account [density]. */
+internal fun Vector3.convertPixelsToMeters(density: Density): Vector3 =
+ Vector3(
+ Meter.fromPixel(x, density).value,
+ Meter.fromPixel(y, density).value,
+ Meter.fromPixel(z, density).value,
+ )
+
+/** Converts values from meters to pixels, taking into account [density]. */
+internal fun Vector3.convertMetersToPixels(density: Density): Vector3 =
+ Vector3(Meter(x).toPx(density), Meter(y).toPx(density), Meter(z).toPx(density))
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Measurable.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Measurable.kt
new file mode 100644
index 0000000..9453d3d
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Measurable.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+import androidx.xr.compose.unit.VolumeConstraints
+
+/**
+ * A part of the composition layout that can be measured.
+ *
+ * Based on [androidx.compose.ui.layout.Measurable].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface Measurable {
+ /**
+ * Measures the layout with [VolumeConstraints], returning a [Placeable] layout that has its new
+ * size.
+ */
+ public fun measure(constraints: VolumeConstraints): Placeable
+
+ /** Adjusts layout with a new [ParentLayoutParamsAdjustable]. */
+ public fun adjustParams(params: ParentLayoutParamsAdjustable)
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/MeasurePolicy.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/MeasurePolicy.kt
new file mode 100644
index 0000000..e38b2c1
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/MeasurePolicy.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+import androidx.xr.compose.unit.VolumeConstraints
+
+/**
+ * Defines the measure and layout behavior of a layout.
+ *
+ * Based on [androidx.compose.ui.layout.MeasurePolicy].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun interface MeasurePolicy {
+ /**
+ * The function that defines the measurement and layout. Each [Measurable] in the [measurables]
+ * list corresponds to a layout child of the layout, and children can be measured using the
+ * [Measurable.measure] method. This method takes the [VolumeConstraints] which the child should
+ * respect; different children can be measured with different constraints.
+ *
+ * [MeasureResult] objects are usually created using the [MeasureScope.layout] factory, which
+ * takes the calculated size of this layout, its alignment lines, and a block defining the
+ * positioning of the children layouts.
+ */
+ public fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: VolumeConstraints,
+ ): MeasureResult
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/MeasureResult.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/MeasureResult.kt
new file mode 100644
index 0000000..720a76c
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/MeasureResult.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+
+/**
+ * Interface holding the size and alignment lines of the measured layout, as well as the children
+ * positioning logic.
+ *
+ * Based on [androidx.compose.ui.layout.MeasureResult].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface MeasureResult {
+ /** The measured width of the layout, in pixels. */
+ public val width: Int
+
+ /** The measured height of the layout, in pixels. */
+ public val height: Int
+
+ /** The measured depth of the layout, in pixels. */
+ public val depth: Int
+
+ /**
+ * Used for positioning children. [Placeable.placeAt] should be called on children inside
+ * [placeChildren]
+ */
+ public fun placeChildren(placementScope: Placeable.PlacementScope)
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/MeasureScope.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/MeasureScope.kt
new file mode 100644
index 0000000..cc77532
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/MeasureScope.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import android.content.res.Resources
+import androidx.annotation.RestrictTo
+import androidx.compose.ui.unit.Density
+
+/**
+ * The receiver scope of a layout's measure lambda. The return value of the measure lambda is
+ * [MeasureResult], which should be returned by [layout]
+ *
+ * Based on [androidx.compose.ui.layout.MeasureScope].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface MeasureScope : Density {
+
+ /**
+ * Sets the size and alignment lines of the measured layout, as well as the positioning block
+ * that defines the children positioning logic. The [placementBlock] is a lambda used for
+ * positioning children. [Placeable.placeAt] should be called on children inside placementBlock.
+ *
+ * @param width the measured width of the layout, in pixels
+ * @param height the measured height of the layout, in pixels
+ * @param depth the measured depth of the layout, in pixels
+ * @param placementBlock block defining the children positioning of the current layout
+ */
+ public fun layout(
+ width: Int,
+ height: Int,
+ depth: Int,
+ placementBlock: Placeable.PlacementScope.() -> Unit,
+ ): MeasureResult {
+ return object : MeasureResult {
+ override val width = width
+ override val height = height
+ override val depth = depth
+
+ override fun placeChildren(placementScope: Placeable.PlacementScope) {
+ placementScope.placementBlock()
+ }
+ }
+ }
+
+ public override val density: Float
+ get() = Resources.getSystem().displayMetrics.density
+
+ public override val fontScale: Float
+ get() = Resources.getSystem().configuration.fontScale
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Movable.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Movable.kt
new file mode 100644
index 0000000..7b8aded
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Movable.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+import androidx.xr.compose.subspace.node.SubspaceModifierElement
+import androidx.xr.runtime.math.Pose
+
+/**
+ * Moves a subspace element (i.e. currently only affects Jetpack XR Entity Panels/Volumes) in space.
+ * The order of the [SubspaceModifier]s is important. Please take note of this when using movable.
+ * If you have the following modifier chain: SubspaceModifier.offset().size().movable(), the
+ * modifiers will work as expected. If instead you have this modifier chain:
+ * SubspaceModifier.size().offset().movable(), you will experience unexpected placement behavior
+ * when using the movable modifier. In general, the offset modifier should be specified before the
+ * size modifier, and the movable modifier should be specified last.
+ *
+ * @param enabled - true if this composable should be movable.
+ * @param stickyPose - if enabled, the user specified position will be retained when the modifier is
+ * disabled or removed.
+ * @param onPoseChange - a callback to process the pose change during movement, with translation in
+ * pixels. This will only be called if [enabled] is true. If the callback returns false the
+ * default behavior of moving this composable's subspace hierarchy will be executed. If it returns
+ * true, it is the responsibility of the callback to process the event.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.movable(
+ enabled: Boolean = true,
+ stickyPose: Boolean = false,
+ onPoseChange: (Pose) -> Boolean = { false },
+): SubspaceModifier = this.then(MovableElement(enabled, onPoseChange, stickyPose))
+
+private class MovableElement(
+ private val enabled: Boolean,
+ private val onPoseChange: (Pose) -> Boolean,
+ private val stickyPose: Boolean,
+) : SubspaceModifierElement<MovableNode>() {
+
+ override fun create(): MovableNode =
+ MovableNode(enabled = enabled, stickyPose = stickyPose, onPoseChange = onPoseChange)
+
+ override fun update(node: MovableNode) {
+ node.enabled = enabled
+ node.onPoseChange = onPoseChange
+ node.stickyPose = stickyPose
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is MovableElement) return false
+
+ if (enabled != other.enabled) return false
+ if (onPoseChange !== other.onPoseChange) return false
+ if (stickyPose != other.stickyPose) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = enabled.hashCode()
+ result = 31 * result + onPoseChange.hashCode()
+ result = 31 * result + stickyPose.hashCode()
+ return result
+ }
+}
+
+internal class MovableNode(
+ public var enabled: Boolean,
+ public var stickyPose: Boolean,
+ public var onPoseChange: (Pose) -> Boolean,
+) : SubspaceModifier.Node()
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Offset.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Offset.kt
new file mode 100644
index 0000000..df96e06
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Offset.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.xr.compose.subspace.node.SubspaceLayoutModifierNode
+import androidx.xr.compose.subspace.node.SubspaceModifierElement
+import androidx.xr.compose.unit.VolumeConstraints
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Vector3
+
+/**
+ * Offset the content by ([x] dp, [y] dp, [z] dp). The offsets can be positive as well as
+ * non-positive.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.offset(x: Dp = 0.dp, y: Dp = 0.dp, z: Dp = 0.dp): SubspaceModifier =
+ this then SubspaceOffsetElement(x = x, y = y, z = z)
+
+private class SubspaceOffsetElement(public val x: Dp, public val y: Dp, public val z: Dp) :
+ SubspaceModifierElement<OffsetNode>() {
+ override fun create(): OffsetNode {
+ return OffsetNode(x, y, z)
+ }
+
+ override fun update(node: OffsetNode) {
+ node.x = x
+ node.y = y
+ node.z = z
+ }
+
+ override fun hashCode(): Int {
+ var result = x.hashCode()
+ result = 31 * result + y.hashCode()
+ result = 31 * result + z.hashCode()
+ return result
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ val otherElement = other as? OffsetNode ?: return false
+
+ return x == otherElement.x && y == otherElement.y && z == otherElement.z
+ }
+}
+
+private class OffsetNode(public var x: Dp, public var y: Dp, public var z: Dp) :
+ SubspaceLayoutModifierNode, SubspaceModifier.Node() {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: VolumeConstraints,
+ ): MeasureResult {
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.measuredWidth, placeable.measuredHeight, placeable.measuredDepth) {
+ placeable.place(
+ Pose(
+ Vector3(
+ x.roundToPx().toFloat(),
+ y.roundToPx().toFloat(),
+ z.roundToPx().toFloat()
+ )
+ )
+ )
+ }
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/OnGloballyPositionedModifier.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/OnGloballyPositionedModifier.kt
new file mode 100644
index 0000000..1756b4d
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/OnGloballyPositionedModifier.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.xr.compose.subspace.node.SubspaceModifierElement
+
+/**
+ * Invoke [onGloballyPositioned] with the [LayoutVolumeCoordinates] of the element when the global
+ * position of the content may have changed. Note that it will be called **after** a composition
+ * when the coordinates are finalized.
+ *
+ * This callback will be invoked at least once when the [LayoutVolumeCoordinates] are available, and
+ * every time the element's position changes within the window. However, it is not guaranteed to be
+ * invoked every time the position _relative to the screen_ of the modified element changes. For
+ * example, the system may move the contents inside a window around without firing a callback. If
+ * you are using the [LayoutVolumeCoordinates] to calculate position on the screen, and not just
+ * inside the window, you may not receive a callback.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.onGloballyPositioned(
+ onGloballyPositioned: (SubspaceLayoutCoordinates) -> Unit
+): SubspaceModifier = this then OnGloballyPositionedVolumeElement(onGloballyPositioned)
+
+private class OnGloballyPositionedVolumeElement(
+ public val onGloballyPositioned: (SubspaceLayoutCoordinates) -> Unit
+) : SubspaceModifierElement<OnGloballyPositionedNode>() {
+ override fun create(): OnGloballyPositionedNode {
+ return OnGloballyPositionedNode(onGloballyPositioned)
+ }
+
+ override fun update(node: OnGloballyPositionedNode) {
+ node.callback = onGloballyPositioned
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is OnGloballyPositionedVolumeElement) return false
+ return onGloballyPositioned === other.onGloballyPositioned
+ }
+
+ override fun hashCode(): Int {
+ return onGloballyPositioned.hashCode()
+ }
+}
+
+/** Node associated with [onGloballyPositioned]. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class OnGloballyPositionedNode(public var callback: (SubspaceLayoutCoordinates) -> Unit) :
+ SubspaceModifier.Node()
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Padding.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Padding.kt
new file mode 100644
index 0000000..46f051e
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Padding.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.xr.compose.subspace.node.SubspaceLayoutModifierNode
+import androidx.xr.compose.subspace.node.SubspaceModifierElement
+import androidx.xr.compose.unit.VolumeConstraints
+import androidx.xr.compose.unit.constrainDepth
+import androidx.xr.compose.unit.constrainHeight
+import androidx.xr.compose.unit.constrainWidth
+import androidx.xr.compose.unit.offset
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Vector3
+
+/**
+ * Apply additional space along each edge of the content in [Dp]: [left], [top], [right], [bottom],
+ * [front] and [back]. Padding is applied before content measurement and takes precedence; content
+ * may only be as large as the remaining space.
+ *
+ * Negative padding is not permitted — it will cause [IllegalArgumentException].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.padding(
+ left: Dp = 0.dp,
+ top: Dp = 0.dp,
+ right: Dp = 0.dp,
+ bottom: Dp = 0.dp,
+ front: Dp = 0.dp,
+ back: Dp = 0.dp,
+): SubspaceModifier =
+ this then
+ SubspacePaddingElement(
+ left = left,
+ top = top,
+ right = right,
+ bottom = bottom,
+ front = front,
+ back = back,
+ )
+
+/**
+ * Apply [horizontal] dp space along the left and right edges of the content, [vertical] dp space
+ * along the top and bottom edges, and [depth] dp space along front and back edged. Padding is
+ * applied before content measurement and takes precedence; content may only be as large as the
+ * remaining space.
+ *
+ * Negative padding is not permitted — it will cause [IllegalArgumentException]. See [padding]
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.padding(
+ horizontal: Dp = 0.dp,
+ vertical: Dp = 0.dp,
+ depth: Dp = 0.dp,
+): SubspaceModifier =
+ this then
+ SubspacePaddingElement(
+ left = horizontal,
+ top = vertical,
+ right = horizontal,
+ bottom = vertical,
+ front = depth,
+ back = depth,
+ )
+
+/**
+ * Apply [all] dp of additional space along each edge of the content, left, top, right, bottom,
+ * front, and back. Padding is applied before content measurement and takes precedence; content may
+ * only be as large as the remaining space.
+ *
+ * Negative padding is not permitted — it will cause [IllegalArgumentException]. See [padding]
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.padding(all: Dp): SubspaceModifier =
+ this then
+ SubspacePaddingElement(
+ left = all,
+ top = all,
+ right = all,
+ bottom = all,
+ front = all,
+ back = all,
+ )
+
+private class SubspacePaddingElement(
+ public val left: Dp,
+ public val top: Dp,
+ public val right: Dp,
+ public val bottom: Dp,
+ public val front: Dp,
+ public val back: Dp,
+) : SubspaceModifierElement<PaddingNode>() {
+
+ init {
+ require(
+ left.value >= 0f &&
+ top.value >= 0f &&
+ right.value >= 0f &&
+ bottom.value >= 0f &&
+ front.value >= 0f &&
+ back.value >= 0f
+ ) {
+ "Padding must be non-negative"
+ }
+ }
+
+ override fun create(): PaddingNode {
+ return PaddingNode(left, top, right, bottom, front, back)
+ }
+
+ override fun update(node: PaddingNode) {
+ node.left = left
+ node.top = top
+ node.right = right
+ node.bottom = bottom
+ node.front = front
+ node.back = back
+ }
+
+ override fun hashCode(): Int {
+ var result = left.hashCode()
+ result = 31 * result + top.hashCode()
+ result = 31 * result + right.hashCode()
+ result = 31 * result + bottom.hashCode()
+ result = 31 * result + front.hashCode()
+ result = 31 * result + back.hashCode()
+ return result
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ val otherElement = other as? PaddingNode ?: return false
+
+ return left == otherElement.left &&
+ top == otherElement.top &&
+ right == otherElement.right &&
+ bottom == otherElement.bottom &&
+ front == otherElement.front &&
+ back == otherElement.back
+ }
+}
+
+private class PaddingNode(
+ public var left: Dp,
+ public var top: Dp,
+ public var right: Dp,
+ public var bottom: Dp,
+ public var front: Dp,
+ public var back: Dp,
+) : SubspaceLayoutModifierNode, SubspaceModifier.Node() {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: VolumeConstraints,
+ ): MeasureResult {
+ val horizontal = left.roundToPx() + right.roundToPx()
+ val vertical = top.roundToPx() + bottom.roundToPx()
+ val frontAndBack = front.roundToPx() + back.roundToPx()
+
+ val placeable =
+ measurable.measure(constraints.offset(-horizontal, -vertical, -frontAndBack))
+
+ val width = constraints.constrainWidth(placeable.measuredWidth + horizontal)
+ val height = constraints.constrainHeight(placeable.measuredHeight + vertical)
+ val depth = constraints.constrainDepth(placeable.measuredDepth + frontAndBack)
+
+ return layout(width, height, depth) {
+ placeable.place(
+ Pose(
+ Vector3(
+ ((left - right) / 2.0f).roundToPx().toFloat(),
+ ((bottom - top) / 2.0f).roundToPx().toFloat(),
+ ((back - front) / 2.0f).roundToPx().toFloat(),
+ )
+ )
+ )
+ }
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/ParentLayoutParamsModifier.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/ParentLayoutParamsModifier.kt
new file mode 100644
index 0000000..e419fb3
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/ParentLayoutParamsModifier.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+
+/** Interface for classes involved in setting layout params. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface ParentLayoutParamsModifier {
+
+ /** Adjusts layout with new [ParentLayoutParamsAdjustable]. */
+ public fun adjustParams(params: ParentLayoutParamsAdjustable)
+}
+
+/** Marker interface for types allowed to be adjusted by a [ParentLayoutParamsModifier]. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface ParentLayoutParamsAdjustable
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Placeable.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Placeable.kt
new file mode 100644
index 0000000..0ca28326
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Placeable.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.math.Pose
+
+/**
+ * A [Placeable] corresponds to a child layout that can be positioned by its parent layout. Most
+ * [Placeable]s are the result of a [Measurable.measure] call.
+ *
+ * Based on [androidx.compose.ui.layout.Placeable].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public abstract class Placeable {
+ /** The measured width of the layout, in pixels. */
+ public var measuredWidth: Int = 0
+
+ /** The measured height of the layout, in pixels. */
+ public var measuredHeight: Int = 0
+
+ /** The measured depth of the layout, in pixels. */
+ public var measuredDepth: Int = 0
+
+ /** Positions the [Placeable] at [position] in its parent's coordinate system. */
+ protected abstract fun placeAt(pose: Pose)
+
+ /** Receiver scope that permits explicit placement of a [Placeable]. */
+ public abstract class PlacementScope {
+ /**
+ * The [SubspaceLayoutCoordinates] of this layout, if known or `null` if the layout hasn't
+ * been placed yet.
+ */
+ public open val coordinates: SubspaceLayoutCoordinates?
+ get() = null
+
+ /** Place a [Placeable] at the [Pose] in its parent's coordinate system. */
+ public fun Placeable.place(pose: Pose) {
+ placeAt(pose)
+ }
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Resizable.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Resizable.kt
new file mode 100644
index 0000000..0598b4f
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Resizable.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+import androidx.compose.ui.unit.Dp
+import androidx.xr.compose.subspace.node.SubspaceModifierElement
+import androidx.xr.compose.unit.DpVolumeSize
+import androidx.xr.compose.unit.IntVolumeSize
+
+/**
+ * Resize a subspace element (i.e. currently only affects Jetpack XR Entity Panels/Volumes) in
+ * space.
+ *
+ * @param enabled - true if this composable should be resizable.
+ * @param minimumSize - the smallest allowed dimensions for this composable.
+ * @param maximumSize - the largest allowed dimensions for this composable.
+ * @param maintainAspectRatio - true if the new size should maintain the same aspect ratio as the
+ * existing size.
+ * @param onSizeChange - a callback to process the size change during resizing. This will only be
+ * called if [enabled] is true. If the callback returns false the default behavior of resizing
+ * this composable will be executed. If it returns true, it is the responsibility of the callback
+ * to process the event.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.resizable(
+ enabled: Boolean = true,
+ minimumSize: DpVolumeSize = DpVolumeSize.Zero,
+ maximumSize: DpVolumeSize = DpVolumeSize(Dp.Infinity, Dp.Infinity, Dp.Infinity),
+ maintainAspectRatio: Boolean = false,
+ onSizeChange: (IntVolumeSize) -> Boolean = { false },
+): SubspaceModifier =
+ this.then(
+ ResizableElement(enabled, minimumSize, maximumSize, maintainAspectRatio, onSizeChange)
+ )
+
+private class ResizableElement(
+ private val enabled: Boolean,
+ private val minimumSize: DpVolumeSize,
+ private val maximumSize: DpVolumeSize,
+ private val maintainAspectRatio: Boolean,
+ private val onSizeChange: (IntVolumeSize) -> Boolean,
+) : SubspaceModifierElement<ResizableNode>() {
+
+ init {
+ // TODO(b/345303299): Decide on implementation for min/max size bound checking against
+ // current
+ // size.
+ require(
+ minimumSize.depth <= maximumSize.depth &&
+ minimumSize.height <= maximumSize.height &&
+ minimumSize.width <= maximumSize.width
+ ) {
+ "minimumSize must be less than or equal to maximumSize"
+ }
+ }
+
+ override fun create(): ResizableNode =
+ ResizableNode(enabled, minimumSize, maximumSize, maintainAspectRatio, onSizeChange)
+
+ override fun update(node: ResizableNode) {
+ node.enabled = enabled
+ node.minimumSize = minimumSize
+ node.maximumSize = maximumSize
+ node.maintainAspectRatio = maintainAspectRatio
+ node.onSizeChange = onSizeChange
+ }
+
+ override fun hashCode(): Int {
+ var result = enabled.hashCode()
+ result = 31 * result + minimumSize.hashCode()
+ result = 31 * result + maximumSize.hashCode()
+ result = 31 * result + maintainAspectRatio.hashCode()
+ result = 31 * result + onSizeChange.hashCode()
+ return result
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ val otherElement = other as? ResizableNode ?: return false
+
+ return enabled == otherElement.enabled &&
+ minimumSize == otherElement.minimumSize &&
+ maximumSize == otherElement.maximumSize &&
+ maintainAspectRatio == otherElement.maintainAspectRatio &&
+ onSizeChange === otherElement.onSizeChange
+ }
+}
+
+internal class ResizableNode(
+ internal var enabled: Boolean,
+ internal var minimumSize: DpVolumeSize,
+ internal var maximumSize: DpVolumeSize,
+ internal var maintainAspectRatio: Boolean,
+ internal var onSizeChange: (IntVolumeSize) -> Boolean,
+) : SubspaceModifier.Node()
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Rotate.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Rotate.kt
new file mode 100644
index 0000000..d5ce88a
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Rotate.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+import androidx.xr.compose.subspace.node.SubspaceLayoutModifierNode
+import androidx.xr.compose.subspace.node.SubspaceModifierElement
+import androidx.xr.compose.unit.VolumeConstraints
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Vector3
+
+/**
+ * Rotate a subspace element (i.e. Panel) in space. Parameter rotation angles are specified in
+ * degrees. The rotations are applied with the order pitch, then yaw, then roll.
+ *
+ * @param pitch Rotation around the x-axis. The x-axis is the axis width is measured on.
+ * @param yaw Rotation around the y-axis. The y-axis is the axis height is measured on.
+ * @param roll Rotation around the z-axis. The z-axis is the axis depth is measured on.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.rotate(pitch: Float, yaw: Float, roll: Float): SubspaceModifier =
+ this.then(RotationElement(pitch, yaw, roll))
+
+/**
+ * Rotate a subspace element (i.e. Panel) in space.
+ *
+ * @param axisAngle Vector representing the axis of rotation.
+ * @param rotation Degrees of rotation.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.rotate(axisAngle: Vector3, rotation: Float): SubspaceModifier =
+ this.then(RotationElement(axisAngle, rotation))
+
+/**
+ * Rotate a subspace element (i.e. Panel) in space.
+ *
+ * @param quaternion Quaternion describing the rotation.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.rotate(quaternion: Quaternion): SubspaceModifier =
+ this.then(RotationElement(quaternion))
+
+private class RotationElement(private val quaternion: Quaternion) :
+ SubspaceModifierElement<RotationNode>() {
+
+ public constructor(
+ pitch: Float,
+ yaw: Float,
+ roll: Float,
+ ) : this(Quaternion.fromEulerAngles(pitch, yaw, roll))
+
+ public constructor(
+ axisAngle: Vector3,
+ rotation: Float,
+ ) : this(Quaternion.fromAxisAngle(axisAngle, rotation))
+
+ override fun create(): RotationNode = RotationNode(quaternion)
+
+ override fun hashCode(): Int {
+ return quaternion.hashCode()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is RotationElement) return false
+
+ if (quaternion != other.quaternion) return false
+
+ return true
+ }
+
+ override fun update(node: RotationNode) {
+ node.quaternion = quaternion
+ }
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class RotationNode(public var quaternion: Quaternion) :
+ SubspaceLayoutModifierNode, SubspaceModifier.Node() {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: VolumeConstraints,
+ ): MeasureResult {
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.measuredWidth, placeable.measuredHeight, placeable.measuredDepth) {
+ placeable.place(Pose(translation = Vector3.Zero, rotation = quaternion))
+ }
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Scale.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Scale.kt
new file mode 100644
index 0000000..d8eb2a2
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Scale.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+import androidx.xr.compose.subspace.node.SubspaceModifierElement
+
+/**
+ * Scale the contents of the composable by the scale factor along horizontal, vertical, and depth
+ * axes. Scaling does not change the measured size of the composable content during layout. Measured
+ * size of @SubspaceComposable elements can be controlled using Size Modifiers. Scale factor should
+ * be a positive number.
+ *
+ * @param scale - Multiplier to scale content along vertical, horizontal, depth axes.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.scale(scale: Float): SubspaceModifier = this.then(ScaleElement(scale))
+
+private class ScaleElement(private val scale: Float) : SubspaceModifierElement<ScaleNode>() {
+
+ init {
+ require(scale > 0.0f) { "scale values must be > 0.0f" }
+ }
+
+ override fun create(): ScaleNode = ScaleNode(scale)
+
+ override fun update(node: ScaleNode) {
+ node.scale = scale
+ }
+
+ override fun hashCode(): Int {
+ return scale.hashCode()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is ScaleElement) return false
+
+ return scale == other.scale
+ }
+}
+
+private class ScaleNode(public var scale: Float) : SubspaceModifier.Node(), CoreEntityNode {
+ override fun modifyCoreEntity(coreEntity: CoreEntity) {
+ coreEntity.scale = scale
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SemanticsModifier.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SemanticsModifier.kt
new file mode 100644
index 0000000..64a8aab
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SemanticsModifier.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
+import androidx.xr.compose.subspace.node.SubspaceModifierElement
+import androidx.xr.compose.subspace.node.SubspaceSemanticsModifierNode
+
+/**
+ * Add semantics key/value pairs to the layout node, for use in testing, accessibility, etc.
+ *
+ * Based on [androidx.compose.ui.semantics.SemanticsModifier].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.semantics(
+ properties: (SemanticsPropertyReceiver.() -> Unit)
+): SubspaceModifier = this then AppendedSemanticsElement(properties = properties)
+
+private class AppendedSemanticsElement(
+ private val properties: (SemanticsPropertyReceiver.() -> Unit)
+) : SubspaceModifierElement<SemanticsModifierNode>() {
+
+ override fun create(): SemanticsModifierNode {
+ return SemanticsModifierNode(properties = properties)
+ }
+
+ override fun update(node: SemanticsModifierNode) {
+ node.properties = properties
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is AppendedSemanticsElement) return false
+ return properties === other.properties
+ }
+
+ override fun hashCode(): Int {
+ return properties.hashCode()
+ }
+}
+
+private class SemanticsModifierNode(public var properties: SemanticsPropertyReceiver.() -> Unit) :
+ SubspaceModifier.Node(), SubspaceSemanticsModifierNode {
+ override fun SemanticsPropertyReceiver.applySemantics() {
+ properties()
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Size.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Size.kt
new file mode 100644
index 0000000..95bbb02
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/Size.kt
@@ -0,0 +1,527 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.FloatRange
+import androidx.annotation.RestrictTo
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.util.fastRoundToInt
+import androidx.xr.compose.subspace.node.SubspaceLayoutModifierNode
+import androidx.xr.compose.subspace.node.SubspaceModifierElement
+import androidx.xr.compose.unit.DpVolumeSize
+import androidx.xr.compose.unit.VolumeConstraints
+import androidx.xr.compose.unit.constrain
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Vector3
+
+/** Declare the preferred size of the content to be exactly [width] dp along the x dimension. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.width(width: Dp): SubspaceModifier =
+ this.then(SizeElement(minWidth = width, maxWidth = width, enforceIncoming = true))
+
+/** Declare the preferred size of the content to be exactly [height] dp along the y dimension. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.height(height: Dp): SubspaceModifier =
+ this.then(SizeElement(minHeight = height, maxHeight = height, enforceIncoming = true))
+
+/**
+ * Declare the preferred size of the content to be exactly [depth] dp along the z dimension. Panels
+ * have 0 depth and ignore this modifier.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.depth(depth: Dp): SubspaceModifier =
+ this.then(SizeElement(minDepth = depth, maxDepth = depth, enforceIncoming = true))
+
+/**
+ * Declare the preferred size of the content to be exactly a [size] dp cube. When applied to a
+ * Panel, the preferred size will be a [size] dp square instead.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.size(size: Dp): SubspaceModifier =
+ this.then(
+ SizeElement(
+ minWidth = size,
+ maxWidth = size,
+ minHeight = size,
+ maxHeight = size,
+ minDepth = size,
+ maxDepth = size,
+ enforceIncoming = true,
+ )
+ )
+
+/**
+ * Declare the preferred size of the content to be exactly [size] in each of the three dimensions.
+ * Panels have 0 depth and ignore the z-component of this modifier.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.size(size: DpVolumeSize): SubspaceModifier =
+ this.then(
+ SizeElement(
+ minWidth = size.width,
+ maxWidth = size.width,
+ minHeight = size.height,
+ maxHeight = size.height,
+ minDepth = size.depth,
+ maxDepth = size.depth,
+ enforceIncoming = true,
+ )
+ )
+
+/** Declare the size of the content to be exactly [width] dp along the x dimension. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.requiredWidth(width: Dp): SubspaceModifier =
+ this.then(SizeElement(minWidth = width, maxWidth = width, enforceIncoming = false))
+
+/** Declare the size of the content to be exactly [height] dp along the y dimension. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.requiredHeight(height: Dp): SubspaceModifier =
+ this.then(SizeElement(minHeight = height, maxHeight = height, enforceIncoming = false))
+
+/**
+ * Declare the size of the content to be exactly [depth] dp along the z dimension. Panels have 0
+ * depth and ignore this modifier.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.requiredDepth(depth: Dp): SubspaceModifier =
+ this.then(SizeElement(minDepth = depth, maxDepth = depth, enforceIncoming = false))
+
+/**
+ * Declare the size of the content to be exactly a [size] dp cube. When applied to a Panel, the size
+ * will be a [size] dp square instead.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.requiredSize(size: Dp): SubspaceModifier =
+ this.then(
+ SizeElement(
+ minWidth = size,
+ maxWidth = size,
+ minHeight = size,
+ maxHeight = size,
+ minDepth = size,
+ maxDepth = size,
+ enforceIncoming = false,
+ )
+ )
+
+/**
+ * Declare the size of the content to be exactly [size] in each of the three dimensions. Panels have
+ * 0 depth and ignore the z-component of this modifier.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.requiredSize(size: DpVolumeSize): SubspaceModifier =
+ this.then(
+ SizeElement(
+ minWidth = size.width,
+ maxWidth = size.width,
+ minHeight = size.height,
+ maxHeight = size.height,
+ minDepth = size.depth,
+ maxDepth = size.depth,
+ enforceIncoming = false,
+ )
+ )
+
+/**
+ * Have the content fill (possibly only partially) the [VolumeConstraints.maxWidth] of the incoming
+ * measurement constraints, by setting the [minimum width][VolumeConstraints.minWidth] and the
+ * [maximum width][VolumeConstraints.maxWidth] to be equal to the
+ * [maximum width][VolumeConstraints.maxWidth] multiplied by [fraction]. Note that, by default, the
+ * [fraction] is 1, so the modifier will make the content fill the whole available width. If the
+ * incoming maximum width is [VolumeConstraints.Infinity] this modifier will have no effect.
+ *
+ * @param fraction The fraction of the maximum width to use, between `0` and `1`, inclusive.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.fillMaxWidth(
+ @FloatRange(from = 0.0, to = 1.0) fraction: Float = 1f
+): SubspaceModifier =
+ this.then(if (fraction == 1f) FillWholeMaxWidth else FillElement.width(fraction))
+
+private val FillWholeMaxWidth = FillElement.width(1f)
+
+/**
+ * Have the content fill (possibly only partially) the [VolumeConstraints.maxHeight] of the incoming
+ * measurement constraints, by setting the [minimum height][VolumeConstraints.minHeight] and the
+ * [maximum height][VolumeConstraints.maxHeight] to be equal to the
+ * [maximum height][VolumeConstraints.maxHeight] multiplied by [fraction]. Note that, by default,
+ * the [fraction] is 1, so the modifier will make the content fill the whole available height. If
+ * the incoming maximum height is [VolumeConstraints.Infinity] this modifier will have no effect.
+ *
+ * @param fraction The fraction of the maximum height to use, between `0` and `1`, inclusive.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.fillMaxHeight(
+ @FloatRange(from = 0.0, to = 1.0) fraction: Float = 1f
+): SubspaceModifier =
+ this.then(if (fraction == 1f) FillWholeMaxHeight else FillElement.height(fraction))
+
+private val FillWholeMaxHeight = FillElement.height(1f)
+
+/**
+ * Have the content fill (possibly only partially) the [VolumeConstraints.maxDepth] of the incoming
+ * measurement constraints, by setting the [minimum depth][VolumeConstraints.minDepth] and the
+ * [maximum depth][VolumeConstraints.maxDepth] to be equal to the
+ * [maximum depth][VolumeConstraints.maxDepth] multiplied by [fraction]. Note that, by default, the
+ * [fraction] is 1, so the modifier will make the content fill the whole available depth. If the
+ * incoming maximum depth is [VolumeConstraints.Infinity] this modifier will have no effect.
+ *
+ * @param fraction The fraction of the maximum height to use, between `0` and `1`, inclusive.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.fillMaxDepth(
+ @FloatRange(from = 0.0, to = 1.0) fraction: Float = 1f
+): SubspaceModifier =
+ this.then(if (fraction == 1f) FillWholeMaxDepth else FillElement.depth(fraction))
+
+private val FillWholeMaxDepth = FillElement.depth(1f)
+
+/**
+ * Have the content fill (possibly only partially) the [VolumeConstraints.maxWidth],
+ * [VolumeConstraints.maxHeight], and [VolumeConstraints.maxDepth] of the incoming measurement
+ * constraints. See [SubspaceModifier.fillMaxWidth], [SubspaceModifier.fillMaxHeight], and
+ * [SubspaceModifier.fillMaxDepth] for details. Note that, by default, the [fraction] is 1, so the
+ * modifier will make the content fill the whole available space. If the incoming maximum width or
+ * height or depth is [VolumeConstraints.Infinity] this modifier will have no effect in that
+ * dimension.
+ *
+ * @param fraction The fraction of the maximum size to use, between `0` and `1`, inclusive.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.fillMaxSize(
+ @FloatRange(from = 0.0, to = 1.0) fraction: Float = 1f
+): SubspaceModifier =
+ this.then(if (fraction == 1f) FillWholeMaxSize else FillElement.size(fraction))
+
+private val FillWholeMaxSize = FillElement.size(1f)
+
+private class FillElement(
+ private val direction: Direction,
+ private val fraction: Float,
+ private val inspectorName: String,
+) : SubspaceModifierElement<FillNode>() {
+ override fun create(): FillNode = FillNode(direction = direction, fraction = fraction)
+
+ override fun update(node: FillNode) {
+ node.direction = direction
+ node.fraction = fraction
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is FillElement) return false
+
+ if (direction != other.direction) return false
+ if (fraction != other.fraction) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = direction.hashCode()
+ result = 31 * result + fraction.hashCode()
+ return result
+ }
+
+ @Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
+ public companion object {
+ public fun width(fraction: Float) =
+ FillElement(
+ direction = Direction.X,
+ fraction = fraction,
+ inspectorName = "fillMaxWidth"
+ )
+
+ public fun height(fraction: Float) =
+ FillElement(
+ direction = Direction.Y,
+ fraction = fraction,
+ inspectorName = "fillMaxHeight"
+ )
+
+ public fun depth(fraction: Float) =
+ FillElement(
+ direction = Direction.Z,
+ fraction = fraction,
+ inspectorName = "fillMaxDepth"
+ )
+
+ public fun size(fraction: Float) =
+ FillElement(
+ direction = Direction.AllThree,
+ fraction = fraction,
+ inspectorName = "fillMaxSize",
+ )
+ }
+}
+
+private class FillNode(public var direction: Direction, public var fraction: Float) :
+ SubspaceLayoutModifierNode, SubspaceModifier.Node() {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: VolumeConstraints,
+ ): MeasureResult {
+ val minWidth: Int
+ val maxWidth: Int
+ if (
+ constraints.hasBoundedWidth &&
+ (direction == Direction.X || direction == Direction.AllThree)
+ ) {
+ val width =
+ (constraints.maxWidth * fraction)
+ .fastRoundToInt()
+ .coerceIn(constraints.minWidth, constraints.maxWidth)
+ minWidth = width
+ maxWidth = width
+ } else {
+ minWidth = constraints.minWidth
+ maxWidth = constraints.maxWidth
+ }
+ val minHeight: Int
+ val maxHeight: Int
+ if (
+ constraints.hasBoundedHeight &&
+ (direction == Direction.Y || direction == Direction.AllThree)
+ ) {
+ val height =
+ (constraints.maxHeight * fraction)
+ .fastRoundToInt()
+ .coerceIn(constraints.minHeight, constraints.maxHeight)
+ minHeight = height
+ maxHeight = height
+ } else {
+ minHeight = constraints.minHeight
+ maxHeight = constraints.maxHeight
+ }
+ val minDepth: Int
+ val maxDepth: Int
+ if (
+ constraints.hasBoundedDepth &&
+ (direction == Direction.Z || direction == Direction.AllThree)
+ ) {
+ val depth =
+ (constraints.maxDepth * fraction)
+ .fastRoundToInt()
+ .coerceIn(constraints.minDepth, constraints.maxDepth)
+ minDepth = depth
+ maxDepth = depth
+ } else {
+ minDepth = constraints.minDepth
+ maxDepth = constraints.maxDepth
+ }
+ val placeable =
+ measurable.measure(
+ VolumeConstraints(minWidth, maxWidth, minHeight, maxHeight, minDepth, maxDepth)
+ )
+
+ return layout(placeable.measuredWidth, placeable.measuredHeight, placeable.measuredDepth) {
+ placeable.place(Pose(translation = Vector3.Zero, rotation = Quaternion.Identity))
+ }
+ }
+}
+
+private class SizeElement(
+ private val minWidth: Dp = Dp.Unspecified,
+ private val maxWidth: Dp = Dp.Unspecified,
+ private val minHeight: Dp = Dp.Unspecified,
+ private val maxHeight: Dp = Dp.Unspecified,
+ private val minDepth: Dp = Dp.Unspecified,
+ private val maxDepth: Dp = Dp.Unspecified,
+ private val enforceIncoming: Boolean,
+) : SubspaceModifierElement<SizeNode>() {
+ override fun create(): SizeNode =
+ SizeNode(
+ minWidth = minWidth,
+ minHeight = minHeight,
+ maxWidth = maxWidth,
+ maxHeight = maxHeight,
+ minDepth = minDepth,
+ maxDepth = maxDepth,
+ enforceIncoming = enforceIncoming,
+ )
+
+ override fun hashCode(): Int {
+ var result = minWidth.hashCode()
+ result = 31 * result + minHeight.hashCode()
+ result = 31 * result + maxWidth.hashCode()
+ result = 31 * result + maxHeight.hashCode()
+ result = 31 * result + minDepth.hashCode()
+ result = 31 * result + maxDepth.hashCode()
+ result = 31 * result + enforceIncoming.hashCode()
+ return result
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is SizeElement) return false
+
+ if (minWidth != other.minWidth) return false
+ if (minHeight != other.minHeight) return false
+ if (maxWidth != other.maxWidth) return false
+ if (maxHeight != other.maxHeight) return false
+ if (minDepth != other.minDepth) return false
+ if (maxDepth != other.maxDepth) return false
+ if (enforceIncoming != other.enforceIncoming) return false
+
+ return true
+ }
+
+ override fun update(node: SizeNode) {
+ node.minWidth = minWidth
+ node.minHeight = minHeight
+ node.maxWidth = maxWidth
+ node.maxHeight = maxHeight
+ node.minDepth = minDepth
+ node.maxDepth = maxDepth
+ node.enforceIncoming = enforceIncoming
+ }
+}
+
+private class SizeNode(
+ public var minWidth: Dp = Dp.Unspecified,
+ public var maxWidth: Dp = Dp.Unspecified,
+ public var minHeight: Dp = Dp.Unspecified,
+ public var maxHeight: Dp = Dp.Unspecified,
+ public var minDepth: Dp = Dp.Unspecified,
+ public var maxDepth: Dp = Dp.Unspecified,
+ public var enforceIncoming: Boolean,
+) : SubspaceLayoutModifierNode, SubspaceModifier.Node() {
+
+ private val MeasureScope.targetConstraints: VolumeConstraints
+ get() {
+ val maxWidth =
+ if (maxWidth != Dp.Unspecified) {
+ maxWidth.roundToPx().coerceAtLeast(0)
+ } else {
+ VolumeConstraints.INFINITY
+ }
+ val maxHeight =
+ if (maxHeight != Dp.Unspecified) {
+ maxHeight.roundToPx().coerceAtLeast(0)
+ } else {
+ VolumeConstraints.INFINITY
+ }
+ val maxDepth =
+ if (maxDepth != Dp.Unspecified) {
+ maxDepth.roundToPx().coerceAtLeast(0)
+ } else {
+ VolumeConstraints.INFINITY
+ }
+ val minWidth =
+ if (minWidth != Dp.Unspecified) {
+ minWidth.roundToPx().coerceAtMost(maxWidth).coerceAtLeast(0).let {
+ if (it != VolumeConstraints.INFINITY) it else 0
+ }
+ } else {
+ 0
+ }
+ val minHeight =
+ if (minHeight != Dp.Unspecified) {
+ minHeight.roundToPx().coerceAtMost(maxHeight).coerceAtLeast(0).let {
+ if (it != VolumeConstraints.INFINITY) it else 0
+ }
+ } else {
+ 0
+ }
+ val minDepth =
+ if (minDepth != Dp.Unspecified) {
+ minDepth.roundToPx().coerceAtMost(maxDepth).coerceAtLeast(0).let {
+ if (it != VolumeConstraints.INFINITY) it else 0
+ }
+ } else {
+ 0
+ }
+ return VolumeConstraints(
+ minWidth = minWidth,
+ minHeight = minHeight,
+ maxWidth = maxWidth,
+ maxHeight = maxHeight,
+ minDepth = minDepth,
+ maxDepth = maxDepth,
+ )
+ }
+
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: VolumeConstraints,
+ ): MeasureResult {
+ val wrappedConstraints =
+ targetConstraints.let {
+ if (enforceIncoming) {
+ constraints.constrain(targetConstraints)
+ } else {
+ val resolvedMinWidth =
+ if (minWidth != Dp.Unspecified) {
+ targetConstraints.minWidth
+ } else {
+ constraints.minWidth.coerceAtMost(targetConstraints.maxWidth)
+ }
+ val resolvedMaxWidth =
+ if (maxWidth != Dp.Unspecified) {
+ targetConstraints.maxWidth
+ } else {
+ constraints.maxWidth.coerceAtLeast(targetConstraints.minWidth)
+ }
+ val resolvedMinHeight =
+ if (minHeight != Dp.Unspecified) {
+ targetConstraints.minHeight
+ } else {
+ constraints.minHeight.coerceAtMost(targetConstraints.maxHeight)
+ }
+ val resolvedMaxHeight =
+ if (maxHeight != Dp.Unspecified) {
+ targetConstraints.maxHeight
+ } else {
+ constraints.maxHeight.coerceAtLeast(targetConstraints.minHeight)
+ }
+ val resolvedMinDepth =
+ if (minDepth != Dp.Unspecified) {
+ targetConstraints.minDepth
+ } else {
+ constraints.minDepth.coerceAtMost(targetConstraints.maxDepth)
+ }
+ val resolvedMaxDepth =
+ if (maxDepth != Dp.Unspecified) {
+ targetConstraints.maxDepth
+ } else {
+ constraints.maxDepth.coerceAtLeast(targetConstraints.minDepth)
+ }
+ VolumeConstraints(
+ resolvedMinWidth,
+ resolvedMaxWidth,
+ resolvedMinHeight,
+ resolvedMaxHeight,
+ resolvedMinDepth,
+ resolvedMaxDepth,
+ )
+ }
+ }
+
+ val placeable = measurable.measure(wrappedConstraints)
+ return layout(placeable.measuredWidth, placeable.measuredHeight, placeable.measuredDepth) {
+ placeable.place(Pose())
+ }
+ }
+}
+
+internal enum class Direction {
+ X,
+ Y,
+ Z,
+ AllThree,
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SpatialAlignment.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SpatialAlignment.kt
new file mode 100644
index 0000000..bb44eca
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SpatialAlignment.kt
@@ -0,0 +1,274 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+import androidx.xr.compose.unit.IntVolumeSize
+import androidx.xr.runtime.math.Vector3
+import kotlin.math.roundToInt
+
+/**
+ * An interface to calculate the position of a sized box inside of an available 3D space.
+ * [SpatialAlignment] is often used to define the alignment of a layout inside a parent layout.
+ *
+ * @see SpatialBiasAlignment
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface SpatialAlignment {
+ /**
+ * Provides the horizontal offset from the origin of the space to the origin of the content.
+ *
+ * @param width The content width in pixels.
+ * @param space The available space in pixels.
+ */
+ public fun horizontalOffset(width: Int, space: Int): Int
+
+ /**
+ * Provides the vertical offset from the origin of the space to the origin of the content.
+ *
+ * @param height The content height in pixels.
+ * @param space The available space in pixels.
+ */
+ public fun verticalOffset(height: Int, space: Int): Int
+
+ /**
+ * Provides the depth offset from the origin of the space to the origin of the content.
+ *
+ * @param depth The content depth in pixels.
+ * @param space The available space in pixels.
+ */
+ public fun depthOffset(depth: Int, space: Int): Int
+
+ /**
+ * Provides the origin-based position of the content in the available space.
+ *
+ * @param size The content size in pixels.
+ * @param space The available space in pixels.
+ */
+ public fun position(size: IntVolumeSize, space: IntVolumeSize): Vector3
+
+ /**
+ * An interface to calculate the position of a box of a certain width inside an available width.
+ */
+ public interface Horizontal {
+ /** @see horizontalOffset */
+ public fun offset(width: Int, space: Int): Int
+ }
+
+ /**
+ * An interface to calculate the position of a box of a certain height inside an available
+ * height.
+ */
+ public interface Vertical {
+ /** @see verticalOffset */
+ public fun offset(height: Int, space: Int): Int
+ }
+
+ /**
+ * An interface to calculate the position of a box of a certain depth inside an available depth.
+ */
+ public interface Depth {
+ /** @see depthOffset */
+ public fun offset(depth: Int, space: Int): Int
+ }
+
+ public companion object {
+ // 2D alignments
+ @JvmStatic public val TopLeft: SpatialAlignment = SpatialBiasAlignment(-1f, 1f, 0f)
+ @JvmStatic public val TopCenter: SpatialAlignment = SpatialBiasAlignment(0f, 1f, 0f)
+ @JvmStatic public val TopRight: SpatialAlignment = SpatialBiasAlignment(1f, 1f, 0f)
+ @JvmStatic public val CenterLeft: SpatialAlignment = SpatialBiasAlignment(-1f, 0f, 0f)
+ @JvmStatic public val Center: SpatialAlignment = SpatialBiasAlignment(0f, 0f, 0f)
+ @JvmStatic public val CenterRight: SpatialAlignment = SpatialBiasAlignment(1f, 0f, 0f)
+ @JvmStatic public val BottomLeft: SpatialAlignment = SpatialBiasAlignment(-1f, -1f, 0f)
+ @JvmStatic public val BottomCenter: SpatialAlignment = SpatialBiasAlignment(0f, -1f, 0f)
+ @JvmStatic public val BottomRight: SpatialAlignment = SpatialBiasAlignment(1f, -1f, 0f)
+
+ // Horizontal alignments
+ @JvmStatic
+ public val Left: SpatialBiasAlignment.Horizontal = SpatialBiasAlignment.Horizontal(-1f)
+ @JvmStatic
+ public val CenterHorizontally: SpatialBiasAlignment.Horizontal =
+ SpatialBiasAlignment.Horizontal(0f)
+ @JvmStatic
+ public val Right: SpatialBiasAlignment.Horizontal = SpatialBiasAlignment.Horizontal(1f)
+
+ // Vertical alignments
+ @JvmStatic
+ public val Bottom: SpatialBiasAlignment.Vertical = SpatialBiasAlignment.Vertical(-1f)
+ @JvmStatic
+ public val CenterVertically: SpatialBiasAlignment.Vertical =
+ SpatialBiasAlignment.Vertical(0f)
+ @JvmStatic public val Top: SpatialBiasAlignment.Vertical = SpatialBiasAlignment.Vertical(1f)
+
+ // Depth alignments
+ @JvmStatic public val Back: SpatialBiasAlignment.Depth = SpatialBiasAlignment.Depth(-1f)
+ @JvmStatic
+ public val CenterDepthwise: SpatialBiasAlignment.Depth = SpatialBiasAlignment.Depth(0f)
+ @JvmStatic public val Front: SpatialBiasAlignment.Depth = SpatialBiasAlignment.Depth(1f)
+ }
+}
+
+/**
+ * Creates a weighted alignment that specifies a horizontal, vertical, and depth bias.
+ *
+ * @param horizontalBias Must be within the range of [-1, 1] with -1 being left and 1 being right.
+ * @param verticalBias Must be within the range of [-1, 1] with -1 being bottom and 1 being top.
+ * @param depthBias Must be within the range of [-1, 1] with -1 being back and 1 being front.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SpatialBiasAlignment(
+ public val horizontalBias: Float,
+ public val verticalBias: Float,
+ public val depthBias: Float,
+) : SpatialAlignment {
+ override fun horizontalOffset(width: Int, space: Int): Int =
+ offset(horizontalBias, width, space)
+
+ override fun verticalOffset(height: Int, space: Int): Int = offset(verticalBias, height, space)
+
+ override fun depthOffset(depth: Int, space: Int): Int = offset(depthBias, depth, space)
+
+ override fun position(size: IntVolumeSize, space: IntVolumeSize): Vector3 =
+ Vector3(
+ horizontalOffset(size.width, space.width).toFloat(),
+ verticalOffset(size.height, space.height).toFloat(),
+ depthOffset(size.depth, space.depth).toFloat(),
+ )
+
+ public fun copy(
+ horizontalBias: Float = this.horizontalBias,
+ verticalBias: Float = this.verticalBias,
+ depthBias: Float = this.depthBias,
+ ): SpatialBiasAlignment =
+ SpatialBiasAlignment(
+ horizontalBias = horizontalBias,
+ verticalBias = verticalBias,
+ depthBias = depthBias,
+ )
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is SpatialBiasAlignment) return false
+
+ if (horizontalBias != other.horizontalBias) return false
+ if (verticalBias != other.verticalBias) return false
+ if (depthBias != other.depthBias) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = horizontalBias.hashCode()
+ result = 31 * result + verticalBias.hashCode()
+ result = 31 * result + depthBias.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "SpatialBiasAlignment(horizontalBias=$horizontalBias, verticalBias=$verticalBias, depthBias=$depthBias)"
+ }
+
+ /**
+ * Creates a weighted alignment that specifies a horizontal bias.
+ *
+ * @param bias Must be within the range of [-1, 1] with -1 being left and 1 being right.
+ */
+ public class Horizontal(public val bias: Float) : SpatialAlignment.Horizontal {
+ override fun offset(width: Int, space: Int): Int = offset(bias, width, space)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Horizontal) return false
+
+ if (bias != other.bias) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return bias.hashCode()
+ }
+
+ override fun toString(): String {
+ return "Horizontal(bias=$bias)"
+ }
+
+ public fun copy(bias: Float = this.bias): Horizontal = Horizontal(bias = bias)
+ }
+
+ /**
+ * Creates a weighted alignment that specifies a vertical bias.
+ *
+ * @param bias Must be within the range of [-1, 1] with -1 being bottom and 1 being top.
+ */
+ public class Vertical(public val bias: Float) : SpatialAlignment.Vertical {
+ override fun offset(height: Int, space: Int): Int = offset(bias, height, space)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Vertical) return false
+
+ if (bias != other.bias) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return bias.hashCode()
+ }
+
+ override fun toString(): String {
+ return "Vertical(bias=$bias)"
+ }
+
+ public fun copy(bias: Float = this.bias): Vertical = Vertical(bias = bias)
+ }
+
+ /**
+ * Creates a weighted alignment that specifies a depth bias.
+ *
+ * @param bias Must be within the range of [-1, 1] with -1 being back and 1 being front.
+ */
+ public class Depth(public val bias: Float) : SpatialAlignment.Depth {
+ override fun offset(depth: Int, space: Int): Int = offset(bias, depth, space)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Depth) return false
+
+ if (bias != other.bias) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return bias.hashCode()
+ }
+
+ override fun toString(): String {
+ return "Depth(bias=$bias)"
+ }
+
+ public fun copy(bias: Float = this.bias): Depth = Depth(bias = bias)
+ }
+
+ public companion object {
+ private fun offset(bias: Float, size: Int, space: Int): Int =
+ ((space - size) / 2.0f * bias).roundToInt()
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SpatialShape.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SpatialShape.kt
new file mode 100644
index 0000000..8096cf9
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SpatialShape.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.unit.Density
+
+/** Base Spatial shape. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class SpatialShape
+
+/** A shape describing a rectangle with rounded corners in 3D space. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SpatialRoundedCornerShape(private val size: CornerSize) : SpatialShape() {
+ /**
+ * Computes corner radius to be no larger than 50 percent of the smallest side.
+ *
+ * @return The corner radius in pixels.
+ */
+ internal fun computeCornerRadius(maxWidth: Float, maxHeight: Float, density: Density): Float {
+ return size
+ .toPx(Size(maxWidth, maxHeight), density)
+ .coerceAtMost(maxWidth / 2f)
+ .coerceAtMost(maxHeight / 2f)
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspaceLayout.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspaceLayout.kt
new file mode 100644
index 0000000..8723919
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspaceLayout.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ComposeNode
+import androidx.xr.compose.subspace.SubspaceComposable
+import androidx.xr.compose.subspace.node.ComposeSubspaceNode
+import androidx.xr.compose.subspace.node.ComposeSubspaceNode.Companion.SetCoreEntity
+import androidx.xr.compose.subspace.node.ComposeSubspaceNode.Companion.SetMeasurePolicy
+import androidx.xr.compose.subspace.node.ComposeSubspaceNode.Companion.SetModifier
+import androidx.xr.compose.subspace.node.ComposeSubspaceNode.Companion.SetName
+import androidx.xr.compose.subspace.node.SubspaceNodeApplier
+
+/**
+ * [SubspaceLayout] is the main core component for layout for "leaf" nodes. It can be used to
+ * measure and position zero children.
+ *
+ * The measurement, layout and intrinsic measurement behaviours of this layout will be defined by
+ * the [measurePolicy] instance. See [MeasurePolicy] for more details.
+ *
+ * @param modifier SubspaceModifier to apply during layout.
+ * @param name a name for the ComposeSubspaceNode. This can be useful for debugging.
+ * @param measurePolicy a policy defining the measurement and positioning of the layout.
+ */
+@Suppress("NOTHING_TO_INLINE")
+@SubspaceComposable
+@Composable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceLayout(
+ modifier: SubspaceModifier = SubspaceModifier,
+ name: String = defaultSubspaceLayoutName(),
+ measurePolicy: MeasurePolicy,
+) {
+ ComposeNode<ComposeSubspaceNode, SubspaceNodeApplier>(
+ factory = ComposeSubspaceNode.Constructor,
+ update = {
+ set(measurePolicy, SetMeasurePolicy)
+ set(modifier, SetModifier)
+ set(name, SetName)
+ },
+ )
+}
+
+/**
+ * [SubspaceLayout] is the main core component for layout. It can be used to measure and position
+ * zero or more layout children.
+ *
+ * The measurement, layout and intrinsic measurement behaviours of this layout will be defined by
+ * the [measurePolicy] instance. See [MeasurePolicy] for more details.
+ *
+ * @param modifier SubspaceModifier to apply during layout
+ * @param content the children composable to be laid out.
+ * @param name a name for the ComposeSubspaceNode. This can be useful for debugging.
+ * @param measurePolicy a policy defining the measurement and positioning of the layout.
+ */
+@Suppress("ComposableLambdaParameterPosition")
+@SubspaceComposable
+@Composable
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceLayout(
+ content: @Composable @SubspaceComposable () -> Unit,
+ modifier: SubspaceModifier = SubspaceModifier,
+ name: String = defaultSubspaceLayoutName(),
+ measurePolicy: MeasurePolicy,
+) {
+ ComposeNode<ComposeSubspaceNode, SubspaceNodeApplier>(
+ factory = ComposeSubspaceNode.Constructor,
+ update = {
+ set(measurePolicy, SetMeasurePolicy)
+ set(modifier, SetModifier)
+ set(name, SetName)
+ },
+ content = content,
+ )
+}
+
+/**
+ * [SubspaceLayout] is the main core component for layout for "leaf" nodes. It can be used to
+ * measure and position zero children.
+ *
+ * The measurement, layout and intrinsic measurement behaviours of this layout will be defined by
+ * the [measurePolicy] instance. See [MeasurePolicy] for more details.
+ *
+ * @param modifier SubspaceModifier to apply during layout.
+ * @param coreEntity SceneCore Entity being placed in this layout. This parameter is generally not
+ * needed for most use cases and should be avoided unless you have specific requirements to manage
+ * entities outside the Compose framework. If provided, it will associate the [SubspaceLayout]
+ * with the given SceneCore Entity.
+ * @param name a name for the ComposeSubspaceNode. This can be useful for debugging.
+ * @param measurePolicy a policy defining the measurement and positioning of the layout.
+ */
+@Suppress("NOTHING_TO_INLINE")
+@SubspaceComposable
+@Composable
+internal fun SubspaceLayout(
+ modifier: SubspaceModifier = SubspaceModifier,
+ coreEntity: CoreEntity? = null,
+ name: String = defaultSubspaceLayoutName(),
+ measurePolicy: MeasurePolicy,
+) {
+ ComposeNode<ComposeSubspaceNode, SubspaceNodeApplier>(
+ factory = ComposeSubspaceNode.Constructor,
+ update = {
+ set(measurePolicy, SetMeasurePolicy)
+ set(coreEntity, SetCoreEntity)
+ // Execute SetModifier after SetCoreEntity, it depends on CoreEntity.
+ set(modifier, SetModifier)
+ set(name, SetName)
+ },
+ )
+}
+
+/**
+ * [SubspaceLayout] is the main core component for layout. It can be used to measure and position
+ * zero or more layout children.
+ *
+ * The measurement, layout and intrinsic measurement behaviours of this layout will be defined by
+ * the [measurePolicy] instance. See [MeasurePolicy] for more details.
+ *
+ * @param modifier SubspaceModifier to apply during layout
+ * @param coreEntity SceneCore Entity being placed in this layout. This parameter is generally not
+ * needed for most use cases and should be avoided unless you have specific requirements to manage
+ * entities outside the Compose framework. If provided, it will associate the [SubspaceLayout]
+ * with the given SceneCore Entity.
+ * @param content the children composable to be laid out.
+ * @param name a name for the ComposeSubspaceNode. This can be useful for debugging.
+ * @param measurePolicy a policy defining the measurement and positioning of the layout.
+ */
+@Suppress("ComposableLambdaParameterPosition")
+@SubspaceComposable
+@Composable
+internal fun SubspaceLayout(
+ content: @Composable @SubspaceComposable () -> Unit,
+ modifier: SubspaceModifier = SubspaceModifier,
+ coreEntity: CoreEntity? = null,
+ name: String = defaultSubspaceLayoutName(),
+ measurePolicy: MeasurePolicy,
+) {
+ ComposeNode<ComposeSubspaceNode, SubspaceNodeApplier>(
+ factory = ComposeSubspaceNode.Constructor,
+ update = {
+ set(measurePolicy, SetMeasurePolicy)
+ set(coreEntity, SetCoreEntity)
+ // Execute SetModifier after SetCoreEntity, it depends on CoreEntity.
+ set(modifier, SetModifier)
+ set(name, SetName)
+ },
+ content = content,
+ )
+}
+
+private var subspaceLayoutNamePart: Int = 0
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun defaultSubspaceLayoutName(): String {
+ return "SubspaceLayoutNode-${subspaceLayoutNamePart++}"
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspaceLayoutCoordinates.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspaceLayoutCoordinates.kt
new file mode 100644
index 0000000..098adff
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspaceLayoutCoordinates.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+import androidx.xr.compose.unit.IntVolumeSize
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Vector3
+
+/**
+ * A holder of the measured bounds.
+ *
+ * Based on [androidx.compose.ui.layout.LayoutCoordinates].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface SubspaceLayoutCoordinates {
+ /** The pose of this layout in the local coordinates space, with translation in pixels. */
+ public val pose: Pose
+
+ /**
+ * The pose of this layout relative to the root entity of the Compose hierarchy, with
+ * translation in pixels.
+ */
+ public val poseInRoot: Pose
+
+ /**
+ * The pose of this layout relative to its parent entity in the Compose hierarchy, with
+ * translation in pixels.
+ */
+ public val poseInParentEntity: Pose
+
+ /** The position of the pose. */
+ @Deprecated("Use pose.position instead", ReplaceWith("pose.position"))
+ public val position: Vector3
+ get() = pose.translation
+
+ /**
+ * The position of this layout relative to the root entity of the Compose hierarchy.
+ *
+ * For testing only.
+ */
+ @Deprecated("Use poseInRoot.position instead", ReplaceWith("poseInRoot.position"))
+ public val positionInRoot: Vector3
+ get() = poseInRoot.translation
+
+ /** The position of this layout relative to its parent entity in the Compose hierarchy. */
+ @Deprecated(
+ "Use poseInParentEntity.position instead",
+ ReplaceWith("poseInParentEntity.position")
+ )
+ public val positionInParentEntity: Vector3
+ get() = poseInParentEntity.translation
+
+ /** The rotation of the pose. */
+ @Deprecated("Use pose.rotation instead", ReplaceWith("pose.rotation"))
+ public val rotation: Quaternion
+ get() = pose.rotation
+
+ /**
+ * The rotation of this layout relative to the root entity of the Compose hierarchy.
+ *
+ * For testing only.
+ */
+ @Deprecated("Use poseInRoot.rotation instead", ReplaceWith("poseInRoot.rotation"))
+ public val rotationInRoot: Quaternion
+ get() = poseInRoot.rotation
+
+ /** The rotation of this layout relative to its parent entity in the Compose hierarchy. */
+ @Deprecated(
+ "Use poseInParentEntity.rotation instead",
+ ReplaceWith("poseInParentEntity.rotation")
+ )
+ public val rotationInParentEntity: Quaternion
+ get() = poseInParentEntity.rotation
+
+ /**
+ * The size of this layout in the local coordinates space.
+ *
+ * This is also useful for providing the size of the node to the [OnGloballyPositionedModifier].
+ */
+ public val size: IntVolumeSize
+}
+
+/** Returns information on pose, position and size. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceLayoutCoordinates.toDebugString(): String = buildString {
+ appendLine("pose: $pose")
+ appendLine("poseInParentEntity: $poseInParentEntity")
+ appendLine("size: $size")
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspaceModifier.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspaceModifier.kt
new file mode 100644
index 0000000..e82ec66
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspaceModifier.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+import androidx.xr.compose.subspace.node.SubspaceLayoutModifierNode
+import androidx.xr.compose.subspace.node.SubspaceLayoutModifierNodeCoordinator
+import androidx.xr.compose.subspace.node.SubspaceModifierElement
+
+/**
+ * An ordered, immutable collection of [subspace modifier elements][SubspaceModifierElement] that
+ * decorate or add behavior to Subspace Compose elements.
+ *
+ * Based on [androidx.compose.ui.Modifier]
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface SubspaceModifier {
+
+ /**
+ * Accumulates a value starting with [initial] and applying [operation] to the current value and
+ * each SubspaceModifierElement from outside in.
+ */
+ public fun <R> foldIn(
+ initial: R,
+ operation: (R, SubspaceModifierElement<SubspaceModifier.Node>) -> R,
+ ): R = initial
+
+ /**
+ * Accumulates a value starting with [initial] and applying [operation] to the current value and
+ * each SubspaceModifierElement from inside out.
+ */
+ public fun <R> foldOut(
+ initial: R,
+ operation: (SubspaceModifierElement<SubspaceModifier.Node>, R) -> R,
+ ): R = initial
+
+ /**
+ * Returns `true` if [predicate] returns true for any [SubspaceModifierElement] in this
+ * [SubspaceModifier].
+ */
+ public fun any(
+ predicate: (SubspaceModifierElement<SubspaceModifier.Node>) -> Boolean
+ ): Boolean = false
+
+ /**
+ * Returns `true` if [predicate] returns true for all [SubspaceModifierElement]s in this
+ * [SubspaceModifier] or if this [SubspaceModifier] contains no [Element]s.
+ */
+ public fun all(
+ predicate: (SubspaceModifierElement<SubspaceModifier.Node>) -> Boolean
+ ): Boolean = true
+
+ /**
+ * Concatenates this modifier with another.
+ *
+ * Returns a [SubspaceModifier] representing this modifier followed by [other] in sequence.
+ */
+ public infix fun then(other: SubspaceModifier): SubspaceModifier =
+ if (other === SubspaceModifier) this else CombinedSubspaceModifier(this, other)
+
+ /**
+ * The longer-lived object that is created for each [SubspaceModifierElement] applied to a
+ * [SubspaceLayout]
+ */
+ public abstract class Node {
+ internal var parent: Node? = null
+ internal var child: Node? = null
+ internal val coordinator: SubspaceLayoutModifierNodeCoordinator? = run {
+ if (this is SubspaceLayoutModifierNode) {
+ SubspaceLayoutModifierNodeCoordinator(this)
+ } else {
+ null
+ }
+ }
+ }
+
+ /**
+ * The companion object `SubspaceModifier` is the empty, default, or starter [SubspaceModifier]
+ * that contains no [SubspaceModifierElements][SubspaceModifierElement].
+ */
+ public companion object : SubspaceModifier {
+
+ public infix fun then(
+ other: SubspaceModifierElement<SubspaceModifier.Node>
+ ): SubspaceModifier = other
+
+ override fun toString(): String = "SubspaceModifier"
+ }
+}
+
+/**
+ * A node in a [SubspaceModifier] chain. A CombinedSubspaceModifier always contains at least two
+ * elements; a SubspaceModifier [outer] that wraps around the SubspaceModifier [inner].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class CombinedSubspaceModifier(
+ internal val outer: SubspaceModifier,
+ internal val inner: SubspaceModifier,
+) : SubspaceModifier {
+ override fun <R> foldIn(
+ initial: R,
+ operation: (R, SubspaceModifierElement<SubspaceModifier.Node>) -> R,
+ ): R = inner.foldIn(outer.foldIn(initial, operation), operation)
+
+ override fun <R> foldOut(
+ initial: R,
+ operation: (SubspaceModifierElement<SubspaceModifier.Node>, R) -> R,
+ ): R = outer.foldOut(inner.foldOut(initial, operation), operation)
+
+ override fun any(
+ predicate: (SubspaceModifierElement<SubspaceModifier.Node>) -> Boolean
+ ): Boolean = outer.any(predicate) || inner.any(predicate)
+
+ override fun all(
+ predicate: (SubspaceModifierElement<SubspaceModifier.Node>) -> Boolean
+ ): Boolean = outer.all(predicate) && inner.all(predicate)
+
+ override fun equals(other: Any?): Boolean =
+ other is CombinedSubspaceModifier && outer == other.outer && inner == other.inner
+
+ override fun hashCode(): Int = outer.hashCode() + 31 * inner.hashCode()
+
+ override fun toString(): String =
+ "[" +
+ foldIn("") { acc, element ->
+ if (acc.isEmpty()) element.toString() else "$acc, $element"
+ } +
+ "]"
+}
+
+/**
+ * Generates a lazy sequence that walks up the node tree to the root.
+ *
+ * If this node is the root, an empty sequence is returned.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.Node.traverseAncestors(): Sequence<SubspaceModifier.Node> {
+ return generateSequence(seed = parent) { it.parent }
+}
+
+/** Generates a sequence with self and elements up the node tree to the root. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.Node.traverseSelfThenAncestors(): Sequence<SubspaceModifier.Node> =
+ sequenceOf(this) + traverseAncestors()
+
+/**
+ * Generates a lazy sequence that walks down the node tree.
+ *
+ * If this node is a leaf node, an empty sequence is returned.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.Node.traverseDescendants(): Sequence<SubspaceModifier.Node> {
+ return generateSequence(seed = child) { it.child }
+}
+
+/** Generates a sequence with self and elements down the node tree. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.Node.traverseSelfThenDescendants(): Sequence<SubspaceModifier.Node> =
+ sequenceOf(this) + traverseDescendants()
+
+/** Returns the first element of type [T] in the sequence, or `null` if none match. */
+internal inline fun <reified T> Sequence<*>.findInstance(): T? = firstOrNull { it is T } as T?
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspaceRootMeasurePolicy.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspaceRootMeasurePolicy.kt
new file mode 100644
index 0000000..016915a
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspaceRootMeasurePolicy.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastMap
+import androidx.xr.compose.unit.VolumeConstraints
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Vector3
+
+/**
+ * MeasurePolicy applied at the root of the compose tree.
+ *
+ * Based on [androidx.compose.ui.layout.RootMeasurePolicy].
+ */
+internal class SubspaceRootMeasurePolicy() : MeasurePolicy {
+ override fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: VolumeConstraints,
+ ): MeasureResult {
+ return when {
+ measurables.isEmpty() -> {
+ layout(constraints.minWidth, constraints.minHeight, constraints.minDepth) {}
+ }
+ measurables.size == 1 -> {
+ val placeable = measurables[0].measure(constraints)
+ layout(placeable.measuredWidth, placeable.measuredHeight, placeable.measuredDepth) {
+ placeable.place(Pose(Vector3.Zero, Quaternion.Identity))
+ }
+ }
+ else -> {
+ val placeables = measurables.fastMap { it.measure(constraints) }
+ var maxWidth = 0
+ var maxHeight = 0
+ var maxDepth = 0
+ placeables.fastForEach { placeable ->
+ maxWidth = maxOf(placeable.measuredWidth, maxWidth)
+ maxHeight = maxOf(placeable.measuredHeight, maxHeight)
+ maxDepth = maxOf(placeable.measuredDepth, maxDepth)
+ }
+ layout(maxWidth, maxHeight, maxDepth) {
+ placeables.fastForEach { placeable ->
+ placeable.place(Pose(Vector3.Zero, Quaternion.Identity))
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/TestTag.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/TestTag.kt
new file mode 100644
index 0000000..b4664fe
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/TestTag.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.annotation.RestrictTo
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
+import androidx.compose.ui.semantics.testTag
+import androidx.xr.compose.subspace.node.SubspaceModifierElement
+import androidx.xr.compose.subspace.node.SubspaceSemanticsModifierNode
+
+/**
+ * Applies a tag to allow modified element to be found in tests.
+ *
+ * This is a convenience method for a [semantics] that sets [SemanticsPropertyReceiver.testTag].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun SubspaceModifier.testTag(tag: String): SubspaceModifier = this then TestTagElement(tag)
+
+private class TestTagElement(private val tag: String) : SubspaceModifierElement<TestTagNode>() {
+ override fun create(): TestTagNode {
+ return TestTagNode(tag)
+ }
+
+ override fun update(node: TestTagNode) {
+ node.tag = tag
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is TestTagElement) return false
+ return tag == other.tag
+ }
+
+ override fun hashCode(): Int {
+ return tag.hashCode()
+ }
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class TestTagNode(public var tag: String) :
+ SubspaceModifier.Node(), SubspaceSemanticsModifierNode {
+ override fun SemanticsPropertyReceiver.applySemantics() {
+ testTag = tag
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/ComposeSubspaceNode.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/ComposeSubspaceNode.kt
new file mode 100644
index 0000000..e3bf376
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/ComposeSubspaceNode.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.node
+
+import androidx.xr.compose.subspace.layout.CoreEntity
+import androidx.xr.compose.subspace.layout.MeasurePolicy
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+
+/**
+ * Represents a Composable Subspace node in the Compose hierarchy.
+ *
+ * This interface is inspired by [androidx.compose.ui.node.ComposeUiNode].
+ */
+internal interface ComposeSubspaceNode {
+
+ /** The [MeasurePolicy] used to define the measure and layout behavior of this node. */
+ public var measurePolicy: MeasurePolicy
+
+ /** The [SubspaceModifier] applied to this node. */
+ public var modifier: SubspaceModifier
+
+ /** The optional [CoreEntity] associated with this node. */
+ public var coreEntity: CoreEntity?
+
+ /** An optional name for this node, useful for debugging and identification purposes. */
+ public var name: String?
+
+ public companion object {
+ /**
+ * Constructor function for creating a new [ComposeSubspaceNode].
+ *
+ * @return an instance of a [ComposeSubspaceNode].
+ */
+ public val Constructor: () -> ComposeSubspaceNode = SubspaceLayoutNode.Constructor
+
+ /**
+ * Sets the [MeasurePolicy] for the given [ComposeSubspaceNode].
+ *
+ * @param measurePolicy the [MeasurePolicy] to be applied.
+ */
+ public val SetMeasurePolicy: ComposeSubspaceNode.(MeasurePolicy) -> Unit = {
+ this.measurePolicy = it
+ }
+
+ /**
+ * Sets the [CoreEntity] for the given [ComposeSubspaceNode].
+ *
+ * @param coreEntity the [CoreEntity] to be associated, or null.
+ */
+ public val SetCoreEntity: ComposeSubspaceNode.(CoreEntity?) -> Unit = {
+ this.coreEntity = it
+ }
+
+ /**
+ * Sets the [SubspaceModifier] for the given [ComposeSubspaceNode].
+ *
+ * Note: [SetCoreEntity] should be called before.
+ *
+ * @param modifier the [SubspaceModifier] to be applied.
+ */
+ public val SetModifier: ComposeSubspaceNode.(SubspaceModifier) -> Unit = {
+ this.modifier = it
+ }
+
+ /**
+ * Sets the name for the given [ComposeSubspaceNode].
+ *
+ * @param name the name to be assigned.
+ */
+ public val SetName: ComposeSubspaceNode.(String) -> Unit = { this.name = it }
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutModifierNode.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutModifierNode.kt
new file mode 100644
index 0000000..9e34c00
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutModifierNode.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.node
+
+import androidx.annotation.RestrictTo
+import androidx.xr.compose.subspace.layout.Measurable
+import androidx.xr.compose.subspace.layout.MeasureResult
+import androidx.xr.compose.subspace.layout.MeasureScope
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.unit.VolumeConstraints
+
+/**
+ * A specialized [SubspaceModifier.Node] responsible for modifying the measurement and layout
+ * behavior of its wrapped content within the Subspace environment.
+ *
+ * Based on [androidx.compose.ui.node.LayoutModifierNode].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface SubspaceLayoutModifierNode {
+
+ /**
+ * Defines the measurement and layout of the [Measurable] within the given [MeasureScope].
+ *
+ * The measurable is subject to the specified [VolumeConstraints].
+ *
+ * @param measurable the content to be measured.
+ * @param constraints the constraints within which the measurement should occur.
+ * @return a [MeasureResult] encapsulating the size and alignment lines of the measured layout.
+ */
+ public fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: VolumeConstraints,
+ ): MeasureResult
+}
+
+/**
+ * Returns the [SubspaceLayoutModifierNodeCoordinator] associated with this
+ * [SubspaceLayoutModifierNode].
+ *
+ * This is used to traverse the modifier node tree to find the correct [SubspaceLayoutCoordinates]
+ * for a given [SubspaceLayoutModifierNode].
+ */
+internal val SubspaceLayoutModifierNode.coordinator: SubspaceLayoutModifierNodeCoordinator
+ get() {
+ check(this is SubspaceModifier.Node && this.coordinator != null) {
+ "SubspaceLayoutModifierNode must be a SubspaceModifier.Node and have a non-null coordinator."
+ }
+ return (this as SubspaceModifier.Node).coordinator!!
+ }
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutModifierNodeCoordinator.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutModifierNodeCoordinator.kt
new file mode 100644
index 0000000..b017ab8
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutModifierNodeCoordinator.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.node
+
+import androidx.xr.compose.subspace.layout.Measurable
+import androidx.xr.compose.subspace.layout.MeasureResult
+import androidx.xr.compose.subspace.layout.MeasureScope
+import androidx.xr.compose.subspace.layout.ParentLayoutParamsAdjustable
+import androidx.xr.compose.subspace.layout.Placeable
+import androidx.xr.compose.subspace.layout.SubspaceLayoutCoordinates
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.unit.IntVolumeSize
+import androidx.xr.compose.unit.VolumeConstraints
+import androidx.xr.runtime.math.Pose
+
+/**
+ * A [Measurable] and [Placeable] object that is used to measure and lay out the children of a
+ * [SubspaceLayoutModifierNode].
+ *
+ * See [androidx.compose.ui.node.NodeCoordinator]
+ *
+ * The [SubspaceLayoutModifierNodeCoordinator] is mapped 1:1 with a [SubspaceLayoutModifierNode] so
+ * we don't need to maintain a separate node coordinator hierarchy. Instead, we can use the
+ * hierarchy already present in [SubspaceModifier.Node] types.
+ */
+internal class SubspaceLayoutModifierNodeCoordinator(
+ private val layoutModifierNode: SubspaceLayoutModifierNode
+) : SubspaceLayoutCoordinates, Measurable {
+
+ private val baseNode: SubspaceModifier.Node
+ get() = layoutModifierNode as SubspaceModifier.Node
+
+ internal var layoutNode: SubspaceLayoutNode? = null
+
+ internal val parent: SubspaceLayoutModifierNodeCoordinator?
+ get() =
+ generateSequence(baseNode.parent) { it.parent }.firstNotNullOfOrNull { it.coordinator }
+
+ internal val child: SubspaceLayoutModifierNodeCoordinator?
+ get() =
+ generateSequence(baseNode.child) { it.child }.firstNotNullOfOrNull { it.coordinator }
+
+ /** The pose of this layout in the local coordinates space. */
+ override val pose: Pose
+ get() = layoutPose ?: Pose.Identity
+
+ /**
+ * The pose of this layout modifier node relative to the root entity of the Compose hierarchy.
+ */
+ override val poseInRoot: Pose
+ get() =
+ coordinatesInRoot?.poseInRoot?.let {
+ pose.translate(it.translation).rotate(it.rotation)
+ } ?: pose
+
+ /**
+ * The pose of this layout modifier node relative to its parent entity in the Compose hierarchy.
+ */
+ override val poseInParentEntity: Pose
+ get() =
+ coordinatesInParentEntity?.poseInParentEntity?.let {
+ pose.translate(it.translation).rotate(it.rotation)
+ } ?: pose
+
+ /**
+ * The layout coordinates of the parent [SubspaceLayoutNode] up to the root of the hierarchy
+ * including application from any [SubspaceLayoutModifierNode] instances applied to this node.
+ *
+ * This applies the layout changes of all [SubspaceLayoutModifierNode] instances in the modifier
+ * chain and then [layoutNode]'s parent or just [layoutNode]'s parent and this modifier if no
+ * other [SubspaceLayoutModifierNode] is present.
+ */
+ private val coordinatesInRoot: SubspaceLayoutCoordinates?
+ get() = parent ?: layoutNode?.measurableLayout?.parentCoordinatesInRoot
+
+ /**
+ * The layout coordinates up to the nearest parent [CoreEntity] including mutations from any
+ * [SubspaceLayoutModifierNode] instances applied to this node.
+ *
+ * This applies the layout changes of all [SubspaceLayoutModifierNode] instances in the modifier
+ * chain and then [layoutNode]'s parent or just [layoutNode]'s parent and this modifier if no
+ * other [SubspaceLayoutModifierNode] is present.
+ */
+ private val coordinatesInParentEntity: SubspaceLayoutCoordinates?
+ get() = parent ?: layoutNode?.measurableLayout?.parentCoordinatesInParentEntity
+
+ /** The size of this layout in the local coordinates space. */
+ override val size: IntVolumeSize
+ get() =
+ IntVolumeSize(
+ width = placeable.measuredWidth,
+ height = placeable.measuredHeight,
+ depth = placeable.measuredDepth,
+ )
+
+ private var measureResult: MeasureResult? = null
+ private var layoutPose: Pose? = null
+
+ /**
+ * The [Placeable] representing the placed content of this modifier. It handles placing child
+ * content based on the layout pose.
+ */
+ public var placeable: Placeable =
+ object : Placeable() {
+ public override fun placeAt(pose: Pose) {
+ layoutPose = pose
+ measureResult?.placeChildren(
+ object : PlacementScope() {
+ public override val coordinates = this@SubspaceLayoutModifierNodeCoordinator
+ }
+ )
+ }
+ }
+
+ /**
+ * Measures the wrapped content within the given [constraints].
+ *
+ * @param constraints the constraints to apply during measurement.
+ * @return the [Placeable] representing the measured child layout that can be positioned by its
+ * parent layout.
+ */
+ override fun measure(constraints: VolumeConstraints): Placeable {
+ with(layoutModifierNode) {
+ val measurable: Measurable = child ?: layoutNode!!.measurableLayout
+ val measureResult: MeasureResult =
+ object : MeasureScope {}.measure(measurable, constraints).also {
+ [email protected] = it
+ }
+ return placeable.apply {
+ measuredWidth = measureResult.width
+ measuredHeight = measureResult.height
+ measuredDepth = measureResult.depth
+ }
+ }
+ }
+
+ /**
+ * Adjusts layout of the wrapped content with a new [ParentLayoutParamsAdjustable].
+ *
+ * @param params the parameters to be adjusted.
+ */
+ override fun adjustParams(params: ParentLayoutParamsAdjustable) {
+ layoutNode?.measurableLayout?.adjustParams(params)
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutNode.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutNode.kt
new file mode 100644
index 0000000..ebfdb55
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutNode.kt
@@ -0,0 +1,423 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.node
+
+import androidx.xr.compose.subspace.layout.CoreEntity
+import androidx.xr.compose.subspace.layout.Measurable
+import androidx.xr.compose.subspace.layout.MeasurePolicy
+import androidx.xr.compose.subspace.layout.MeasureResult
+import androidx.xr.compose.subspace.layout.MeasureScope
+import androidx.xr.compose.subspace.layout.OnGloballyPositionedNode
+import androidx.xr.compose.subspace.layout.ParentLayoutParamsAdjustable
+import androidx.xr.compose.subspace.layout.ParentLayoutParamsModifier
+import androidx.xr.compose.subspace.layout.Placeable
+import androidx.xr.compose.subspace.layout.SubspaceLayoutCoordinates
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.layout.SubspaceRootMeasurePolicy
+import androidx.xr.compose.unit.IntVolumeSize
+import androidx.xr.compose.unit.VolumeConstraints
+import androidx.xr.runtime.math.Pose
+import java.util.concurrent.atomic.AtomicInteger
+
+private var lastIdentifier = AtomicInteger(0)
+
+internal fun generateSemanticsId() = lastIdentifier.incrementAndGet()
+
+/**
+ * An element in the Subspace layout hierarchy (spatial scene graph), built with Compose UI for
+ * subspace.
+ *
+ * This class is based on [androidx.compose.ui.node.LayoutNode].
+ *
+ * TODO(b/330925589): Write unit tests.
+ * TODO(b/333904965): Make this class 'internal'.
+ */
+internal class SubspaceLayoutNode : ComposeSubspaceNode {
+ /**
+ * The children of this [SubspaceLayoutNode], controlled by [insertAt], [move], and [removeAt].
+ */
+ internal val children: MutableList<SubspaceLayoutNode> = mutableListOf()
+
+ /** The parent node in the [SubspaceLayoutNode] hierarchy. */
+ internal var parent: SubspaceLayoutNode? = null
+
+ /** Unique ID used by semantics libraries. */
+ public val semanticsId: Int = generateSemanticsId()
+
+ /** Instance of [MeasurableLayout] to aid with measure/layout phases. */
+ public val measurableLayout: MeasurableLayout = MeasurableLayout()
+
+ /** The element system [SubspaceOwner]. This value is `null` until [attach] is called */
+ internal var owner: SubspaceOwner? = null
+ private set
+
+ internal val nodes: SubspaceModifierNodeChain = SubspaceModifierNodeChain(this)
+
+ override var measurePolicy: MeasurePolicy = ErrorMeasurePolicy
+
+ override var modifier: SubspaceModifier = SubspaceModifier
+ set(value) {
+ field = value
+ nodes.updateFrom(value)
+ updateCoreEntity()
+ }
+
+ override var coreEntity: CoreEntity? = null
+ set(value) {
+ check(field == null) { "overwriting non-null CoreEntity is not supported" }
+ field = value
+ if (value != null) {
+ value.layout = measurableLayout
+ }
+ }
+
+ override var name: String? = null
+
+ private fun updateCoreEntity() {
+ coreEntity?.applyModifiers(nodes.getAll())
+ }
+
+ /**
+ * This function sets up CoreEntity parent/child relationships that reflect the parent/child
+ * relationships of the corresponding SubspaceLayoutNodes. This should be called any time the
+ * `parent` or `coreEntity` fields are updated.
+ */
+ private fun syncCoreEntityHierarchy() {
+ coreEntity?.parent = findCoreEntityParent(this)
+ }
+
+ /** Inserts a child [SubspaceLayoutNode] at the given [index]. */
+ internal fun insertAt(index: Int, instance: SubspaceLayoutNode) {
+ check(instance.parent == null) {
+ "Cannot insert $instance because it already has a parent." +
+ " This tree: " +
+ debugTreeToString() +
+ " Parent tree: " +
+ parent?.debugTreeToString()
+ }
+ check(instance.owner == null) {
+ "Cannot insert $instance because it already has an owner." +
+ " This tree: " +
+ debugTreeToString() +
+ " Other tree: " +
+ instance.debugTreeToString()
+ }
+
+ instance.parent = this
+ children.add(index, instance)
+
+ owner?.let { instance.attach(it) }
+ }
+
+ /**
+ * Moves [count] elements starting at index [from] to index [to].
+ *
+ * The [to] index is related to the position before the change, so, for example, to move an
+ * element at position 1 to after the element at position 2, [from] should be `1` and [to]
+ * should be `3`. If the elements were [SubspaceLayoutNode] instances, A B C D E, calling
+ * `move(1, 3, 1)` would result in the nodes being reordered to A C B D E.
+ */
+ internal fun move(from: Int, to: Int, count: Int) {
+ if (from == to) {
+ return // nothing to do
+ }
+
+ for (i in 0 until count) {
+ // if "from" is after "to," the from index moves because we're inserting before it
+ val fromIndex = if (from > to) from + i else from
+ val toIndex = if (from > to) to + i else to + count - 2
+ val child = children.removeAt(fromIndex)
+
+ children.add(toIndex, child)
+ }
+ }
+
+ /** Removes one or more children, starting at [index]. */
+ internal fun removeAt(index: Int, count: Int) {
+ require(count >= 0) { "count ($count) must be greater than 0." }
+
+ for (i in index + count - 1 downTo index) {
+ onChildRemoved(children[i])
+ }
+
+ children.removeAll(children.subList(index, index + count))
+ }
+
+ /** Removes all children nodes. */
+ internal fun removeAll() {
+ children.reversed().forEach { child -> onChildRemoved(child) }
+
+ children.clear()
+ }
+
+ /** Called when the [child] node is removed from this [SubspaceLayoutNode] hierarchy. */
+ private fun onChildRemoved(child: SubspaceLayoutNode) {
+ owner?.let { child.detach() }
+ child.parent = null
+ }
+
+ /**
+ * Sets the [SubspaceOwner] of this node.
+ *
+ * This SubspaceLayoutNode must not already be attached and [subspaceOwner] must match the
+ * [parent]'s [subspaceOwner].
+ */
+ internal fun attach(subspaceOwner: SubspaceOwner) {
+ check(this.owner == null) {
+ "Cannot attach $this as it already is attached. Tree: " + debugTreeToString()
+ }
+ check(parent == null || parent?.owner == subspaceOwner) {
+ "Attaching to a different owner($subspaceOwner) than the parent's owner" +
+ "(${parent?.owner})." +
+ " This tree: " +
+ debugTreeToString() +
+ " Parent tree: " +
+ parent?.debugTreeToString()
+ }
+
+ this.owner = subspaceOwner
+
+ subspaceOwner.onAttach(this)
+ syncCoreEntityHierarchy()
+
+ children.forEach { child -> child.attach(subspaceOwner) }
+ }
+
+ /**
+ * Detaches this node from the [owner].
+ *
+ * The [owner] must not be `null` when this method is called.
+ *
+ * This will also [detach] all children. After executing, the [owner] will be `null`.
+ */
+ internal fun detach() {
+ val owner = owner
+
+ checkNotNull(owner) {
+ "Cannot detach node that is already detached! Tree: " + parent?.debugTreeToString()
+ }
+
+ children.forEach { child -> child.detach() }
+
+ owner.onDetach(this)
+ this.owner = null
+ }
+
+ override fun toString(): String {
+ return name ?: super.toString()
+ }
+
+ /** Call this method to see a dump of the SpatialLayoutNode tree structure. */
+ @Suppress("unused")
+ internal fun debugTreeToString(depth: Int = 0): String {
+ val tree = StringBuilder()
+ val depthString = " ".repeat(depth)
+ tree.append("$depthString|-${toString()}\n")
+
+ var currentNode: SubspaceModifier.Node? = nodes.head
+ while (currentNode != null && currentNode != nodes.tail) {
+ tree.append("$depthString *-$currentNode\n")
+ currentNode = currentNode.child
+ }
+
+ children.forEach { child -> tree.append(child.debugTreeToString(depth + 1)) }
+
+ var treeString = tree.toString()
+ if (depth == 0) {
+ // Delete trailing newline
+ treeString = treeString.substring(0, treeString.length - 1)
+ }
+
+ return treeString
+ }
+
+ /** Call this method to see a dump of the Jetpack XR node hierarchy. */
+ @Suppress("unused")
+ internal fun debugEntityTreeToString(depth: Int = 0): String {
+ val tree = StringBuilder()
+ val depthString = " ".repeat(depth)
+ var nextDepth = depth
+ if (coreEntity != null) {
+ tree.append(
+ "$depthString|-${coreEntity?.entity} -> ${findCoreEntityParent(this)?.entity}\n"
+ )
+ nextDepth++
+ }
+
+ children.forEach { child -> tree.append(child.debugEntityTreeToString(nextDepth)) }
+
+ var treeString = tree.toString()
+ if (depth == 0 && treeString.isNotEmpty()) {
+ // Delete trailing newline
+ treeString = treeString.substring(0, treeString.length - 1)
+ }
+
+ return treeString
+ }
+
+ /**
+ * A [Measurable] and [Placeable] object that is used to measure and lay out the children of
+ * this node.
+ *
+ * See [androidx.compose.ui.node.NodeCoordinator]
+ */
+ public inner class MeasurableLayout : Measurable, SubspaceLayoutCoordinates, Placeable() {
+ private var layoutPose: Pose? = null
+ private var measureResult: MeasureResult? = null
+
+ /**
+ * The tail node of [SubspaceModifierNodeChain].
+ *
+ * This node is used to mark the end of the modifier chain.
+ */
+ public val tail: SubspaceModifier.Node = TailModifierNode()
+
+ override fun measure(constraints: VolumeConstraints): Placeable =
+ nodes.measureChain(constraints, ::measureJustThis)
+
+ override fun adjustParams(params: ParentLayoutParamsAdjustable) {
+ nodes.getAll<ParentLayoutParamsModifier>().forEach { it.adjustParams(params) }
+ }
+
+ private fun measureJustThis(constraints: VolumeConstraints): Placeable {
+ measureResult =
+ with(measurePolicy) {
+ object : MeasureScope {}.measure(
+ [email protected] { it.measurableLayout }.toList(),
+ constraints,
+ )
+ }
+
+ measuredWidth = measureResult!!.width
+ measuredHeight = measureResult!!.height
+ measuredDepth = measureResult!!.depth
+
+ coreEntity?.size = IntVolumeSize(measuredWidth, measuredHeight, measuredDepth)
+
+ return this
+ }
+
+ /**
+ * Places the children of this node at the given pose.
+ *
+ * @param pose The pose to place the children at, with translation in pixels.
+ */
+ public override fun placeAt(pose: Pose) {
+ layoutPose = pose
+
+ coreEntity?.applyLayoutChanges()
+
+ measureResult?.placeChildren(
+ object : PlacementScope() {
+ override val coordinates = this@MeasurableLayout
+ }
+ )
+
+ // Call OnGloballyPositioned callbacks after the node and its children are placed.
+ nodes.getAll<OnGloballyPositionedNode>().forEach { it.callback(this) }
+ }
+
+ override val pose: Pose
+ get() = layoutPose ?: Pose.Identity
+
+ override val poseInRoot: Pose
+ get() =
+ coordinatesInRoot?.poseInRoot?.let {
+ pose.translate(it.translation).rotate(it.rotation)
+ } ?: pose
+
+ override val poseInParentEntity: Pose
+ get() =
+ coordinatesInParentEntity?.poseInParentEntity?.let {
+ pose.translate(it.translation).rotate(it.rotation)
+ } ?: pose
+
+ /**
+ * The layout coordinates of the parent [SubspaceLayoutNode] up to the root of the hierarchy
+ * including application from any [SubspaceLayoutModifierNode] instances applied to this
+ * node.
+ *
+ * This applies the layout changes of all [SubspaceLayoutModifierNode] instances in the
+ * modifier chain and then [parentCoordinatesInRoot] or just [parentCoordinatesInRoot] if no
+ * [SubspaceLayoutModifierNode] is present.
+ */
+ private val coordinatesInRoot: SubspaceLayoutCoordinates?
+ get() =
+ coreEntity
+ ?: nodes.getLast<SubspaceLayoutModifierNode>()?.coordinator
+ ?: parentCoordinatesInRoot
+
+ /** Traverse the parent hierarchy up to the root. */
+ internal val parentCoordinatesInRoot: SubspaceLayoutCoordinates?
+ get() = parent?.measurableLayout
+
+ /**
+ * The layout coordinates up to the nearest parent [CoreEntity] including mutations from any
+ * [SubspaceLayoutModifierNode] instances applied to this node.
+ *
+ * This applies the layout changes of all [SubspaceLayoutModifierNode] instances in the
+ * modifier chain and then [parentCoordinatesInParentEntity] or just
+ * [parentCoordinatesInParentEntity] if no [SubspaceLayoutModifierNode] is present.
+ */
+ private val coordinatesInParentEntity: SubspaceLayoutCoordinates?
+ get() =
+ coreEntity
+ ?: nodes.getLast<SubspaceLayoutModifierNode>()?.coordinator
+ ?: parentCoordinatesInParentEntity
+
+ /** Traverse up the parent hierarchy until we reach a node with an entity. */
+ internal val parentCoordinatesInParentEntity: SubspaceLayoutCoordinates?
+ get() = if (parent?.coreEntity == null) parent?.measurableLayout else null
+
+ override val size: IntVolumeSize
+ get() {
+ return coreEntity?.size
+ ?: IntVolumeSize(measuredWidth, measuredHeight, measuredDepth)
+ }
+
+ override fun toString(): String {
+ return [email protected]()
+ }
+ }
+
+ /** Companion object for [SubspaceLayoutNode]. */
+ public companion object {
+ private val ErrorMeasurePolicy: MeasurePolicy = MeasurePolicy { _, _ ->
+ error("Undefined measure and it is required")
+ }
+
+ /**
+ * A [MeasurePolicy] that is used for the root node of the Subspace layout hierarchy.
+ *
+ * Note: Root node itself has no size outside its children.
+ */
+ public val RootMeasurePolicy: MeasurePolicy = SubspaceRootMeasurePolicy()
+
+ /** A constructor that creates a new [SubspaceLayoutNode]. */
+ public val Constructor: () -> SubspaceLayoutNode = { SubspaceLayoutNode() }
+
+ /** Walk up the parent hierarchy to find the closest ancestor attached to a [CoreEntity]. */
+ private fun findCoreEntityParent(node: SubspaceLayoutNode) =
+ generateSequence(node.parent) { it.parent }.firstNotNullOfOrNull { it.coreEntity }
+ }
+}
+
+internal class TailModifierNode : SubspaceModifier.Node() {
+ override fun toString(): String {
+ return "<tail>"
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceModifierElement.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceModifierElement.kt
new file mode 100644
index 0000000..7beba95
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceModifierElement.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.node
+
+import androidx.annotation.RestrictTo
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+
+/**
+ * An abstract class for [SubspaceModifier] that creates and updates [SubspaceModifier.Node]
+ * instances.
+ *
+ * @param N The type of node that this element creates and updates.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public abstract class SubspaceModifierElement<N : SubspaceModifier.Node> : SubspaceModifier {
+ /**
+ * Creates a new node of type [SubspaceModifier.Node].
+ *
+ * @return A new node of type [SubspaceModifier.Node].
+ */
+ public abstract fun create(): N
+
+ /**
+ * Updates the given [SubspaceModifier.Node] with the current state of this element.
+ *
+ * @param node The [SubspaceModifier.Node] to update.
+ */
+ public abstract fun update(node: N)
+
+ /**
+ * Returns a hash code value for this element.
+ *
+ * @return A hash code value for this element.
+ */
+ public abstract override fun hashCode(): Int
+
+ /**
+ * Indicates whether some other object is "equal to" this one.
+ *
+ * @param other The other object to compare to.
+ * @return `true` if this object is the same as the [other] argument; `false` otherwise.
+ */
+ public abstract override fun equals(other: Any?): Boolean
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceModifierNodeChain.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceModifierNodeChain.kt
new file mode 100644
index 0000000..c8099ce
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceModifierNodeChain.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.node
+
+import androidx.xr.compose.subspace.layout.CombinedSubspaceModifier
+import androidx.xr.compose.subspace.layout.Placeable
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.layout.findInstance
+import androidx.xr.compose.subspace.layout.traverseSelfThenAncestors
+import androidx.xr.compose.subspace.layout.traverseSelfThenDescendants
+import androidx.xr.compose.unit.VolumeConstraints
+
+private val SentinelHead =
+ object : SubspaceModifier.Node() {
+ override fun toString() = "<Head>"
+ }
+
+/** See [androidx.compose.ui.node.NodeChain] */
+internal class SubspaceModifierNodeChain(private val subspaceLayoutNode: SubspaceLayoutNode) {
+ private var current: MutableList<SubspaceModifier>? = null
+ private var buffer: MutableList<SubspaceModifier>? = null
+ internal val tail: SubspaceModifier.Node = subspaceLayoutNode.measurableLayout.tail
+ internal var head: SubspaceModifier.Node = tail
+ private var inMeasurePass: Boolean = false
+
+ private fun padChain(): SubspaceModifier.Node {
+ val currentHead = head
+ currentHead.parent = SentinelHead
+ SentinelHead.child = currentHead
+ return SentinelHead
+ }
+
+ private fun trimChain(): SubspaceModifier.Node {
+ val result = SentinelHead.child ?: tail
+ result.parent = null
+ SentinelHead.child = null
+ return result
+ }
+
+ internal fun updateFrom(modifier: SubspaceModifier) {
+ val paddedHead = padChain()
+ val before = current
+ val beforeSize = before?.size ?: 0
+ val after = modifier.fillVector(buffer ?: mutableListOf())
+ var i = 0
+
+ if (beforeSize == 0) {
+ // Common case where we are initializing the chain or the previous size is zero.
+ var node = paddedHead
+ while (i < after.size) {
+ val next = after[i]
+ val parent = node
+ node = createAndInsertNodeAsChild(next, parent)
+ i++
+ }
+ } else if (after.size == 0) {
+ // Common case where we are removing all the modifiers.
+ var node = paddedHead.child
+ while (node != null && i < beforeSize) {
+ node = removeNode(node).child
+ i++
+ }
+ } else {
+ // Find the diffs between before and after sets. This is not as complex as base Compose
+ // which
+ // does a full diff. Revisit this if we see any performance issues with dynamic
+ // modifiers.
+ var node = paddedHead
+
+ // First match as many identical modifiers at the beginning of the lists.
+ checkNotNull(before) { "prior modifier list should be non-empty" }
+ while (i < beforeSize && i < after.size && before[i] == after[i]) {
+ node = checkNotNull(node.child) { "child should not be null" }
+ i++
+ }
+
+ // Then remove the remaining existing modifiers.
+ var nodeToDelete = node.child
+ var beforeIndex = i
+ while (nodeToDelete != null && beforeIndex < beforeSize) {
+ nodeToDelete = removeNode(nodeToDelete).child
+ beforeIndex++
+ }
+
+ // Finally add the remaining new modifiers.
+ while (i < after.size) {
+ val next = after[i]
+ val parent = node
+ node = createAndInsertNodeAsChild(next, parent)
+ i++
+ }
+ }
+
+ current = after
+ // Clear the before vector to allow old modifiers to be Garbage Collected.
+ buffer = before?.also { it.clear() }
+ head = trimChain()
+ }
+
+ internal fun measureChain(
+ constraints: VolumeConstraints,
+ wrappedMeasureBlock: (VolumeConstraints) -> Placeable,
+ ): Placeable {
+ val layoutNode = getAll<SubspaceLayoutModifierNode>().firstOrNull()
+ if (layoutNode == null || inMeasurePass) {
+ inMeasurePass = false
+ return wrappedMeasureBlock(constraints)
+ }
+ inMeasurePass = true
+ val placeable = layoutNode.coordinator.measure(constraints)
+ inMeasurePass = false
+ return placeable
+ }
+
+ /** Returns all nodes of the given type in the chain in the declared modifier order. */
+ internal inline fun <reified T> getAll(): Sequence<T> =
+ head.traverseSelfThenDescendants().filterIsInstance<T>()
+
+ /**
+ * Returns the last node of the given type in the chain if it exists, null otherwise.
+ *
+ * When considering only one instance of a modifier type, prefer the last instance.
+ */
+ internal inline fun <reified T> getLast(): T? =
+ tail.traverseSelfThenAncestors().findInstance<T>()
+
+ private fun createAndInsertNodeAsChild(
+ element: SubspaceModifier,
+ parent: SubspaceModifier.Node,
+ ): SubspaceModifier.Node {
+ val node = (element as SubspaceModifierElement<*>).create()
+ if (node is SubspaceLayoutModifierNode) {
+ node.coordinator?.layoutNode = subspaceLayoutNode
+ }
+ return insertChild(node, parent)
+ }
+
+ private fun insertChild(
+ node: SubspaceModifier.Node,
+ parent: SubspaceModifier.Node,
+ ): SubspaceModifier.Node {
+ val theChild = parent.child
+ if (theChild != null) {
+ theChild.parent = node
+ node.child = theChild
+ }
+ parent.child = node
+ node.parent = parent
+ return node
+ }
+
+ private fun removeNode(node: SubspaceModifier.Node): SubspaceModifier.Node {
+ val child = node.child
+ val parent = node.parent
+ if (child != null) {
+ child.parent = parent
+ node.child = null
+ }
+ if (parent != null) {
+ parent.child = child
+ node.parent = null
+ }
+ return parent!!
+ }
+}
+
+private fun SubspaceModifier.fillVector(
+ result: MutableList<SubspaceModifier>
+): MutableList<SubspaceModifier> {
+ val capacity = result.size.coerceAtLeast(16)
+ val stack = ArrayList<SubspaceModifier>(capacity).also { it.add(this) }
+ var predicate: ((SubspaceModifier) -> Boolean)? = null
+ while (stack.isNotEmpty()) {
+ when (val next = stack.removeAt(stack.size - 1)) {
+ is CombinedSubspaceModifier -> {
+ stack.add(next.inner)
+ stack.add(next.outer)
+ }
+ is SubspaceModifierElement<*> -> result.add(next as SubspaceModifier)
+
+ // some other [androidx.compose.ui.node.Modifier] implementation that we don't know
+ // about...
+ // late-allocate the predicate only once for the entire stack
+ else ->
+ next.all(
+ predicate
+ ?: { element: SubspaceModifier ->
+ result.add(element)
+ true
+ }
+ .also { predicate = it }
+ )
+ }
+ }
+ return result
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceNodeApplier.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceNodeApplier.kt
new file mode 100644
index 0000000..eff0483
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceNodeApplier.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.node
+
+import androidx.compose.runtime.AbstractApplier
+import androidx.xr.compose.platform.Logger
+import androidx.xr.compose.unit.VolumeConstraints
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Vector3
+
+/**
+ * Node applier for subspace compositions.
+ *
+ * See [androidx.compose.ui.node.UiApplier]
+ */
+internal class SubspaceNodeApplier(root: SubspaceLayoutNode) :
+ AbstractApplier<SubspaceLayoutNode>(root) {
+
+ init {
+ root.measurePolicy = SubspaceLayoutNode.RootMeasurePolicy
+ }
+
+ override fun insertTopDown(index: Int, instance: SubspaceLayoutNode) {
+ // Ignored. Insert is performed in [insertBottomUp] to build the tree bottom-up to avoid
+ // duplicate notification when the child nodes enter the tree.
+ }
+
+ override fun insertBottomUp(index: Int, instance: SubspaceLayoutNode) {
+ current.insertAt(index, instance)
+ }
+
+ override fun remove(index: Int, count: Int) {
+ current.removeAt(index, count)
+ }
+
+ override fun move(from: Int, to: Int, count: Int) {
+ current.move(from, to, count)
+ }
+
+ override fun onClear() {
+ root.removeAll()
+ }
+
+ override fun onEndChanges() {
+ val measureResults =
+ root.measurableLayout.measure(
+ VolumeConstraints(0, VolumeConstraints.INFINITY, 0, VolumeConstraints.INFINITY)
+ )
+
+ (measureResults as SubspaceLayoutNode.MeasurableLayout).placeAt(
+ Pose(Vector3.Zero, Quaternion.Identity)
+ )
+ Logger.log("SubspaceNodeApplier") { root.debugTreeToString() }
+ Logger.log("SubspaceNodeApplier") { root.debugEntityTreeToString() }
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceOwner.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceOwner.kt
new file mode 100644
index 0000000..22e5385
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceOwner.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.node
+
+/**
+ * Owner interface that defines the connection to the underlying element system.
+ *
+ * On Android, this connects to Android [elements][androidx.xr.subspace.Element] and all layout,
+ * draw, input, and accessibility is hooked through them.
+ *
+ * See [androidx.compose.ui.node.Owner]
+ */
+internal interface SubspaceOwner {
+ /** The root layout node in the component tree. */
+ public val root: SubspaceLayoutNode
+
+ /**
+ * Called by [SubspaceLayoutNode] when the node is attached to this owner's element system.
+ *
+ * This is used by [SubspaceOwner] to track which nodes are associated with it. It is only
+ * called by a [node] that just got attached to this owner.
+ */
+ public fun onAttach(node: SubspaceLayoutNode)
+
+ /**
+ * Called by [SubspaceLayoutNode] when it is detached from the element system (for example
+ * during [SubspaceLayoutNode.removeAt]).
+ *
+ * @param node the node that is being detached from this owner's element system.
+ */
+ public fun onDetach(node: SubspaceLayoutNode)
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceSemanticsModifierNode.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceSemanticsModifierNode.kt
new file mode 100644
index 0000000..6ab3360
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceSemanticsModifierNode.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.node
+
+import androidx.annotation.RestrictTo
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
+
+/**
+ * A [SubspaceModifier.Node] that adds semantics key/values for use in testing, accessibility, and
+ * similar use cases.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface SubspaceSemanticsModifierNode {
+ /**
+ * Adds semantics key/value pairs to the layout node, for use in testing, accessibility, etc.
+ *
+ * The [SemanticsPropertyReceiver] provides "key = value"-style setters for any
+ * [SemanticsPropertyKey]. Also, chaining multiple semantics modifiers is supported.
+ */
+ public fun SemanticsPropertyReceiver.applySemantics()
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceSemanticsNode.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceSemanticsNode.kt
new file mode 100644
index 0000000..12f58a1
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceSemanticsNode.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.node
+
+import androidx.annotation.RestrictTo
+import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.xr.compose.subspace.layout.CoreEntity
+import androidx.xr.compose.unit.IntVolumeSize
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Vector3
+import androidx.xr.scenecore.Component
+
+/**
+ * A list of key/value pairs associated with a layout node or its subtree.
+ *
+ * Each SubspaceSemanticsNode takes its id and initial key/value list from the outermost modifier on
+ * one layout node. It also contains the "collapsed" configuration of any other semantics modifiers
+ * on the same layout node.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SubspaceSemanticsNode
+internal constructor(private val layoutNode: SubspaceLayoutNode) {
+ /** The unique ID of this semantics node. */
+ public val id: Int = layoutNode.semanticsId
+
+ /** The size of the bounding box for this node. */
+ public val size: IntVolumeSize
+ get() = layoutNode.measurableLayout.size
+
+ /**
+ * The position of this node relative to its parent layout node in the Compose hierarchy, in
+ * pixels.
+ */
+ public val position: Vector3
+ get() = layoutNode.measurableLayout.pose.translation
+
+ /** The position of this node relative to the root of this Compose hierarchy, in pixels. */
+ public val positionInRoot: Vector3
+ get() = layoutNode.measurableLayout.poseInRoot.translation
+
+ /** The rotation of this node relative to its parent layout node in the Compose hierarchy. */
+ public val rotation: Quaternion
+ get() = layoutNode.measurableLayout.pose.rotation
+
+ /** The rotation of this node relative to the root of this Compose hierarchy. */
+ public val rotationInRoot: Quaternion
+ get() = layoutNode.measurableLayout.poseInRoot.rotation
+
+ /** The components attached to this node by SubspaceLayoutNode update. */
+ @get:Suppress("NullableCollection")
+ public val components: List<Component>?
+ get() = layoutNode.coreEntity?.entity?.getComponents()
+
+ /** The scale factor of this node relative to its parent. */
+ public val scale: Float
+ get() = layoutNode.coreEntity?.entity?.getScale() ?: 1.0f
+
+ /** The CoreEntity attached to this node. */
+ internal val coreEntity: CoreEntity?
+ get() = layoutNode.coreEntity
+
+ /**
+ * The semantics configuration of this node.
+ *
+ * This includes all properties attached as modifiers to the current layout node.
+ */
+ public val config: SemanticsConfiguration
+ get() {
+ val config = SemanticsConfiguration()
+ layoutNode.nodes.getAll<SubspaceSemanticsModifierNode>().forEach {
+ with(config) { with(it) { applySemantics() } }
+ }
+ return config
+ }
+
+ /**
+ * The children of this node in the semantics tree.
+ *
+ * The children are ordered in inverse hit test order (i.e., paint order).
+ */
+ public val children: List<SubspaceSemanticsNode>
+ get() {
+ val list: MutableList<SubspaceSemanticsNode> = mutableListOf()
+ layoutNode.fillOneLayerOfSemanticsWrappers(list)
+ return list
+ }
+
+ /** Whether this node is the root of a semantics tree. */
+ public val isRoot: Boolean
+ get() = parent == null
+
+ /** The parent of this node in the semantics tree. */
+ public val parent: SubspaceSemanticsNode?
+ get() {
+ var node: SubspaceLayoutNode? = layoutNode.parent
+ while (node != null) {
+ if (node.hasSemantics) return SubspaceSemanticsNode(node)
+ node = node.parent
+ }
+ return null
+ }
+
+ private fun SubspaceLayoutNode.fillOneLayerOfSemanticsWrappers(
+ list: MutableList<SubspaceSemanticsNode>
+ ) {
+ children.forEach { child ->
+ if (child.hasSemantics) {
+ list.add(SubspaceSemanticsNode(child))
+ } else {
+ child.fillOneLayerOfSemanticsWrappers(list)
+ }
+ }
+ }
+
+ private val SubspaceLayoutNode.hasSemantics: Boolean
+ get() = nodes.getLast<SubspaceSemanticsModifierNode>() != null
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/CoreConversions.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/CoreConversions.kt
new file mode 100644
index 0000000..5bce510
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/CoreConversions.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.unit
+
+import androidx.compose.ui.unit.Density
+import androidx.xr.scenecore.Dimensions
+
+/**
+ * Converts this [IntVolumeSize] to a [Dimensions] object in meters, taking into account [density].
+ *
+ * @return a [Dimensions] object representing the volume size in meters.
+ */
+internal fun IntVolumeSize.toDimensionsInMeters(density: Density): Dimensions =
+ Dimensions(
+ Meter.fromPixel(width.toFloat(), density).value,
+ Meter.fromPixel(height.toFloat(), density).value,
+ Meter.fromPixel(depth.toFloat(), density).value,
+ )
+
+/**
+ * Creates an [IntVolumeSize] from a [Dimensions] object in meters.
+ *
+ * The dimensions in meters are rounded to the nearest pixel value.
+ *
+ * @param density The pixel density of the display.
+ * @return an [IntVolumeSize] object representing the same volume size in pixels.
+ */
+internal fun Dimensions.toIntVolumeSize(density: Density): IntVolumeSize =
+ IntVolumeSize(
+ Meter(this.width).roundToPx(density),
+ Meter(this.height).roundToPx(density),
+ Meter(this.depth).roundToPx(density),
+ )
+
+/**
+ * Converts this [DpVolumeSize] to a [Dimensions] object in meters.
+ *
+ * @return a [Dimensions] object representing the volume size in meters
+ */
+internal fun DpVolumeSize.toDimensionsInMeters(): Dimensions =
+ Dimensions(width.toMeter().value, height.toMeter().value, depth.toMeter().value)
+
+/**
+ * Creates a [DpVolumeSize] from a [Dimensions] object in meters.
+ *
+ * @return a [DpVolumeSize] object representing the same volume size in Dp.
+ */
+internal fun Dimensions.toDpVolumeSize(): DpVolumeSize =
+ DpVolumeSize(Meter(width).toDp(), Meter(height).toDp(), Meter(depth).toDp())
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/DpVolumeSize.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/DpVolumeSize.kt
new file mode 100644
index 0000000..8f027cb
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/DpVolumeSize.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.unit
+
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/**
+ * Represents the size of a volume in density-independent pixels ([Dp]).
+ *
+ * This class provides a convenient way to store and manipulate the [width], [height], and [depth]
+ * of a 3D volume in Dp. It also provides methods to convert to and from [Dimensions] in meters.
+ *
+ * @property width the size of the volume along the x dimension, in Dp.
+ * @property height the size of the volume along the y dimension, in Dp.
+ * @property depth the size of the volume along the z dimension, in Dp. Panels have 0 depth and
+ * cannot be set to non-zero depth.
+ */
+public class DpVolumeSize(public val width: Dp, public val height: Dp, public val depth: Dp) {
+
+ override fun toString(): String {
+ return "DpVolumeSize(width=$width, height=$height, depth=$depth)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is DpVolumeSize) return false
+
+ if (width != other.width) return false
+ if (height != other.height) return false
+ if (depth != other.depth) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = width.hashCode()
+ result = 31 * result + height.hashCode()
+ result = 31 * result + depth.hashCode()
+ return result
+ }
+
+ /** Contains a common constant */
+ public companion object {
+ /** A [DpVolumeSize] with all dimensions set to 0.dp. */
+ public val Zero: DpVolumeSize = DpVolumeSize(0.dp, 0.dp, 0.dp)
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/IntVolumeSize.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/IntVolumeSize.kt
new file mode 100644
index 0000000..f4a0641
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/IntVolumeSize.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.unit
+
+import androidx.xr.scenecore.Dimensions
+
+/**
+ * Represents the size of a volume in pixels.
+ *
+ * This class provides a convenient way to store and manipulate the [width], [height], and [depth]
+ * of a 3D volume in pixels. It also provides methods to convert to and from [Dimensions] in meters.
+ *
+ * Note: As with all [Int] values in Compose XR, the values in this class represent pixels.
+ *
+ * @property width the size of the volume along the x dimension, in pixels.
+ * @property height the size of the volume along the y dimension, in pixels.
+ * @property depth the size of the volume along the z dimension, in pixels. Panels have 0 depth and
+ * cannot be set to non-zero depth.
+ */
+public class IntVolumeSize(public val width: Int, public val height: Int, public val depth: Int) {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is IntVolumeSize) return false
+
+ if (width != other.width) return false
+ if (height != other.height) return false
+ if (depth != other.depth) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = width
+ result = 31 * result + height
+ result = 31 * result + depth
+ return result
+ }
+
+ override fun toString(): String {
+ return "IntVolumeSize(width=$width, height=$height, depth=$depth)"
+ }
+
+ /** Contains common constants and factory methods for creating [IntVolumeSize] objects */
+ public companion object {
+ /** An [IntVolumeSize] with all dimensions set to 0. */
+ public val Zero: IntVolumeSize = IntVolumeSize(0, 0, 0)
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/Meter.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/Meter.kt
new file mode 100644
index 0000000..008ea95
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/Meter.kt
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.xr.compose.unit
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.isFinite
+import androidx.compose.ui.unit.isSpecified
+import androidx.xr.extensions.XrExtensions
+import androidx.xr.extensions.XrExtensionsProvider
+import kotlin.math.roundToInt
+
+/**
+ * Represents a dimension value in meters within 3D space.
+ *
+ * This is the standard unit used by the system for representing size and distance in 3D
+ * environments.
+ */
+@Immutable
+@JvmInline
+public value class Meter(public val value: Float) : Comparable<Meter> {
+
+ /**
+ * Adds another [Meter] value to this one.
+ *
+ * @param other the [Meter] value to add.
+ * @return a new [Meter] representing the sum of the two values.
+ */
+ public inline operator fun plus(other: Meter): Meter = Meter(value + other.value)
+
+ /**
+ * Subtracts another [Meter] value from this one.
+ *
+ * @param other the [Meter] value to subtract.
+ * @return a new [Meter] representing the difference between the two values.
+ */
+ public inline operator fun minus(other: Meter): Meter = Meter(value - other.value)
+
+ /**
+ * Multiplies this [Meter] value by an [Int] factor.
+ *
+ * @param other the integer factor to multiply by.
+ * @return a new [Meter] representing the product.
+ */
+ public inline operator fun times(other: Int): Meter = Meter(value * other)
+
+ /**
+ * Multiplies this [Meter] value by a [Float] factor.
+ *
+ * @param other the float factor to multiply by.
+ * @return a new [Meter] representing the product.
+ */
+ public inline operator fun times(other: Float): Meter = Meter(value * other)
+
+ /**
+ * Multiplies this [Meter] value by a [Double] factor.
+ *
+ * @param other the double factor to multiply by.
+ * @return a new [Meter] representing the product.
+ */
+ public inline operator fun times(other: Double): Meter = Meter(value * other.toFloat())
+
+ /**
+ * Divides this [Meter] value by an [Int] factor.
+ *
+ * @param other the [Int] factor to divide by.
+ * @return a new [Meter] representing the quotient.
+ */
+ public inline operator fun div(other: Int): Meter = Meter(value / other)
+
+ /**
+ * Divides this [Meter] value by a [Float] factor.
+ *
+ * @param other the [Float] factor to divide by.
+ * @return a new [Meter] representing the quotient.
+ */
+ public inline operator fun div(other: Float): Meter = Meter(value / other)
+
+ /**
+ * Divides this [Meter] value by a [Double] factor.
+ *
+ * @param other the [Double] factor to divide by.
+ * @return a new [Meter] representing the quotient.
+ */
+ public inline operator fun div(other: Double): Meter = Meter(value / other.toFloat())
+
+ /**
+ * Converts this [Meter] value to [Float] millimeters.
+ *
+ * @return the equivalent value in millimeters as a [Float].
+ */
+ public inline fun toMm(): Float = value * 1000f
+
+ /**
+ * Converts this [Meter] value to [Float] centimeters.
+ *
+ * @return the equivalent value in centimeters as a [Float].
+ */
+ public inline fun toCm(): Float = value * 100f
+
+ /**
+ * Converts this [Meter] value to [Float] meters.
+ *
+ * @return the equivalent value in meters as a [Float].
+ */
+ public inline fun toM(): Float = value
+
+ /**
+ * Converts this [Meter] value to an approximate number of pixels it contains.
+ *
+ * @return the approximate equivalent value in pixels as a [Float].
+ */
+ public inline fun toPx(density: Density): Float {
+ with(density) {
+ return toDp().toPx()
+ }
+ }
+
+ /**
+ * Converts this [Meter] value to the nearest [Int] number of pixels, taking into account
+ * [density].
+ *
+ * @return the rounded equivalent value in pixels as an [Int].
+ */
+ public inline fun roundToPx(density: Density): Int {
+ return toPx(density).roundToInt()
+ }
+
+ /**
+ * Converts this [Meter] value to the [Dp] number of density-independent pixels it contains.
+ *
+ * @return the equivalent value in [Dp].
+ */
+ public inline fun toDp(): Dp {
+ return (toM() * DP_PER_METER).dp
+ }
+
+ /**
+ * Checks if this [Meter] value is specified (i.e., not NaN).
+ *
+ * @return `true` if the value is specified, `false` otherwise.
+ */
+ public inline val isSpecified: Boolean
+ get() = !value.isNaN()
+
+ /**
+ * Checks if this [Meter] value is finite.
+ *
+ * @return `true` if the value is finite, `false` when [Meter.Infinity].
+ */
+ public inline val isFinite: Boolean
+ get() = value != Float.POSITIVE_INFINITY
+
+ /**
+ * Compares this [Meter] value to [other].
+ *
+ * @param other The other [Meter] value to compare to.
+ * @return a negative value if this [Meter] is less than [other], a positive value if it's
+ * greater, or 0 if they are equal.
+ */
+ override fun compareTo(other: Meter): Int {
+ return value.compareTo(other.value)
+ }
+
+ public companion object {
+ /**
+ * If we can't look up the DPs per meter from the system, we will use this value. This value
+ * was measured on the current Android XR device and will need to be updated if the device's
+ * config changes.
+ */
+ private const val DP_PER_METER_FALLBACK: Float = 1151.856f
+
+ /**
+ * DPs per meter. The system's API is in pixels, but we can get the value we want be
+ * specifying 1 dp == 1 pixel.
+ */
+ @PublishedApi
+ internal val DP_PER_METER: Float =
+ tryGetXrExtensions()?.config?.defaultPixelsPerMeter(1.0f) ?: DP_PER_METER_FALLBACK
+
+ /** Represents an infinite distance in meters. */
+ public val Infinity: Meter = Meter(Float.POSITIVE_INFINITY)
+
+ /** Represents an undefined or unrepresentable distance in meters. */
+ public val NaN: Meter = Meter(Float.NaN)
+
+ /**
+ * Attempts to retrieve an instance of [XrExtensions].
+ *
+ * @return an instance of [XrExtensions] if available, or [null] otherwise.
+ */
+ private fun tryGetXrExtensions(): XrExtensions? =
+ try {
+ XrExtensionsProvider.getXrExtensions()
+ } catch (e: Exception) {
+ null
+ } catch (e: Error) {
+ null
+ }
+
+ /**
+ * Creates a [Meter] value from a given number of pixels.
+ *
+ * @param px the number of pixels.
+ * @param density The pixel density of the display.
+ * @return a [Meter] value representing the equivalent distance in meters.
+ */
+ public inline fun fromPixel(px: Float, density: Density): Meter {
+ with(density) {
+ // We do the conversion inline instead of calling Dp.toMeter(), which will check its
+ // inputs.
+ // We know if the input is an integer pixel, we won't have any exceptional Dp
+ // values, e.g.,
+ // Dp.Infinity.
+ return Meter(px.toDp().value / DP_PER_METER)
+ }
+ }
+
+ // Primitive conversion functions for creating [Meter] values from various units.
+
+ /** Creates a [Meter] from the [Int] millimeter value. */
+ public val Int.millimeters: Meter
+ get() = Meter(this.toFloat() * 0.001f)
+
+ /** Creates a [Meter] from the [Float] millimeter value. */
+ public val Float.millimeters: Meter
+ get() = Meter(this * 0.001f)
+
+ /** Creates a [Meter] from the [Double] millimeter value. */
+ public val Double.millimeters: Meter
+ get() = Meter(this.toFloat() * 0.001f)
+
+ /** Creates a [Meter] from the [Int] centimeter value. */
+ public val Int.centimeters: Meter
+ get() = Meter(this * 0.01f)
+
+ /** Creates a [Meter] from the [Float] centimeter value. */
+ public val Float.centimeters: Meter
+ get() = Meter(this * 0.01f)
+
+ /** Creates a [Meter] from the [Double] centimeter value. */
+ public val Double.centimeters: Meter
+ get() = Meter(this.toFloat() * 0.01f)
+
+ /** Creates a [Meter] from the [Int] meter value. */
+ public val Int.meters: Meter
+ get() = Meter(this.toFloat())
+
+ /** Creates a [Meter] from the [Float] meter value. */
+ public val Float.meters: Meter
+ get() = Meter(this)
+
+ /** Creates a [Meter] from the [Double] meter value. */
+ public val Double.meters: Meter
+ get() = Meter(this.toFloat())
+ }
+}
+
+/**
+ * Converts a [Dp] value to [Meter].
+ *
+ * Handles unspecified and infinite [Dp] values gracefully.
+ *
+ * @return the equivalent value in meters, or [Meter.NaN] if the [Dp] is unspecified, or
+ * [Meter.Infinity] if the [Dp] is infinite.
+ */
+public inline fun Dp.toMeter(): Meter {
+ if (!isSpecified) {
+ return Meter.NaN
+ }
+ if (!isFinite) {
+ return Meter.Infinity
+ }
+ return Meter(this.value / Meter.DP_PER_METER)
+}
+
+// Operator functions for performing arithmetic operations between numeric types and Meter
+
+public inline operator fun Int.times(other: Meter): Meter = Meter(this * other.value)
+
+public inline operator fun Float.times(other: Meter): Meter = Meter(this * other.value)
+
+public inline operator fun Double.times(other: Meter): Meter = Meter(this.toFloat() * other.value)
+
+public inline operator fun Int.div(other: Meter): Meter = Meter(this / other.value)
+
+public inline operator fun Float.div(other: Meter): Meter = Meter(this / other.value)
+
+public inline operator fun Double.div(other: Meter): Meter = Meter(this.toFloat() / other.value)
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/VolumeConstraints.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/VolumeConstraints.kt
new file mode 100644
index 0000000..50d8427
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/VolumeConstraints.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.unit
+
+/**
+ * Defines constraints for a 3D volume, specifying minimum and maximum values for width, height, and
+ * depth.
+ *
+ * This class is similar in concept to [androidx.compose.ui.unit.Constraints], but adapted for 3D
+ * volumes.
+ *
+ * @property minWidth the minimum allowed width in pixels.
+ * @property maxWidth the maximum allowed width in pixels, use [INFINITY] to indicate no maximum.
+ * @property minHeight the minimum allowed height in pixels.
+ * @property maxHeight the maximum allowed height in pixels. Can be [INFINITY] to indicate no
+ * maximum.
+ * @property minDepth the minimum allowed depth in pixels. Defaults to 0.
+ * @property maxDepth the maximum allowed depth in pixels. Can be [INFINITY] to indicate no maximum.
+ * Defaults to [INFINITY].
+ */
+public class VolumeConstraints(
+ public val minWidth: Int,
+ public val maxWidth: Int,
+ public val minHeight: Int,
+ public val maxHeight: Int,
+ public val minDepth: Int = 0,
+ public val maxDepth: Int = INFINITY,
+) {
+
+ /** Indicates whether the width is bounded (has a maximum value other than [INFINITY]). */
+ @get:JvmName("hasBoundedWidth")
+ public val hasBoundedWidth: Boolean
+ get() {
+ return maxWidth != INFINITY
+ }
+
+ /** Indicates whether the height is bounded (has a maximum value other than [INFINITY]). */
+ @get:JvmName("hasBoundedHeight")
+ public val hasBoundedHeight: Boolean
+ get() {
+ return maxHeight != INFINITY
+ }
+
+ /** Indicates whether the depth is bounded (has a maximum value other than [INFINITY]). */
+ @get:JvmName("hasBoundedDepth")
+ public val hasBoundedDepth: Boolean
+ get() {
+ return maxDepth != INFINITY
+ }
+
+ /** Returns a string representation of the [VolumeConstraints]. */
+ override fun toString(): String =
+ "width: $minWidth-$maxWidth, height: $minHeight-$maxHeight, depth=$minDepth-$maxDepth"
+
+ /** Checks if this [VolumeConstraints] object is equal to [other] object. */
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is VolumeConstraints) return false
+
+ if (minWidth != other.minWidth) return false
+ if (maxWidth != other.maxWidth) return false
+ if (minHeight != other.minHeight) return false
+ if (maxHeight != other.maxHeight) return false
+ if (minDepth != other.minDepth) return false
+ if (maxDepth != other.maxDepth) return false
+
+ return true
+ }
+
+ /** Calculates a hash code for this [VolumeConstraints] object. */
+ override fun hashCode(): Int {
+ var result = minWidth
+ result = 31 * result + maxWidth
+ result = 31 * result + minHeight
+ result = 31 * result + maxHeight
+ result = 31 * result + minDepth
+ result = 31 * result + maxDepth
+ return result
+ }
+
+ /**
+ * Creates a copy of this [VolumeConstraints] object with modifications to its properties.
+ *
+ * @param minWidth the minimum allowed width in the new constraints.
+ * @param maxWidth the maximum allowed width in the new constraints.
+ * @param minHeight the minimum allowed height in the new constraints.
+ * @param maxHeight the maximum allowed height in the new constraints.
+ * @param minDepth the minimum allowed depth in the new constraints.
+ * @param maxDepth the maximum allowed depth in the new constraints.
+ * @return a new [VolumeConstraints] object with the specified modifications.
+ */
+ public fun copy(
+ minWidth: Int = this.minWidth,
+ maxWidth: Int = this.maxWidth,
+ minHeight: Int = this.minHeight,
+ maxHeight: Int = this.maxHeight,
+ minDepth: Int = this.minDepth,
+ maxDepth: Int = this.maxDepth,
+ ): VolumeConstraints =
+ VolumeConstraints(
+ minWidth = minWidth,
+ maxWidth = maxWidth,
+ minHeight = minHeight,
+ maxHeight = maxHeight,
+ minDepth = minDepth,
+ maxDepth = maxDepth,
+ )
+
+ public companion object {
+ /** Represents an unbounded (infinite) constraint value. */
+ public const val INFINITY: Int = Int.MAX_VALUE
+ }
+}
+
+/**
+ * Constrains the dimensions of this [VolumeConstraints] object to fit within the bounds of the
+ * other [VolumeConstraints] object.
+ *
+ * @param otherConstraints the other [VolumeConstraints] to constrain against.
+ * @return a new [VolumeConstraints] object with dimensions constrained within the bounds of
+ * [otherConstraints].
+ */
+public fun VolumeConstraints.constrain(otherConstraints: VolumeConstraints): VolumeConstraints =
+ VolumeConstraints(
+ minWidth = otherConstraints.minWidth.coerceIn(minWidth, maxWidth),
+ maxWidth = otherConstraints.maxWidth.coerceIn(minWidth, maxWidth),
+ minHeight = otherConstraints.minHeight.coerceIn(minHeight, maxHeight),
+ maxHeight = otherConstraints.maxHeight.coerceIn(minHeight, maxHeight),
+ minDepth = otherConstraints.minDepth.coerceIn(minDepth, maxDepth),
+ maxDepth = otherConstraints.maxDepth.coerceIn(minDepth, maxDepth),
+ )
+
+/**
+ * Constrains a given [width] value to fit within the bounds of this [VolumeConstraints] object.
+ *
+ * @param width the width value to constrain.
+ * @return the constrained width value, ensuring it's within the minimum and maximum width bounds.
+ */
+public fun VolumeConstraints.constrainWidth(width: Int): Int = width.coerceIn(minWidth, maxWidth)
+
+/**
+ * Constrains a given height value to fit within the bounds of this [VolumeConstraints] object.
+ *
+ * @param height the height value to constrain.
+ * @return the constrained height value, ensuring it's within the minimum and maximum height bounds.
+ */
+public fun VolumeConstraints.constrainHeight(height: Int): Int =
+ height.coerceIn(minHeight, maxHeight)
+
+/**
+ * Constrains a given depth value to fit within the bounds of this [VolumeConstraints] object.
+ *
+ * @param depth the depth value to constrain.
+ * @return the constrained depth value, ensuring it's within the minimum and maximum depth bounds.
+ */
+public fun VolumeConstraints.constrainDepth(depth: Int): Int = depth.coerceIn(minDepth, maxDepth)
+
+/**
+ * Creates a new [VolumeConstraints] object by offsetting the minimum and maximum values of this
+ * one.
+ *
+ * @param horizontal the horizontal offset to apply.
+ * @param vertical the vertical offset to apply.
+ * @param depth the depth offset to apply.
+ * @param resetMins if true, the minimum values in the new constraints will be set to 0, otherwise.
+ * they will be offset.
+ * @return a new [VolumeConstraints] object with offset values.
+ */
+public fun VolumeConstraints.offset(
+ horizontal: Int = 0,
+ vertical: Int = 0,
+ depth: Int = 0,
+ resetMins: Boolean = false,
+): VolumeConstraints =
+ VolumeConstraints(
+ if (resetMins) 0 else (minWidth + horizontal).coerceAtLeast(0),
+ addMaxWithMinimum(maxWidth, horizontal),
+ if (resetMins) 0 else (minHeight + vertical).coerceAtLeast(0),
+ addMaxWithMinimum(maxHeight, vertical),
+ if (resetMins) 0 else (minDepth + depth).coerceAtLeast(0),
+ addMaxWithMinimum(maxDepth, depth),
+ )
+
+/** Adds a value to a maximum value, ensuring it stays within the minimum bound. */
+private fun addMaxWithMinimum(max: Int, value: Int): Int {
+ return if (max == VolumeConstraints.INFINITY) {
+ max
+ } else {
+ (max + value).coerceAtLeast(0)
+ }
+}
diff --git a/xr/compose/compose/src/test/AndroidManifest.xml b/xr/compose/compose/src/test/AndroidManifest.xml
new file mode 100644
index 0000000..665ec06
--- /dev/null
+++ b/xr/compose/compose/src/test/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2024 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
+
+ http://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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <application>
+ <activity android:name="androidx.xr.compose.testing.SubspaceTestingActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/platform/SpatialCapabilitiesTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/platform/SpatialCapabilitiesTest.kt
new file mode 100644
index 0000000..62fd4bb
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/platform/SpatialCapabilitiesTest.kt
@@ -0,0 +1,316 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import androidx.compose.material3.Text
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.testing.SubspaceTestingActivity
+import androidx.xr.compose.testing.TestSetup
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SpatialCapabilitiesTest {
+
+ @get:Rule val composeTestRule = createAndroidComposeRule<SubspaceTestingActivity>()
+
+ @Test
+ fun isSpatialUiEnabled_xrNotEnabled_returnsFalse() {
+ composeTestRule.setContent {
+ TestSetup(isXrEnabled = false) {
+ Text(text = "${LocalSpatialCapabilities.current.isSpatialUiEnabled}")
+ }
+ }
+
+ composeTestRule.onNodeWithText("${false}").assertExists()
+ }
+
+ @Test
+ fun isContent3dEnabled_xrNotEnabled_returnsFalse() {
+ composeTestRule.setContent {
+ TestSetup(isXrEnabled = false) {
+ Text(text = "${LocalSpatialCapabilities.current.isContent3dEnabled}")
+ }
+ }
+
+ composeTestRule.onNodeWithText("${false}").assertExists()
+ }
+
+ @Test
+ fun isAppEnvironmentEnabled_xrNotEnabled_returnsFalse() {
+ composeTestRule.setContent {
+ TestSetup(isXrEnabled = false) {
+ Text(text = "${LocalSpatialCapabilities.current.isAppEnvironmentEnabled}")
+ }
+ }
+
+ composeTestRule.onNodeWithText("${false}").assertExists()
+ }
+
+ @Test
+ fun isPassthroughControlEnabled_xrNotEnabled_returnsFalse() {
+ composeTestRule.setContent {
+ TestSetup(isXrEnabled = false) {
+ Text(text = "${LocalSpatialCapabilities.current.isPassthroughControlEnabled}")
+ }
+ }
+
+ composeTestRule.onNodeWithText("${false}").assertExists()
+ }
+
+ @Test
+ fun isSpatialAudioEnabled_xrNotEnabled_returnsFalse() {
+ composeTestRule.setContent {
+ TestSetup(isXrEnabled = false) {
+ Text(text = "${LocalSpatialCapabilities.current.isSpatialAudioEnabled}")
+ }
+ }
+
+ composeTestRule.onNodeWithText("${false}").assertExists()
+ }
+
+ @Test
+ fun isSpatialUiEnabled_fullSpaceMode_returnsTrue() {
+ composeTestRule.setContent {
+ TestSetup { Text(text = "${LocalSpatialCapabilities.current.isSpatialUiEnabled}") }
+ }
+
+ composeTestRule.onNodeWithText("${true}").assertExists()
+ }
+
+ @Test
+ fun isContent3dEnabled_fullSpaceMode_returnsTrue() {
+ composeTestRule.setContent {
+ TestSetup { Text(text = "${LocalSpatialCapabilities.current.isContent3dEnabled}") }
+ }
+
+ composeTestRule.onNodeWithText("${true}").assertExists()
+ }
+
+ @Test
+ fun isAppEnvironmentEnabled_fullSpaceMode_returnsTrue() {
+ composeTestRule.setContent {
+ TestSetup { Text(text = "${LocalSpatialCapabilities.current.isAppEnvironmentEnabled}") }
+ }
+
+ composeTestRule.onNodeWithText("${true}").assertExists()
+ }
+
+ @Test
+ fun isPassthroughControlEnabled_fullSpaceMode_returnsTrue() {
+ composeTestRule.setContent {
+ TestSetup {
+ Text(text = "${LocalSpatialCapabilities.current.isPassthroughControlEnabled}")
+ }
+ }
+
+ composeTestRule.onNodeWithText("${true}").assertExists()
+ }
+
+ @Test
+ fun isSpatialAudioEnabled_fullSpaceMode_returnsTrue() {
+ composeTestRule.setContent {
+ TestSetup { Text(text = "${LocalSpatialCapabilities.current.isSpatialAudioEnabled}") }
+ }
+
+ composeTestRule.onNodeWithText("${true}").assertExists()
+ }
+
+ @Test
+ fun isSpatialUiEnabled_homeSpaceMode_returnsFalse() {
+ composeTestRule.setContent {
+ TestSetup(isFullSpace = false) {
+ Text("${LocalSpatialCapabilities.current.isSpatialUiEnabled}")
+ }
+ }
+
+ composeTestRule.onNodeWithText("${false}").assertExists()
+ }
+
+ @Test
+ fun isSpatialUiEnabled_homeSpaceMode_requestFullSpaceMode_returnsTrue() {
+ composeTestRule.setContent {
+ TestSetup(isFullSpace = false) {
+ Text("${LocalSpatialCapabilities.current.isSpatialUiEnabled}")
+ requestFullSpaceMode()
+ }
+ }
+
+ composeTestRule.onNodeWithText("${true}").assertExists()
+ }
+
+ @Test
+ fun isSpatialUiEnabled_fullSpaceMode_requestHomeSpaceMode_returnsFalse() {
+ composeTestRule.setContent {
+ TestSetup(isFullSpace = true) {
+ Text("${LocalSpatialCapabilities.current.isSpatialUiEnabled}")
+ requestHomeSpaceMode()
+ }
+ }
+
+ composeTestRule.onNodeWithText("${false}").assertExists()
+ }
+
+ @Test
+ fun isContent3dEnabled_homeSpaceMode_returnsFalse() {
+ composeTestRule.setContent {
+ TestSetup {
+ requestHomeSpaceMode()
+ Text("${LocalSpatialCapabilities.current.isContent3dEnabled}")
+ }
+ }
+
+ composeTestRule.onNodeWithText("${false}").assertExists()
+ }
+
+ @Test
+ fun isContent3dEnabled_homeSpaceMode_requestFullSpaceMode_returnsTrue() {
+ composeTestRule.setContent {
+ TestSetup(isFullSpace = false) {
+ Text("${LocalSpatialCapabilities.current.isContent3dEnabled}")
+ requestFullSpaceMode()
+ }
+ }
+
+ composeTestRule.onNodeWithText("${true}").assertExists()
+ }
+
+ @Test
+ fun isContent3dEnabled_fullSpaceMode_requestHomeSpaceMode_returnsFalse() {
+ composeTestRule.setContent {
+ TestSetup(isFullSpace = true) {
+ Text("${LocalSpatialCapabilities.current.isContent3dEnabled}")
+ requestHomeSpaceMode()
+ }
+ }
+
+ composeTestRule.onNodeWithText("${false}").assertExists()
+ }
+
+ @Test
+ fun isAppEnvironmentEnabled_homeSpaceMode_returnsFalse() {
+ composeTestRule.setContent {
+ TestSetup {
+ requestHomeSpaceMode()
+ Text(text = "${LocalSpatialCapabilities.current.isAppEnvironmentEnabled}")
+ }
+ }
+
+ composeTestRule.onNodeWithText("${false}").assertExists()
+ }
+
+ @Test
+ fun isAppEnvironmentEnabled_homeSpaceMode_requestFullSpaceMode_returnsTrue() {
+ composeTestRule.setContent {
+ TestSetup(isFullSpace = false) {
+ Text(text = "${LocalSpatialCapabilities.current.isAppEnvironmentEnabled}")
+ requestFullSpaceMode()
+ }
+ }
+
+ composeTestRule.onNodeWithText("${true}").assertExists()
+ }
+
+ @Test
+ fun isAppEnvironmentEnabled_fullSpaceMode_requestHomeSpaceMode_returnsFalse() {
+ composeTestRule.setContent {
+ TestSetup(isFullSpace = true) {
+ Text(text = "${LocalSpatialCapabilities.current.isAppEnvironmentEnabled}")
+ requestHomeSpaceMode()
+ }
+ }
+
+ composeTestRule.onNodeWithText("${false}").assertExists()
+ }
+
+ @Test
+ fun isPassthroughControlEnabled_homeSpaceMode_returnsFalse() {
+ composeTestRule.setContent {
+ TestSetup {
+ requestHomeSpaceMode()
+ Text(text = "${LocalSpatialCapabilities.current.isPassthroughControlEnabled}")
+ }
+ }
+
+ composeTestRule.onNodeWithText("${false}").assertExists()
+ }
+
+ @Test
+ fun isPassthroughControlEnabled_homeSpaceMode_requestFullSpaceMode_returnsTrue() {
+ composeTestRule.setContent {
+ TestSetup(isFullSpace = false) {
+ Text(text = "${LocalSpatialCapabilities.current.isPassthroughControlEnabled}")
+ requestFullSpaceMode()
+ }
+ }
+
+ composeTestRule.onNodeWithText("${true}").assertExists()
+ }
+
+ @Test
+ fun isPassthroughControlEnabled_fullSpaceMode_requestHomeSpaceMode_returnsFalse() {
+ composeTestRule.setContent {
+ TestSetup(isFullSpace = true) {
+ Text(text = "${LocalSpatialCapabilities.current.isPassthroughControlEnabled}")
+ requestHomeSpaceMode()
+ }
+ }
+
+ composeTestRule.onNodeWithText("${false}").assertExists()
+ }
+
+ @Test
+ fun isSpatialAudioEnabled_homeSpaceMode_returnsFalse() {
+ composeTestRule.setContent {
+ TestSetup {
+ requestHomeSpaceMode()
+ Text(text = "${LocalSpatialCapabilities.current.isSpatialAudioEnabled}")
+ }
+ }
+
+ composeTestRule.onNodeWithText("${false}").assertExists()
+ }
+
+ @Test
+ fun isSpatialAudioEnabled_homeSpaceMode_requestFullSpaceMode_returnsTrue() {
+ composeTestRule.setContent {
+ TestSetup(isFullSpace = false) {
+ Text(text = "${LocalSpatialCapabilities.current.isSpatialAudioEnabled}")
+ requestFullSpaceMode()
+ }
+ }
+
+ composeTestRule.onNodeWithText("${true}").assertExists()
+ }
+
+ @Test
+ fun isSpatialAudioEnabled_fullSpaceMode_requestHomeSpaceMode_returnsFalse() {
+ composeTestRule.setContent {
+ TestSetup(isFullSpace = true) {
+ Text(text = "${LocalSpatialCapabilities.current.isSpatialAudioEnabled}")
+ requestHomeSpaceMode()
+ }
+ }
+
+ composeTestRule.onNodeWithText("${false}").assertExists()
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/platform/SpatialConfigurationTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/platform/SpatialConfigurationTest.kt
new file mode 100644
index 0000000..97dfa73
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/platform/SpatialConfigurationTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.platform
+
+import androidx.compose.material3.Text
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.testing.SubspaceTestingActivity
+import androidx.xr.compose.testing.TestSetup
+import androidx.xr.compose.unit.DpVolumeSize
+import com.google.common.truth.Truth.assertThat
+import java.lang.UnsupportedOperationException
+import kotlin.test.assertFailsWith
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SpatialConfigurationTest {
+
+ @get:Rule val composeTestRule = createAndroidComposeRule<SubspaceTestingActivity>()
+
+ @Test
+ fun hasXrSpatialFeature_nonXr_isFalse() {
+ composeTestRule.setContent {
+ TestSetup(isXrEnabled = false) {
+ Text(text = "${LocalSpatialConfiguration.current.hasXrSpatialFeature}")
+ }
+ }
+
+ composeTestRule.onNodeWithText("${false}").assertExists()
+ }
+
+ @Test
+ fun requestFullSpaceMode_nonXr_throwsException() {
+ composeTestRule.setContent {
+ TestSetup(isXrEnabled = false) {
+ assertFailsWith<UnsupportedOperationException> {
+ LocalSpatialConfiguration.current.requestFullSpaceMode()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun requestHomeSpaceMode_nonXr_throwsException() {
+ composeTestRule.setContent {
+ TestSetup(isXrEnabled = false) {
+ assertFailsWith<UnsupportedOperationException> {
+ LocalSpatialConfiguration.current.requestHomeSpaceMode()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun requestModeChange_changesBounds() {
+ var configuration: SpatialConfiguration? = null
+
+ composeTestRule.setContent {
+ TestSetup(isFullSpace = true) {
+ configuration = LocalSpatialConfiguration.current
+ if (configuration?.bounds == DpVolumeSize(Dp.Infinity, Dp.Infinity, Dp.Infinity)) {
+ Text("Full")
+ } else {
+ Text("Home")
+ }
+ }
+ }
+
+ composeTestRule.onNodeWithText("Full").assertExists()
+ configuration?.requestHomeSpaceMode()
+ composeTestRule.onNodeWithText("Home").assertExists()
+ configuration?.requestFullSpaceMode()
+ composeTestRule.onNodeWithText("Full").assertExists()
+ }
+
+ @Test
+ fun hasXrSpatialFeature_fullSpaceMode_returnsTrue() {
+ composeTestRule.setContent {
+ TestSetup { Text(text = "${LocalSpatialConfiguration.current.hasXrSpatialFeature}") }
+ }
+
+ composeTestRule.onNodeWithText("${true}").assertExists()
+ }
+
+ @Test
+ fun hasXrSpatialFeature_homeSpaceMode_returnsTrue() {
+ composeTestRule.setContent {
+ TestSetup(isFullSpace = false) {
+ Text(text = "${LocalSpatialConfiguration.current.hasXrSpatialFeature}")
+ }
+ }
+
+ composeTestRule.onNodeWithText("${true}").assertExists()
+ }
+
+ @Test
+ fun bounds_homeSpaceMode_isPositiveAndNotMax() {
+ composeTestRule.setContent {
+ TestSetup(isFullSpace = false) {
+ assertThat(LocalSpatialConfiguration.current.bounds.width).isNotEqualTo(Dp.Infinity)
+ assertThat(LocalSpatialConfiguration.current.bounds.width).isGreaterThan(0.dp)
+ assertThat(LocalSpatialConfiguration.current.bounds.height)
+ .isNotEqualTo(Dp.Infinity)
+ assertThat(LocalSpatialConfiguration.current.bounds.height).isGreaterThan(0.dp)
+ assertThat(LocalSpatialConfiguration.current.bounds.depth).isNotEqualTo(Dp.Infinity)
+ assertThat(LocalSpatialConfiguration.current.bounds.depth).isGreaterThan(0.dp)
+ }
+ }
+ }
+
+ @Test
+ fun bounds_fullSpaceMode_isMax() {
+ composeTestRule.setContent {
+ TestSetup(isFullSpace = true) {
+ assertThat(LocalSpatialConfiguration.current.bounds.width).isEqualTo(Dp.Infinity)
+ assertThat(LocalSpatialConfiguration.current.bounds.height).isEqualTo(Dp.Infinity)
+ assertThat(LocalSpatialConfiguration.current.bounds.depth).isEqualTo(Dp.Infinity)
+ }
+ }
+ }
+
+ @Test
+ fun bounds_nonXr_equalsViewSize() {
+ composeTestRule.setContent {
+ TestSetup(isXrEnabled = false) {
+ // 320x470 is the default screen size returned by the testing architecture.
+ assertThat(LocalSpatialConfiguration.current.bounds.width).isEqualTo(320.dp)
+ assertThat(LocalSpatialConfiguration.current.bounds.height).isEqualTo(470.dp)
+ assertThat(LocalSpatialConfiguration.current.bounds.depth).isEqualTo(0.dp)
+ }
+ }
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/spatial/OrbiterTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/spatial/OrbiterTest.kt
new file mode 100644
index 0000000..61eb28b
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/spatial/OrbiterTest.kt
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.spatial
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Text
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertCountEquals
+import androidx.compose.ui.test.assertTextContains
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onChild
+import androidx.compose.ui.test.onChildren
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.spatial.EdgeOffset.Companion.inner
+import androidx.xr.compose.testing.SubspaceTestingActivity
+import androidx.xr.compose.testing.TestSetup
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class OrbiterTest {
+
+ @get:Rule val composeTestRule = createAndroidComposeRule<SubspaceTestingActivity>()
+
+ @Test
+ fun orbiter_contentIsElevated() {
+ composeTestRule.setContent {
+ TestSetup { Parent { Orbiter(OrbiterEdge.Top) { Text("Main Content") } } }
+ }
+
+ composeTestRule.onNodeWithText("Main Content").assertExists()
+ composeTestRule.onParent().onChild().assertDoesNotExist()
+ }
+
+ @Test
+ fun orbiter_nonXr_contentIsInline() {
+ composeTestRule.setContent {
+ TestSetup(isXrEnabled = false) {
+ Parent { Orbiter(OrbiterEdge.Top) { Text("Main Content") } }
+ }
+ }
+
+ composeTestRule.onParent().onChild().assertTextContains("Main Content")
+ }
+
+ @Test
+ fun orbiter_homeSpaceMode_contentIsInline() {
+ composeTestRule.setContent {
+ TestSetup {
+ requestHomeSpaceMode()
+ Parent { Orbiter(OrbiterEdge.Top) { Text("Main Content") } }
+ }
+ }
+
+ composeTestRule.onParent().onChild().assertTextContains("Main Content")
+ }
+
+ @Test
+ fun orbiter_nonSpatial_doesNotRenderContent() {
+ composeTestRule.setContent {
+ TestSetup {
+ requestHomeSpaceMode()
+ Parent {
+ Orbiter(
+ OrbiterEdge.Top,
+ settings = OrbiterSettings(shouldRenderInNonSpatial = false)
+ ) {
+ Text("Main Content")
+ }
+ }
+ }
+ }
+
+ composeTestRule.onNodeWithText("Main Content").assertDoesNotExist()
+ }
+
+ @Test
+ fun orbiter_multipleInstances_rendersInSpatial() {
+ composeTestRule.setContent {
+ TestSetup {
+ Parent {
+ Orbiter(position = OrbiterEdge.Top) { Text("Top") }
+ Orbiter(position = OrbiterEdge.Start) { Text("Start") }
+ Orbiter(position = OrbiterEdge.End) { Text("End") }
+ Orbiter(position = OrbiterEdge.Bottom) { Text("Bottom") }
+ }
+ }
+ }
+
+ composeTestRule.onParent().onChild().assertDoesNotExist()
+ }
+
+ @Test
+ fun orbiter_afterSwitchToFullSpaceMode_isSpatial() {
+ composeTestRule.setContent {
+ TestSetup {
+ requestHomeSpaceMode()
+ Parent { Orbiter(position = OrbiterEdge.Bottom) { Text("Bottom") } }
+ requestFullSpaceMode()
+ }
+ }
+
+ composeTestRule.onParent().onChild().assertDoesNotExist()
+ }
+
+ @Test
+ fun orbiter_setting_contentIsNotInline() {
+ composeTestRule.setContent {
+ TestSetup(isXrEnabled = false) {
+ Parent {
+ Orbiter(
+ OrbiterEdge.Top,
+ settings = OrbiterSettings(shouldRenderInNonSpatial = false)
+ ) {
+ Text("Main Content")
+ }
+ }
+ }
+ }
+
+ composeTestRule.onParent().onChild().assertDoesNotExist()
+ }
+
+ @Test
+ fun orbiter_settingChange_contentIsInline() {
+ var shouldRenderInNonSpatial by mutableStateOf(false)
+ composeTestRule.setContent {
+ TestSetup(isXrEnabled = false) {
+ Parent {
+ Orbiter(
+ OrbiterEdge.Top,
+ settings =
+ OrbiterSettings(shouldRenderInNonSpatial = shouldRenderInNonSpatial),
+ ) {
+ Text("Main Content")
+ }
+ }
+ }
+ }
+
+ shouldRenderInNonSpatial = true
+
+ composeTestRule.onParent().onChild().assertTextContains("Main Content")
+ }
+
+ @Test
+ fun orbiter_orbiterRendered() {
+ composeTestRule.setContent {
+ TestSetup {
+ requestHomeSpaceMode()
+ Box {
+ Text("Main Content")
+ Orbiter(OrbiterEdge.Start) { Text("Orbiter Content") }
+ }
+ }
+ }
+
+ composeTestRule.onNodeWithText("Main Content").assertExists()
+ composeTestRule.onNodeWithText("Orbiter Content").assertExists()
+ }
+
+ @Test
+ fun orbiter_orbiterCanBeRemoved() {
+ var showOrbiter by mutableStateOf(true)
+
+ composeTestRule.setContent {
+ TestSetup {
+ requestHomeSpaceMode()
+ Box(modifier = Modifier.size(100.dp)) {
+ Text("Main Content")
+ if (showOrbiter) {
+ Orbiter(position = OrbiterEdge.Top) { Text("Top Orbiter Content") }
+ }
+ }
+ }
+ }
+
+ composeTestRule.onNodeWithText("Top Orbiter Content").assertExists()
+ showOrbiter = false
+ composeTestRule.onNodeWithText("Top Orbiter Content").assertDoesNotExist()
+ }
+
+ @Test
+ fun orbiter_orbiterRenderedInlineInHomeSpaceMode() {
+ var isFullSpaceMode by mutableStateOf(true)
+
+ composeTestRule.setContent {
+ TestSetup {
+ LaunchedEffect(isFullSpaceMode) {
+ if (isFullSpaceMode) {
+ requestFullSpaceMode()
+ } else {
+ requestHomeSpaceMode()
+ }
+ }
+
+ Parent {
+ Box(modifier = Modifier.size(100.dp)) { Text("Main Content") }
+ Orbiter(position = OrbiterEdge.Top, offset = inner(0.dp)) {
+ Text("Top Orbiter Content")
+ }
+ Orbiter(position = OrbiterEdge.Start) { Text("Start Orbiter Content") }
+ Orbiter(position = OrbiterEdge.Bottom) { Text("Bottom Orbiter Content") }
+ Orbiter(position = OrbiterEdge.End) { Text("End Orbiter Content") }
+ }
+ }
+ }
+
+ composeTestRule.onParent().onChild().assertTextContains("Main Content")
+ isFullSpaceMode = false
+ // All orbiters become children of the Parent node
+ composeTestRule.onParent().onChildren().assertCountEquals(5)
+ isFullSpaceMode = true
+ // Orbiters exist outside of the compose hierarchy
+ composeTestRule.onParent().onChildren().assertCountEquals(1)
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/spatial/Parent.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/spatial/Parent.kt
new file mode 100644
index 0000000..80de3d9
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/spatial/Parent.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.spatial
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.onNodeWithTag
+
+@Composable
+internal fun Parent(content: @Composable () -> Unit) {
+ Box(modifier = Modifier.testTag("parent")) { content() }
+}
+
+internal fun ComposeContentTestRule.onParent() = onNodeWithTag("parent")
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/spatial/SpatialElevationTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/spatial/SpatialElevationTest.kt
new file mode 100644
index 0000000..97b741d
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/spatial/SpatialElevationTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.spatial
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Text
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertTextContains
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onChild
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.Popup
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.testing.SubspaceTestingActivity
+import androidx.xr.compose.testing.TestSetup
+import kotlin.test.assertFailsWith
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SpatialElevationTest {
+
+ @get:Rule val composeTestRule = createAndroidComposeRule<SubspaceTestingActivity>()
+
+ @Test
+ fun spatialElevation_mainContent_isComposed() {
+ composeTestRule.setContent {
+ TestSetup {
+ SpatialElevation {
+ Box(modifier = Modifier.size(100.dp).testTag("MainContent")) {
+ Text("Main Content")
+ }
+ }
+ }
+ }
+
+ composeTestRule.onNodeWithTag("MainContent").assertExists()
+ }
+
+ @Test
+ fun spatialElevation_dialog_throwsError() {
+ assertFailsWith<RuntimeException> {
+ composeTestRule.setContent {
+ TestSetup { SpatialElevation { Dialog(onDismissRequest = {}) { Text("Title") } } }
+ }
+ }
+ }
+
+ @Test
+ fun spatialElevation_popup_throwsError() {
+ assertFailsWith<RuntimeException> {
+ composeTestRule.setContent {
+ TestSetup { SpatialElevation { Popup { Text("Popup") } } }
+ }
+ }
+ }
+
+ @Test
+ fun spatialElevation_xrNotSupported_doesNotThrowError() {
+ composeTestRule.setContent {
+ TestSetup(isXrEnabled = false) { SpatialElevation { Popup { Text("Popup") } } }
+ }
+
+ composeTestRule.onNodeWithText("Popup").assertExists()
+ }
+
+ @Test
+ fun spatialElevation_homeSpaceMode_doesNotElevate() {
+ composeTestRule.setContent {
+ TestSetup {
+ requestHomeSpaceMode()
+ Parent { SpatialElevation { Text("Main Content") } }
+ }
+ }
+
+ composeTestRule.onParent().onChild().assertTextContains("Main Content")
+ }
+
+ @Test
+ fun spatialElevation_fullSpaceMode_doesElevate() {
+ composeTestRule.setContent {
+ TestSetup {
+ requestFullSpaceMode()
+ Parent { SpatialElevation { Text("Main Content") } }
+ }
+ }
+
+ composeTestRule.onParent().onChild().assertDoesNotExist()
+ composeTestRule.onNodeWithText("Main Content").assertExists()
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/spatial/SubspaceTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/spatial/SubspaceTest.kt
new file mode 100644
index 0000000..81677f9
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/spatial/SubspaceTest.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.spatial
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.platform.SceneManager
+import androidx.xr.compose.subspace.SpatialPanel
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.layout.testTag
+import androidx.xr.compose.testing.SubspaceTestingActivity
+import androidx.xr.compose.testing.TestSetup
+import androidx.xr.compose.testing.assertPositionInRootIsEqualTo
+import androidx.xr.compose.testing.createFakeRuntime
+import androidx.xr.compose.testing.onSubspaceNodeWithTag
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SubspaceTest {
+
+ @get:Rule val composeTestRule = createAndroidComposeRule<SubspaceTestingActivity>()
+
+ @Test
+ fun subspace_xrEnabled_contentIsCreated() {
+ composeTestRule.setContent {
+ TestSetup { Subspace { SpatialPanel(SubspaceModifier.testTag("panel")) {} } }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp, 0.dp)
+ }
+
+ @Test
+ fun subspace_nonXr_contentIsNotCreated() {
+ composeTestRule.setContent {
+ TestSetup(isXrEnabled = false) {
+ Subspace { SpatialPanel(SubspaceModifier.testTag("panel")) {} }
+ }
+ }
+
+ composeTestRule.onSubspaceNodeWithTag("panel").assertDoesNotExist()
+ }
+
+ @Test
+ fun subspace_applicationSubspace_contentIsParentedToActivitySpace() {
+ composeTestRule.setContent {
+ TestSetup { Subspace { SpatialPanel(SubspaceModifier.testTag("panel")) {} } }
+ }
+
+ val node = composeTestRule.onSubspaceNodeWithTag("panel").fetchSemanticsNode()
+ assertThat(node.coreEntity?.entity?.getParent())
+ .isEqualTo(composeTestRule.activity.session.activitySpace)
+ }
+
+ @Test
+ fun subspace_nestedSubspace_contentIsParentedToContainingPanel() {
+ composeTestRule.setContent {
+ TestSetup {
+ Subspace {
+ SpatialPanel(SubspaceModifier.testTag("panel")) {
+ Subspace { SpatialPanel(SubspaceModifier.testTag("innerPanel")) {} }
+ }
+ }
+ }
+ }
+
+ val panelNode = composeTestRule.onSubspaceNodeWithTag("panel").fetchSemanticsNode()
+ val innerPanelNode =
+ composeTestRule.onSubspaceNodeWithTag("innerPanel").fetchSemanticsNode()
+ val innerPanelEntity = innerPanelNode.coreEntity?.entity
+ val subspaceRootEntity = innerPanelEntity?.getParent()
+ val subspaceRootContainerEntity = subspaceRootEntity?.getParent()
+ val parentPanel = subspaceRootContainerEntity?.getParent()
+ assertThat(parentPanel).isNotNull()
+ assertThat(parentPanel).isEqualTo(panelNode.coreEntity?.entity)
+ }
+
+ @Test
+ fun subspace_isDisposed() {
+ var showSubspace by mutableStateOf(true)
+
+ composeTestRule.setContent {
+ TestSetup {
+ if (showSubspace) {
+ Subspace { SpatialPanel(SubspaceModifier.testTag("panel")) {} }
+ }
+ }
+ }
+
+ composeTestRule.onSubspaceNodeWithTag("panel").assertExists()
+ assertThat(SceneManager.getSceneCount()).isEqualTo(1)
+ showSubspace = false
+ composeTestRule.onSubspaceNodeWithTag("panel").assertDoesNotExist()
+ assertThat(SceneManager.getSceneCount()).isEqualTo(0)
+ }
+
+ @Test
+ fun subspace_onlyOneSceneExists_afterSpaceModeChanges() {
+ val runtime = createFakeRuntime(composeTestRule.activity)
+
+ composeTestRule.setContent {
+ TestSetup(runtime = runtime) {
+ Subspace { SpatialPanel(SubspaceModifier.testTag("panel")) {} }
+ }
+ }
+
+ composeTestRule.onSubspaceNodeWithTag("panel").assertExists()
+ assertThat(SceneManager.getSceneCount()).isEqualTo(1)
+ runtime.requestHomeSpaceMode()
+ composeTestRule.onSubspaceNodeWithTag("panel").assertDoesNotExist()
+ assertThat(SceneManager.getSceneCount()).isEqualTo(0)
+ runtime.requestFullSpaceMode()
+ composeTestRule.onSubspaceNodeWithTag("panel").assertExists()
+ assertThat(SceneManager.getSceneCount()).isEqualTo(1)
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/SpatialBoxTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/SpatialBoxTest.kt
new file mode 100644
index 0000000..dafaff6
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/SpatialBoxTest.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace
+
+import androidx.compose.material3.Text
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.subspace.layout.SpatialAlignment
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.layout.size
+import androidx.xr.compose.subspace.layout.testTag
+import androidx.xr.compose.testing.SubspaceTestingActivity
+import androidx.xr.compose.testing.assertHeightIsEqualTo
+import androidx.xr.compose.testing.assertPositionInRootIsEqualTo
+import androidx.xr.compose.testing.assertWidthIsEqualTo
+import androidx.xr.compose.testing.onSubspaceNodeWithTag
+import androidx.xr.compose.testing.setSubspaceContent
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [SpatialBox]. */
+@RunWith(AndroidJUnit4::class)
+class SpatialBoxTest {
+ @get:Rule val composeTestRule = createAndroidComposeRule<SubspaceTestingActivity>()
+
+ @Test
+ fun spatialBox_elementsAreCenteredByDefault() {
+ composeTestRule.setSubspaceContent {
+ SpatialBox(SubspaceModifier.size(100.dp)) {
+ SpatialPanel(SubspaceModifier.testTag("panel1").size(50.dp)) {
+ Text(text = "Panel 1")
+ }
+ SpatialPanel(SubspaceModifier.testTag("panel2").size(50.dp)) {
+ Text(text = "Panel 2")
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel1")
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp, 0.dp)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(50.dp)
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel2")
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp, 0.dp)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(50.dp)
+ }
+
+ @Test
+ fun spatialBox_elementsAreAlignedWithBoxSpatialAlignment_topLeft() {
+ composeTestRule.setSubspaceContent {
+ SpatialBox(SubspaceModifier.size(100.dp), alignment = SpatialAlignment.TopLeft) {
+ SpatialPanel(SubspaceModifier.testTag("panel1").size(50.dp)) {
+ Text(text = "Panel 1")
+ }
+ SpatialPanel(SubspaceModifier.testTag("panel2").size(50.dp)) {
+ Text(text = "Panel 2")
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel1")
+ .assertPositionInRootIsEqualTo(-25.dp, 25.dp, 0.dp)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(50.dp)
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel2")
+ .assertPositionInRootIsEqualTo(-25.dp, 25.dp, 0.dp)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(50.dp)
+ }
+
+ @Test
+ fun spatialBox_elementsAreAlignedWithBoxSpatialAlignment_bottomRight() {
+ composeTestRule.setSubspaceContent {
+ SpatialBox(SubspaceModifier.size(100.dp), alignment = SpatialAlignment.BottomRight) {
+ SpatialPanel(SubspaceModifier.testTag("panel1").size(50.dp)) {
+ Text(text = "Panel 1")
+ }
+ SpatialPanel(SubspaceModifier.testTag("panel2").size(50.dp)) {
+ Text(text = "Panel 2")
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel1")
+ .assertPositionInRootIsEqualTo(25.dp, -25.dp, 0.dp)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(50.dp)
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel2")
+ .assertPositionInRootIsEqualTo(25.dp, -25.dp, 0.dp)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(50.dp)
+ }
+
+ @Test
+ fun spatialBox_elementsAreAlignedWithModifier() {
+ composeTestRule.setSubspaceContent {
+ SpatialBox(SubspaceModifier.size(100.dp)) {
+ SpatialPanel(
+ SubspaceModifier.testTag("panel1")
+ .size(50.dp)
+ .align(SpatialAlignment.BottomLeft)
+ ) {
+ Text(text = "Panel 1")
+ }
+ SpatialPanel(
+ SubspaceModifier.testTag("panel2").size(50.dp).align(SpatialAlignment.TopRight)
+ ) {
+ Text(text = "Panel 2")
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel1")
+ .assertPositionInRootIsEqualTo(-25.dp, -25.dp, 0.dp)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(50.dp)
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel2")
+ .assertPositionInRootIsEqualTo(25.dp, 25.dp, 0.dp)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(50.dp)
+ }
+
+ @Test
+ fun spatialBox_elementsHonorPropagatedMinConstraints() {
+ composeTestRule.setSubspaceContent {
+ SpatialBox(SubspaceModifier.size(100.dp), propagateMinConstraints = true) {
+ SpatialPanel(SubspaceModifier.testTag("panel1").size(50.dp)) {
+ Text(text = "Panel 1")
+ }
+ SpatialPanel(SubspaceModifier.testTag("panel2").size(50.dp)) {
+ Text(text = "Panel 2")
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel1")
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp, 0.dp)
+ .assertWidthIsEqualTo(100.dp)
+ .assertHeightIsEqualTo(100.dp)
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel2")
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp, 0.dp)
+ .assertWidthIsEqualTo(100.dp)
+ .assertHeightIsEqualTo(100.dp)
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/SpatialPanelTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/SpatialPanelTest.kt
new file mode 100644
index 0000000..972f2ca
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/SpatialPanelTest.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace
+
+import android.view.View
+import android.widget.TextView
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.subspace.layout.CorePanelEntity
+import androidx.xr.compose.subspace.layout.SpatialRoundedCornerShape
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.layout.height
+import androidx.xr.compose.subspace.layout.testTag
+import androidx.xr.compose.subspace.layout.width
+import androidx.xr.compose.testing.SubspaceTestingActivity
+import androidx.xr.compose.testing.onSubspaceNodeWithTag
+import androidx.xr.compose.testing.setSubspaceContent
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertThrows
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [SpatialPanel]. */
+@RunWith(AndroidJUnit4::class)
+class SpatialPanelTest {
+ @get:Rule val composeTestRule = createAndroidComposeRule<SubspaceTestingActivity>()
+
+ @Test
+ fun spatialPanel_internalElementsAreLaidOutProperly() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(SubspaceModifier.width(100.dp).testTag("panel")) {
+ // Row with 2 elements, one is 3x as large as the other
+ Row {
+ Spacer(Modifier.testTag("spacer1").weight(1f))
+ Spacer(Modifier.testTag("spacer2").weight(3f))
+ }
+ }
+ }
+ composeTestRule.onSubspaceNodeWithTag("panel").assertExists()
+ composeTestRule.onNodeWithTag("spacer1").assertWidthIsEqualTo(25.dp)
+ composeTestRule.onNodeWithTag("spacer2").assertWidthIsEqualTo(75.dp)
+ }
+
+ @Test
+ fun spatialPanel_textTooLong_panelDoesNotGrowBeyondSpecifiedWidth() {
+ composeTestRule.setSubspaceContent {
+ // Panel with 10dp width, way too small for the text we're putting into it
+ SpatialPanel(SubspaceModifier.width(10.dp).testTag("panel")) {
+ // Panel contains a column.
+ Column {
+ Text("Hello World long text", style = MaterialTheme.typography.headlineLarge)
+ }
+ }
+ }
+ // Text element stays 10dp long, even though it needs more space, as the Panel will not grow
+ // for the text.
+ composeTestRule.onSubspaceNodeWithTag("panel").assertExists()
+ composeTestRule.onNodeWithText("Hello World long text").assertWidthIsEqualTo(10.dp)
+ }
+
+ @Test
+ fun spatialPanel_viewBasedPanelComposes() {
+ composeTestRule.setSubspaceContent {
+ val context = LocalContext.current
+ val textView = remember { TextView(context).apply { text = "Hello World" } }
+ SpatialPanel(view = textView, SubspaceModifier.testTag("panel"))
+ // The View is not inserted in the compose tree, we need to test it differentlly
+ assertEquals(View.VISIBLE, textView.visibility)
+ }
+ // TODO: verify that the TextView is add to the Panel
+ composeTestRule.onSubspaceNodeWithTag("panel").assertExists()
+ }
+
+ @Test
+ fun mainPanel_renders() {
+ val text = "Main Window Text"
+ composeTestRule.setSubspaceContent({ Text(text) }) {
+ MainPanel(SubspaceModifier.testTag("panel"))
+ }
+
+ composeTestRule.onSubspaceNodeWithTag("panel").assertExists()
+ composeTestRule.onNodeWithText(text).assertExists()
+ }
+
+ @Test
+ fun mainPanel_addedTwice_asserts() {
+ val text = "Main Window Text"
+
+ assertThrows(IllegalStateException::class.java) {
+ composeTestRule.setSubspaceContent({ Text(text) }) {
+ MainPanel(SubspaceModifier.testTag("panel"))
+ MainPanel(SubspaceModifier.testTag("panel2"))
+ }
+ }
+ }
+
+ @Test
+ fun mainPanel_addedTwiceInDifferentSubtrees_asserts() {
+ val text = "Main Window Text"
+
+ assertThrows(IllegalStateException::class.java) {
+ composeTestRule.setSubspaceContent({ Text(text) }) {
+ SpatialColumn {
+ SpatialRow { MainPanel(SubspaceModifier.testTag("panel")) }
+ SpatialRow { MainPanel(SubspaceModifier.testTag("panel2")) }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun spatialPanel_cornerRadius_dp() {
+ val density = Density(1.0f)
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(
+ modifier = SubspaceModifier.width(200.dp).height(300.dp).testTag("panel"),
+ shape = SpatialRoundedCornerShape(CornerSize(32.dp)),
+ ) {}
+ }
+
+ assertThat(getCorePanelEntity("panel")?.getCornerRadius(density)).isEqualTo(32f)
+ }
+
+ @Test
+ fun spatialPanel_cornerRadius_percent() {
+ val density = Density(1.0f)
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(
+ modifier = SubspaceModifier.width(200.dp).height(300.dp).testTag("panel"),
+ shape = SpatialRoundedCornerShape(CornerSize(50)),
+ ) {}
+ }
+
+ // 50 percent of the shorter side (200.dp) at 1.0 Density is 100 pixels.
+ assertThat(getCorePanelEntity("panel")?.getCornerRadius(density)).isEqualTo(100f)
+ }
+
+ @Test
+ fun spatialPanel_cornerRadius_increasedDensity() {
+ val density = Density(3.0f)
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(
+ modifier = SubspaceModifier.width(200.dp).height(300.dp).testTag("panel"),
+ shape = SpatialRoundedCornerShape(CornerSize(50)),
+ ) {}
+ }
+
+ // 50 percent of the shorter side (200.dp) at 3.0 Density is 300 pixels.
+ assertThat(getCorePanelEntity("panel")?.getCornerRadius(density)).isEqualTo(300f)
+ }
+
+ private fun getCorePanelEntity(tag: String): CorePanelEntity? {
+ return composeTestRule.onSubspaceNodeWithTag(tag).fetchSemanticsNode().coreEntity
+ as? CorePanelEntity
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/SpatialRowColumnTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/SpatialRowColumnTest.kt
new file mode 100644
index 0000000..bae11de
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/SpatialRowColumnTest.kt
@@ -0,0 +1,457 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace
+
+import androidx.compose.material3.Text
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.subspace.layout.SpatialAlignment
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.layout.offset
+import androidx.xr.compose.subspace.layout.size
+import androidx.xr.compose.subspace.layout.testTag
+import androidx.xr.compose.subspace.layout.width
+import androidx.xr.compose.testing.SubspaceTestingActivity
+import androidx.xr.compose.testing.assertHeightIsEqualTo
+import androidx.xr.compose.testing.assertLeftPositionInRootIsEqualTo
+import androidx.xr.compose.testing.assertRotationInRootIsEqualTo
+import androidx.xr.compose.testing.assertTopPositionInRootIsEqualTo
+import androidx.xr.compose.testing.assertWidthIsEqualTo
+import androidx.xr.compose.testing.assertXPositionInRootIsEqualTo
+import androidx.xr.compose.testing.assertYPositionInRootIsEqualTo
+import androidx.xr.compose.testing.assertZPositionInRootIsEqualTo
+import androidx.xr.compose.testing.onSubspaceNodeWithTag
+import androidx.xr.compose.testing.setSubspaceContent
+import androidx.xr.runtime.math.Quaternion
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [SpatialRow] and [SpatialColumn]. */
+@RunWith(AndroidJUnit4::class)
+class SpatialRowColumnTest {
+ @get:Rule val composeTestRule = createAndroidComposeRule<SubspaceTestingActivity>()
+
+ @Test
+ fun spatialRowColumn_internalElementsAreLaidOutProperly() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.testTag("row1").width(10.dp)) {
+ // This column will get the first 7dp
+ SpatialColumn(SubspaceModifier.testTag("column1").width(7.dp)) {
+ SpatialPanel { Text(text = "Column 1") }
+ }
+ // There are only 3dp left, so this column will end up being 3dp
+ SpatialColumn(SubspaceModifier.testTag("column2").width(7.dp)) {
+ SpatialPanel { Text(text = "Column 2") }
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("row1")
+ .assertLeftPositionInRootIsEqualTo(-5.dp)
+ .assertXPositionInRootIsEqualTo(0.dp)
+ .assertWidthIsEqualTo(10.dp)
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column1")
+ .assertLeftPositionInRootIsEqualTo(-5.dp)
+ .assertWidthIsEqualTo(7.dp)
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column2")
+ .assertLeftPositionInRootIsEqualTo(2.dp)
+ .assertWidthIsEqualTo(3.dp)
+ }
+
+ @Test
+ fun spatialRow_internalElementsAreAligned() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(
+ SubspaceModifier.testTag("row1").size(20.dp),
+ alignment = SpatialAlignment.CenterLeft,
+ ) {
+ SpatialColumn(SubspaceModifier.testTag("column1").size(5.dp)) {
+ SpatialPanel { Text(text = "Column 1") }
+ }
+ SpatialColumn(SubspaceModifier.testTag("column2").size(5.dp)) {
+ SpatialPanel { Text(text = "Column 2") }
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("row1")
+ .assertLeftPositionInRootIsEqualTo(-10.dp)
+ .assertXPositionInRootIsEqualTo(0.dp)
+ .assertWidthIsEqualTo(20.dp)
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column1")
+ .assertLeftPositionInRootIsEqualTo(-10.dp)
+ .assertWidthIsEqualTo(5.dp)
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column2")
+ .assertLeftPositionInRootIsEqualTo(-5.dp)
+ .assertWidthIsEqualTo(5.dp)
+ }
+
+ @Test
+ fun spatialRow_internalElementsAreAligned_withModifier() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(
+ SubspaceModifier.testTag("row1").size(20.dp),
+ alignment = SpatialAlignment.CenterLeft,
+ ) {
+ SpatialColumn(
+ SubspaceModifier.testTag("column1").size(10.dp).align(SpatialAlignment.Top)
+ ) {
+ SpatialPanel { Text(text = "Column 1") }
+ }
+ SpatialColumn(
+ SubspaceModifier.testTag("column2").size(10.dp).align(SpatialAlignment.Front)
+ ) {
+ SpatialPanel { Text(text = "Column 2") }
+ }
+ }
+ }
+
+ composeTestRule.onSubspaceNodeWithTag("column1").assertYPositionInRootIsEqualTo(5.dp)
+
+ composeTestRule.onSubspaceNodeWithTag("column2").assertZPositionInRootIsEqualTo(5.dp)
+ }
+
+ @Test
+ fun spatialColumn_internalElementsAreAligned() {
+ composeTestRule.setSubspaceContent {
+ SpatialColumn(
+ SubspaceModifier.testTag("column1").size(20.dp),
+ alignment = SpatialAlignment.TopCenter,
+ ) {
+ SpatialRow(SubspaceModifier.testTag("row1").size(5.dp)) {
+ SpatialPanel { Text(text = "SpatialRow 1") }
+ }
+ SpatialRow(SubspaceModifier.testTag("row2").size(5.dp)) {
+ SpatialPanel { Text(text = "SpatialRow 2") }
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column1")
+ .assertTopPositionInRootIsEqualTo(10.dp)
+ .assertYPositionInRootIsEqualTo(0.dp)
+ .assertHeightIsEqualTo(20.dp)
+
+ composeTestRule
+ .onSubspaceNodeWithTag("row1")
+ .assertTopPositionInRootIsEqualTo(10.dp)
+ .assertHeightIsEqualTo(5.dp)
+
+ composeTestRule
+ .onSubspaceNodeWithTag("row2")
+ .assertTopPositionInRootIsEqualTo(5.dp)
+ .assertHeightIsEqualTo(5.dp)
+ }
+
+ @Test
+ fun spatialColumn_internalElementsAreAligned_withModifier() {
+ composeTestRule.setSubspaceContent {
+ SpatialColumn(
+ SubspaceModifier.testTag("column1").size(20.dp),
+ alignment = SpatialAlignment.TopCenter,
+ ) {
+ SpatialRow(
+ SubspaceModifier.testTag("row1").size(10.dp).align(SpatialAlignment.Left)
+ ) {
+ SpatialPanel { Text(text = "SpatialRow 1") }
+ }
+ SpatialRow(
+ SubspaceModifier.testTag("row2").size(10.dp).align(SpatialAlignment.Back)
+ ) {
+ SpatialPanel { Text(text = "SpatialRow 2") }
+ }
+ }
+ }
+
+ composeTestRule.onSubspaceNodeWithTag("row1").assertXPositionInRootIsEqualTo(-5.dp)
+
+ composeTestRule.onSubspaceNodeWithTag("row2").assertZPositionInRootIsEqualTo(-5.dp)
+ }
+
+ @Test
+ fun spatialRowColumn_twoWeightBasedChildren_internalElementsAreLaidOutProperly() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.testTag("row1").width(1000.dp)) {
+ // 25% width (250dp)
+ SpatialColumn(SubspaceModifier.testTag("column1").weight(1f)) {
+ SpatialPanel { Text(text = "Column 1") }
+ }
+ // 75% width (750dp)
+ SpatialColumn(SubspaceModifier.testTag("column2").weight(3f)) {
+ SpatialPanel { Text(text = "Column 2") }
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("row1")
+ .assertLeftPositionInRootIsEqualTo(-500.dp)
+ .assertXPositionInRootIsEqualTo(0.dp)
+ .assertWidthIsEqualTo(1000.dp)
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column1")
+ .assertLeftPositionInRootIsEqualTo(-500.dp)
+ .assertWidthIsEqualTo(250.dp)
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column2")
+ .assertLeftPositionInRootIsEqualTo(-250.dp)
+ .assertWidthIsEqualTo(750.dp)
+ }
+
+ @Test
+ fun spatialRowColumn_oneFixedAndTwoWeightBasedChildren_internalElementsAreLaidOutProperly() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.testTag("row1").width(1000.dp)) {
+ // 250dp fixed width
+ SpatialColumn(SubspaceModifier.testTag("column1").width(250.dp)) {
+ SpatialPanel { Text(text = "Column 1") }
+ }
+ // 1/5th of the remaining 750dp (150dp)
+ SpatialColumn(SubspaceModifier.testTag("column2").weight(1f)) {
+ SpatialPanel { Text(text = "Column 2") }
+ }
+ // 4/5th of the remaining 750dp (600dp)
+ SpatialColumn(SubspaceModifier.testTag("column3").weight(4f)) {
+ SpatialPanel { Text(text = "Column 3") }
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("row1")
+ .assertLeftPositionInRootIsEqualTo(-500.dp)
+ .assertXPositionInRootIsEqualTo(0.dp)
+ .assertWidthIsEqualTo(1000.dp)
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column1")
+ .assertLeftPositionInRootIsEqualTo(-500.dp)
+ .assertWidthIsEqualTo(250.dp)
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column2")
+ .assertLeftPositionInRootIsEqualTo(-250.dp)
+ .assertWidthIsEqualTo(150.dp)
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column3")
+ .assertLeftPositionInRootIsEqualTo(-100.dp)
+ .assertWidthIsEqualTo(600.dp)
+ }
+
+ @Test
+ fun spatialRowColumn_weightCalculationRemainderIsAppliedCorrectly() {
+ composeTestRule.setSubspaceContent {
+ // 200dp row, 7 children:
+ // 200 / 7 = 28.57, which gets rounded to 29dp.
+ // 29 * 7 = 203dp, so the first 3 children should have 1dp removed from each of them.
+ SpatialRow(SubspaceModifier.testTag("row1").width(200.dp)) {
+ // 28dp (1dp remainder removed)
+ SpatialColumn(SubspaceModifier.testTag("column1").weight(1f)) { SpatialPanel {} }
+ // 28dp (1dp remainder removed)
+ SpatialColumn(SubspaceModifier.testTag("column2").weight(1f)) { SpatialPanel {} }
+ // 28dp (1dp remainder removed)
+ SpatialColumn(SubspaceModifier.testTag("column3").weight(1f)) { SpatialPanel {} }
+ // 29dp
+ SpatialColumn(SubspaceModifier.testTag("column4").weight(1f)) { SpatialPanel {} }
+ // 29dp
+ SpatialColumn(SubspaceModifier.testTag("column5").weight(1f)) { SpatialPanel {} }
+ // 29dp
+ SpatialColumn(SubspaceModifier.testTag("column6").weight(1f)) { SpatialPanel {} }
+ // 29dp
+ SpatialColumn(SubspaceModifier.testTag("column7").weight(1f)) { SpatialPanel {} }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("row1")
+ .assertLeftPositionInRootIsEqualTo(-100.dp)
+ .assertXPositionInRootIsEqualTo(0.dp)
+ .assertWidthIsEqualTo(200.dp)
+
+ var currentLeftPosition = -100.dp
+ composeTestRule
+ .onSubspaceNodeWithTag("column1")
+ .assertLeftPositionInRootIsEqualTo(currentLeftPosition)
+ .assertWidthIsEqualTo(28.dp)
+ currentLeftPosition += 28.dp
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column2")
+ .assertLeftPositionInRootIsEqualTo(currentLeftPosition)
+ .assertWidthIsEqualTo(28.dp)
+ currentLeftPosition += 28.dp
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column3")
+ .assertLeftPositionInRootIsEqualTo(currentLeftPosition)
+ .assertWidthIsEqualTo(28.dp)
+ currentLeftPosition += 28.dp
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column4")
+ .assertLeftPositionInRootIsEqualTo(currentLeftPosition)
+ .assertWidthIsEqualTo(29.dp)
+ currentLeftPosition += 29.dp
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column5")
+ .assertLeftPositionInRootIsEqualTo(currentLeftPosition)
+ .assertWidthIsEqualTo(29.dp)
+ currentLeftPosition += 29.dp
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column6")
+ .assertLeftPositionInRootIsEqualTo(currentLeftPosition)
+ .assertWidthIsEqualTo(29.dp)
+ currentLeftPosition += 29.dp
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column7")
+ .assertLeftPositionInRootIsEqualTo(currentLeftPosition)
+ .assertWidthIsEqualTo(29.dp)
+ }
+
+ @Test
+ fun spatialRowColumn_negativeCurvatureIsIgnored() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.testTag("row1").width(500.dp), curveRadius = -100.dp) {
+ SpatialColumn(SubspaceModifier.testTag("column1").width(250.dp)) {
+ SpatialPanel { Text(text = "Column 1") }
+ }
+ SpatialColumn(SubspaceModifier.testTag("column2").width(250.dp)) {
+ SpatialPanel { Text(text = "Column 2") }
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column1")
+ .assertXPositionInRootIsEqualTo(-125.dp)
+ .assertYPositionInRootIsEqualTo(0.dp)
+ .assertZPositionInRootIsEqualTo(0.dp) // No curvature.
+ .assertRotationInRootIsEqualTo(Quaternion.Identity)
+ composeTestRule
+ .onSubspaceNodeWithTag("column2")
+ .assertXPositionInRootIsEqualTo(125.dp)
+ .assertYPositionInRootIsEqualTo(0.dp)
+ .assertZPositionInRootIsEqualTo(0.dp) // No curvature.
+ .assertRotationInRootIsEqualTo(Quaternion.Identity)
+ }
+
+ @Test
+ fun spatialRowColumn_zeroCurvatureIsIgnored() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.testTag("row1").width(500.dp), curveRadius = 0.dp) {
+ SpatialColumn(SubspaceModifier.testTag("column1").width(250.dp)) {
+ SpatialPanel { Text(text = "Column 1") }
+ }
+ SpatialColumn(SubspaceModifier.testTag("column2").width(250.dp)) {
+ SpatialPanel { Text(text = "Column 2") }
+ }
+ }
+ }
+
+ // Verify that the row is flat, i.e., it has no curvature.
+ composeTestRule
+ .onSubspaceNodeWithTag("column1")
+ .assertXPositionInRootIsEqualTo(-125.dp)
+ .assertYPositionInRootIsEqualTo(0.dp)
+ .assertZPositionInRootIsEqualTo(0.dp) // No curvature.
+ .assertRotationInRootIsEqualTo(Quaternion.Identity)
+ composeTestRule
+ .onSubspaceNodeWithTag("column2")
+ .assertXPositionInRootIsEqualTo(125.dp)
+ .assertYPositionInRootIsEqualTo(0.dp)
+ .assertZPositionInRootIsEqualTo(0.dp) // No curvature.
+ .assertRotationInRootIsEqualTo(Quaternion.Identity)
+ }
+
+ @Test
+ fun spatialRowColumn_positiveCurvatureCreatesCurvature() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.testTag("row1").width(500.dp), curveRadius = 100.dp) {
+ SpatialColumn(SubspaceModifier.testTag("column1").width(250.dp)) {
+ SpatialPanel { Text(text = "Column 1") }
+ }
+ SpatialColumn(SubspaceModifier.testTag("column2").width(250.dp)) {
+ SpatialPanel { Text(text = "Column 2") }
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column1")
+ .assertXPositionInRootIsEqualTo(-94.dp)
+ .assertYPositionInRootIsEqualTo(0.dp)
+ .assertZPositionInRootIsEqualTo(68.dp)
+ .assertRotationInRootIsEqualTo(Quaternion(0.0f, 0.58509725f, 0.0f, 0.8109631f))
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column2")
+ .assertXPositionInRootIsEqualTo(94.dp)
+ .assertYPositionInRootIsEqualTo(0.dp)
+ .assertZPositionInRootIsEqualTo(68.dp)
+ .assertRotationInRootIsEqualTo(Quaternion(0.0f, -0.58509725f, 0.0f, 0.8109631f))
+ }
+
+ @Test
+ fun spatialRowColumn_zOffsetIsRespected() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(
+ SubspaceModifier.testTag("row1").width(500.dp).offset(0.dp, 0.dp, -50.dp),
+ curveRadius = 100.dp,
+ ) {
+ SpatialColumn(SubspaceModifier.testTag("column1").width(250.dp)) {
+ SpatialPanel { Text(text = "Column 1") }
+ }
+ SpatialColumn(SubspaceModifier.testTag("column2").width(250.dp)) {
+ SpatialPanel { Text(text = "Column 2") }
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column1")
+ .assertXPositionInRootIsEqualTo(-94.dp)
+ .assertYPositionInRootIsEqualTo(0.dp)
+ .assertZPositionInRootIsEqualTo(18.dp) // Offset by -50.dp.
+ .assertRotationInRootIsEqualTo(Quaternion(0.0f, 0.58509725f, 0.0f, 0.8109631f))
+
+ composeTestRule
+ .onSubspaceNodeWithTag("column2")
+ .assertXPositionInRootIsEqualTo(94.dp)
+ .assertYPositionInRootIsEqualTo(0.dp)
+ .assertZPositionInRootIsEqualTo(18.dp) // Offset by -50.dp.
+ .assertRotationInRootIsEqualTo(Quaternion(0.0f, -0.58509725f, 0.0f, 0.8109631f))
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/CoreEntityTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/CoreEntityTest.kt
new file mode 100644
index 0000000..badc30c
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/CoreEntityTest.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.testing.SubspaceTestingActivity
+import java.lang.IllegalArgumentException
+import kotlin.test.assertFailsWith
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class CoreEntityTest {
+
+ @get:Rule val composeTestRule = createAndroidComposeRule<SubspaceTestingActivity>()
+
+ @Test
+ fun coreEntity_coreContentlessEntity_shouldThrowIfNotContentless() {
+ composeTestRule.setContent {}
+
+ assertFailsWith<IllegalArgumentException> {
+ CoreContentlessEntity(composeTestRule.activity.session.activitySpace)
+ }
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/MeasureScopeTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/MeasureScopeTest.kt
new file mode 100644
index 0000000..0ed4c6f
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/MeasureScopeTest.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+class TestMeasureScope : MeasureScope {
+ override val density: Float
+ get() = 2f
+}
+
+/** Tests for [MeasureScope]. */
+@RunWith(AndroidJUnit4::class)
+class MeasureScopeTest {
+ @Test
+ fun toPx_convertsCorrectly() {
+ with(TestMeasureScope()) {
+ assertThat(Dp(0f).toPx()).isEqualTo(0f)
+ assertThat(Dp(10.3f).toPx()).isEqualTo(20.6f)
+ assertThat(Dp(-10.3f).toPx()).isEqualTo(-20.6f)
+ assertThat(Dp(Float.POSITIVE_INFINITY).toPx()).isEqualTo(Float.POSITIVE_INFINITY)
+ assertThat(Dp(Float.NEGATIVE_INFINITY).toPx()).isEqualTo(Float.NEGATIVE_INFINITY)
+ }
+ }
+
+ @Test
+ fun roundToPx_convertsCorrectly() {
+ with(TestMeasureScope()) {
+ assertThat(Dp(0f).roundToPx()).isEqualTo(0)
+ assertThat(Dp(10.3f).roundToPx()).isEqualTo(21)
+ assertThat(Dp(-10.3f).roundToPx()).isEqualTo(-21)
+ assertThat(Dp(Float.POSITIVE_INFINITY).roundToPx()).isEqualTo(Constraints.Infinity)
+ assertThat(Dp(Float.NEGATIVE_INFINITY).roundToPx()).isEqualTo(Constraints.Infinity)
+ }
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/MovableModifierTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/MovableModifierTest.kt
new file mode 100644
index 0000000..cd21291
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/MovableModifierTest.kt
@@ -0,0 +1,454 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.subspace.SpatialColumn
+import androidx.xr.compose.subspace.SpatialPanel
+import androidx.xr.compose.subspace.SpatialRow
+import androidx.xr.compose.subspace.Volume
+import androidx.xr.compose.testing.SubspaceTestingActivity
+import androidx.xr.compose.testing.onSubspaceNodeWithTag
+import androidx.xr.compose.testing.setSubspaceContent
+import androidx.xr.scenecore.MovableComponent
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [Movable] modifier. */
+@RunWith(AndroidJUnit4::class)
+class MovableModifierTest {
+ @get:Rule val composeTestRule = createAndroidComposeRule<SubspaceTestingActivity>()
+
+ @Test
+ fun movable_noComponentByDefault() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(SubspaceModifier.testTag("panel")) { Text(text = "Panel") }
+ }
+ assertTrue(
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .fetchSemanticsNode()
+ .components
+ .isNullOrEmpty()
+ )
+ }
+
+ @Test
+ fun movable_componentIsNotNullAndOnlyContainsSingleMovable() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(SubspaceModifier.testTag("panel").movable()) { Text(text = "Panel") }
+ }
+ assertSingleMovableComponentExist()
+ }
+
+ @Test
+ fun movable_modifierIsDisabledAndComponentDoesNotExist() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(SubspaceModifier.testTag("panel").movable(false)) { Text(text = "Panel") }
+ }
+
+ assertMovableComponentDoesNotExist()
+ }
+
+ @Test
+ fun movable_modifierDoesNotChangeAndOnlyOneComponentExist() {
+ composeTestRule.setSubspaceContent {
+ var panelWidth by remember { mutableStateOf(50.dp) }
+ SpatialPanel(
+ SubspaceModifier.testTag("panel").width(panelWidth).movable(enabled = true)
+ ) {
+ Button(modifier = Modifier.testTag("button"), onClick = { panelWidth += 50.dp }) {
+ Text(text = "Sample button for testing")
+ }
+ }
+ }
+
+ assertSingleMovableComponentExist()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After recompose there should still only exist one Component.
+ assertSingleMovableComponentExist()
+ }
+
+ @Test
+ fun movable_modifierEnabledToDisabledAndComponentUpdates() {
+ composeTestRule.setSubspaceContent {
+ var movableEnabled by remember { mutableStateOf(true) }
+ SpatialPanel(SubspaceModifier.testTag("panel").movable(enabled = movableEnabled)) {
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = { movableEnabled = !movableEnabled },
+ ) {
+ Text(text = "Sample button for testing")
+ }
+ }
+ }
+
+ assertSingleMovableComponentExist()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After recompose no Components should exist.
+ assertMovableComponentDoesNotExist()
+ }
+
+ @Test
+ fun movable_modifierOnPoseChangeUpdateAndComponentUpdates() {
+ composeTestRule.setSubspaceContent {
+ var onPoseReturnValue by remember { mutableStateOf(true) }
+ SpatialPanel(
+ SubspaceModifier.testTag("panel")
+ .movable(enabled = true, onPoseChange = { onPoseReturnValue })
+ ) {
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = { onPoseReturnValue = !onPoseReturnValue },
+ ) {
+ Text(text = "Sample button for testing")
+ }
+ }
+ }
+
+ assertSingleMovableComponentExist()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After recompose there should only exist one Component, not necessarily the same as
+ // before.
+ assertSingleMovableComponentExist()
+ }
+
+ @Test
+ fun movable_modifierDisableWithOnPoseChangeUpdateAndComponentRemoved() {
+ composeTestRule.setSubspaceContent {
+ var movableEnabled by remember { mutableStateOf(true) }
+ var onPoseReturnValue by remember { mutableStateOf(true) }
+ SpatialPanel(
+ SubspaceModifier.testTag("panel")
+ .movable(enabled = movableEnabled, onPoseChange = { onPoseReturnValue })
+ ) {
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = {
+ movableEnabled = !movableEnabled
+ onPoseReturnValue = !onPoseReturnValue
+ },
+ ) {
+ Text(text = "Sample button for testing")
+ }
+ }
+ }
+
+ assertSingleMovableComponentExist()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After recompose Component should be removed.
+ assertMovableComponentDoesNotExist()
+ }
+
+ @Test
+ fun movable_modifierEnabledWithOnPoseChangeUpdateAndComponentUpdates() {
+ composeTestRule.setSubspaceContent {
+ var movableEnabled by remember { mutableStateOf(false) }
+ var onPoseReturnValue by remember { mutableStateOf(true) }
+ SpatialPanel(
+ SubspaceModifier.testTag("panel")
+ .movable(enabled = movableEnabled, onPoseChange = { onPoseReturnValue })
+ ) {
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = {
+ movableEnabled = !movableEnabled
+ onPoseReturnValue = !onPoseReturnValue
+ },
+ ) {
+ Text(text = "Sample button for testing")
+ }
+ }
+ }
+
+ assertMovableComponentDoesNotExist()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After recompose Component should exist and be attached.
+ assertSingleMovableComponentExist()
+ }
+
+ @Test
+ fun movable_modifierDisabledThenEnabledAndComponentUpdates() {
+ composeTestRule.setSubspaceContent {
+ var movableEnabled by remember { mutableStateOf(true) }
+ SpatialPanel(SubspaceModifier.testTag("panel").movable(enabled = movableEnabled)) {
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = { movableEnabled = !movableEnabled },
+ ) {
+ Text(text = "Sample button for testing")
+ }
+ }
+ }
+
+ assertSingleMovableComponentExist()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After disabled, recompose Component should not exist.
+ assertMovableComponentDoesNotExist()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After enabled, recompose Component should be attached.
+ assertSingleMovableComponentExist()
+ }
+
+ @Test
+ fun movable_modifierOnPoseChangeTwiceUpdateAndComponentUpdates() {
+ composeTestRule.setSubspaceContent {
+ var onPoseReturnValue by remember { mutableStateOf(true) }
+ SpatialPanel(
+ SubspaceModifier.testTag("panel")
+ .movable(enabled = true, onPoseChange = { onPoseReturnValue })
+ ) {
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = { onPoseReturnValue = !onPoseReturnValue },
+ ) {
+ Text(text = "Sample button for testing")
+ }
+ }
+ }
+
+ assertSingleMovableComponentExist()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After recompose there should only exist one Component, not necessarily the same as
+ // before.
+ assertSingleMovableComponentExist()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After recompose there should only exist one Component, not necessarily the same as
+ // before.
+ assertSingleMovableComponentExist()
+ }
+
+ @Test
+ fun movable_modifierDisabledThenEnabledWithOnPoseChangeUpdateAndComponentUpdates() {
+ composeTestRule.setSubspaceContent {
+ var movableEnabled by remember { mutableStateOf(true) }
+ var onPoseReturnValue by remember { mutableStateOf(true) }
+ SpatialPanel(
+ SubspaceModifier.testTag("panel")
+ .movable(enabled = movableEnabled, onPoseChange = { onPoseReturnValue })
+ ) {
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = {
+ movableEnabled = !movableEnabled
+ onPoseReturnValue = !onPoseReturnValue
+ },
+ ) {
+ Text(text = "Sample button for testing")
+ }
+ }
+ }
+
+ assertSingleMovableComponentExist()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After disabled, recompose removes Component.
+ assertMovableComponentDoesNotExist()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After enabled, recompose Component should be attached. There should only exist one
+ // Component,
+ // not necessarily the same as before.
+ assertSingleMovableComponentExist()
+ }
+
+ @Test
+ fun movable_modifierEnabledThenDisabledWithOnPoseChangeUpdateAndComponentUpdates() {
+ composeTestRule.setSubspaceContent {
+ var movableEnabled by remember { mutableStateOf(false) }
+ var onPoseReturnValue by remember { mutableStateOf(true) }
+ SpatialPanel(
+ SubspaceModifier.testTag("panel")
+ .movable(enabled = movableEnabled, onPoseChange = { onPoseReturnValue })
+ ) {
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = {
+ movableEnabled = !movableEnabled
+ onPoseReturnValue = !onPoseReturnValue
+ },
+ ) {
+ Text(text = "Sample button for testing")
+ }
+ }
+ }
+
+ assertMovableComponentDoesNotExist()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After enabled, recompose Component should be attached. There should only exist one
+ // Component,
+ // not necessarily the same as before.
+ assertSingleMovableComponentExist()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After disabled, recompose removes Component.
+ assertMovableComponentDoesNotExist()
+ }
+
+ @Test
+ fun movable_columnEntity_noComponentByDefault() {
+ composeTestRule.setSubspaceContent {
+ SpatialColumn(SubspaceModifier.testTag("column")) {
+ SpatialPanel { Text(text = "Column") }
+ }
+ }
+ assertTrue(
+ composeTestRule
+ .onSubspaceNodeWithTag("column")
+ .fetchSemanticsNode()
+ .components
+ .isNullOrEmpty()
+ )
+ }
+
+ @Test
+ fun movable_columnEntity_noComponentWhenMovableIsEnabled() {
+ composeTestRule.setSubspaceContent {
+ SpatialColumn(SubspaceModifier.testTag("column").movable()) {
+ SpatialPanel { Text(text = "Column") }
+ }
+ }
+ assertMovableComponentDoesNotExist("column")
+ }
+
+ @Test
+ fun movable_columnEntity_noComponentWhenMovableIsDisabled() {
+ composeTestRule.setSubspaceContent {
+ SpatialColumn(SubspaceModifier.testTag("column").movable(false)) {
+ SpatialPanel { Text(text = "Column") }
+ }
+ }
+ assertMovableComponentDoesNotExist("column")
+ }
+
+ @Test
+ fun movable_rowEntity_noComponentByDefault() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.testTag("row")) { SpatialPanel { Text(text = "Row") } }
+ }
+ assertTrue(
+ composeTestRule
+ .onSubspaceNodeWithTag("row")
+ .fetchSemanticsNode()
+ .components
+ .isNullOrEmpty()
+ )
+ }
+
+ @Test
+ fun movable_rowEntity_noComponentWhenMovableIsEnabled() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.testTag("row").movable()) {
+ SpatialPanel { Text(text = "Row") }
+ }
+ }
+ assertMovableComponentDoesNotExist("row")
+ }
+
+ @Test
+ fun movable_rowEntity_noComponentWhenMovableIsDisabled() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.testTag("row").movable(false)) {
+ SpatialPanel { Text(text = "Row") }
+ }
+ }
+ assertMovableComponentDoesNotExist("row")
+ }
+
+ @Test
+ fun movable_volumeEntity_noComponentByDefault() {
+ composeTestRule.setSubspaceContent { Volume(SubspaceModifier.testTag("volume")) {} }
+ assertTrue(
+ composeTestRule
+ .onSubspaceNodeWithTag("volume")
+ .fetchSemanticsNode()
+ .components
+ .isNullOrEmpty()
+ )
+ }
+
+ @Test
+ fun movable_volumeEntity_noComponentWhenMovableIsEnabled() {
+ composeTestRule.setSubspaceContent {
+ Volume(SubspaceModifier.testTag("volume").movable()) {}
+ }
+ assertMovableComponentDoesNotExist("volume")
+ }
+
+ @Test
+ fun movable_volumeEntity_noComponentWhenMovableIsDisabled() {
+ composeTestRule.setSubspaceContent {
+ Volume(SubspaceModifier.testTag("volume").movable(false)) {}
+ }
+ assertMovableComponentDoesNotExist("volume")
+ }
+
+ private fun assertSingleMovableComponentExist(testTag: String = "panel") {
+ val components =
+ composeTestRule.onSubspaceNodeWithTag(testTag).fetchSemanticsNode().components
+ assertNotNull(components)
+ assertEquals(1, components.size)
+ assertIs<MovableComponent>(components[0])
+ }
+
+ private fun assertMovableComponentDoesNotExist(testTag: String = "panel") {
+ val components =
+ composeTestRule.onSubspaceNodeWithTag(testTag).fetchSemanticsNode().components
+ assertNotNull(components)
+ assertEquals(0, components.size)
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/OffsetTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/OffsetTest.kt
new file mode 100644
index 0000000..c8edacf
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/OffsetTest.kt
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.subspace.SpatialColumn
+import androidx.xr.compose.subspace.SpatialPanel
+import androidx.xr.compose.subspace.SpatialRow
+import androidx.xr.compose.testing.SubspaceTestingActivity
+import androidx.xr.compose.testing.assertPositionInRootIsEqualTo
+import androidx.xr.compose.testing.assertPositionIsEqualTo
+import androidx.xr.compose.testing.onSubspaceNodeWithTag
+import androidx.xr.compose.testing.setSubspaceContent
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [offset] modifier. */
+@RunWith(AndroidJUnit4::class)
+class OffsetTest {
+ @get:Rule val composeTestRule = createAndroidComposeRule<SubspaceTestingActivity>()
+
+ @Test
+ fun offset_positiveValuesArePositionedCorrectly() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(SubspaceModifier.testTag("panel").offset(20.dp, 20.dp, 20.dp)) {
+ Text(text = "Panel")
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertPositionInRootIsEqualTo(20.dp, 20.dp, 20.dp)
+ }
+
+ @Test
+ fun offset_negativeValuesArePositionedCorrectly() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(SubspaceModifier.testTag("panel").offset(-20.dp, -20.dp, -20.dp)) {
+ Text(text = "Panel")
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertPositionInRootIsEqualTo(-20.dp, -20.dp, -20.dp)
+ }
+
+ @Test
+ fun offset_combinedWithOtherModifiersArePositionedCorrectly() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(
+ SubspaceModifier.testTag("panel")
+ .width(100.dp)
+ .offset(10.dp, 10.dp, 10.dp)
+ .height(100.dp)
+ ) {
+ Text(text = "Panel")
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertPositionInRootIsEqualTo(10.dp, 10.dp, 10.dp)
+ }
+
+ @Test
+ fun offset_nestedLayoutsArePositionedCorrectly() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.width(1000.dp)) {
+ SpatialColumn(SubspaceModifier.weight(1f)) {
+ SpatialPanel(SubspaceModifier.testTag("panel1").offset(10.dp, 10.dp, 10.dp)) {
+ Text(text = "Panel 1")
+ }
+ }
+ SpatialColumn(SubspaceModifier.weight(1f)) {
+ SpatialPanel(SubspaceModifier.testTag("panel2").offset(10.dp, 10.dp, 10.dp)) {
+ Text(text = "Panel 2")
+ }
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel1")
+ .assertPositionInRootIsEqualTo(-240.dp, 10.dp, 10.dp) // x=-(1000/2/2) + 10
+ .assertPositionIsEqualTo(10.dp, 10.dp, 10.dp)
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel2")
+ .assertPositionInRootIsEqualTo(260.dp, 10.dp, 10.dp) // x=(1000/2/2) + 10
+ .assertPositionIsEqualTo(10.dp, 10.dp, 10.dp)
+ }
+
+ @Test
+ fun offset_updatesDynamically() {
+ composeTestRule.setSubspaceContent {
+ var offsetX by remember { mutableStateOf(0.dp) }
+ SpatialPanel(SubspaceModifier.testTag("panel").offset(x = offsetX)) {
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = { offsetX = offsetX + 10.dp }
+ ) {
+ Text(text = "Click to change offset")
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp, 0.dp)
+ composeTestRule.onNodeWithTag("button").performClick()
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertPositionInRootIsEqualTo(10.dp, 0.dp, 0.dp)
+ composeTestRule.onNodeWithTag("button").performClick().performClick().performClick()
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertPositionInRootIsEqualTo(40.dp, 0.dp, 0.dp)
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/OnGloballyPositionedModifierTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/OnGloballyPositionedModifierTest.kt
new file mode 100644
index 0000000..c964c64
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/OnGloballyPositionedModifierTest.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.compose.material3.Text
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.subspace.SpatialPanel
+import androidx.xr.compose.testing.SubspaceTestingActivity
+import androidx.xr.compose.testing.setSubspaceContent
+import androidx.xr.compose.testing.toDp
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [onGloballyPositioned] modifier. */
+@RunWith(AndroidJUnit4::class)
+class OnGloballyPositionedModifierTest {
+ @get:Rule val composeTestRule = createAndroidComposeRule<SubspaceTestingActivity>()
+
+ @Test
+ fun onGloballyPositioned_positionIsAlwaysSet() {
+ var coordinates: SubspaceLayoutCoordinates? = null
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(
+ SubspaceModifier.offset(20.dp, 20.dp, 20.dp).onGloballyPositioned {
+ coordinates = it
+ assertEquals(20.dp, coordinates?.poseInRoot?.translation?.x?.toDp())
+ assertEquals(20.dp, coordinates?.poseInRoot?.translation?.y?.toDp())
+ assertEquals(20.dp, coordinates?.poseInRoot?.translation?.z?.toDp())
+ }
+ ) {
+ Text(text = "Panel")
+ }
+ }
+
+ assertNotNull(coordinates)
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/PaddingTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/PaddingTest.kt
new file mode 100644
index 0000000..2835dc7
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/PaddingTest.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.compose.material3.Text
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.subspace.SpatialPanel
+import androidx.xr.compose.testing.SubspaceTestingActivity
+import androidx.xr.compose.testing.assertDepthIsEqualTo
+import androidx.xr.compose.testing.assertHeightIsEqualTo
+import androidx.xr.compose.testing.assertPositionInRootIsEqualTo
+import androidx.xr.compose.testing.assertWidthIsEqualTo
+import androidx.xr.compose.testing.onSubspaceNodeWithTag
+import androidx.xr.compose.testing.setSubspaceContent
+import kotlin.test.assertFailsWith
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [padding] modifier. */
+@RunWith(AndroidJUnit4::class)
+class PaddingTest {
+ @get:Rule val composeTestRule = createAndroidComposeRule<SubspaceTestingActivity>()
+
+ @Test
+ fun padding_settingValuesIndependentlySizesCorrectly() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(
+ SubspaceModifier.testTag("panel")
+ .size(100.dp)
+ .padding(
+ left = 20.dp,
+ top = 10.dp,
+ right = 10.dp,
+ bottom = 20.dp,
+ front = 10.dp,
+ back = 20.dp,
+ )
+ ) {
+ Text(text = "Panel")
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertPositionInRootIsEqualTo(5.dp, 5.dp, 5.dp)
+ .assertWidthIsEqualTo(70.dp)
+ .assertHeightIsEqualTo(70.dp)
+ .assertDepthIsEqualTo(70.dp)
+ }
+
+ @Test
+ fun padding_settingDirectionalValuesSizesCorrectly() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(
+ SubspaceModifier.testTag("panel")
+ .size(100.dp)
+ .padding(horizontal = 20.dp, vertical = 20.dp, depth = 20.dp)
+ ) {
+ Text(text = "Panel")
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp, 0.dp)
+ .assertWidthIsEqualTo(60.dp)
+ .assertHeightIsEqualTo(60.dp)
+ .assertDepthIsEqualTo(60.dp)
+ }
+
+ @Test
+ fun padding_settingAllValuesSizesCorrectly() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(SubspaceModifier.testTag("panel").size(100.dp).padding(all = 20.dp)) {
+ Text(text = "Panel")
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp, 0.dp)
+ .assertWidthIsEqualTo(60.dp)
+ .assertHeightIsEqualTo(60.dp)
+ .assertDepthIsEqualTo(60.dp)
+ }
+
+ @Test
+ fun padding_negativePaddingThrowsException() {
+ assertFailsWith<IllegalArgumentException> {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(SubspaceModifier.testTag("panel").size(100.dp).padding(top = -20.dp)) {
+ Text(text = "Panel")
+ }
+ }
+ }
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/ResizableModifierTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/ResizableModifierTest.kt
new file mode 100644
index 0000000..9ead8ea
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/ResizableModifierTest.kt
@@ -0,0 +1,562 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.subspace.SpatialColumn
+import androidx.xr.compose.subspace.SpatialPanel
+import androidx.xr.compose.subspace.SpatialRow
+import androidx.xr.compose.subspace.Volume
+import androidx.xr.compose.subspace.node.SubspaceSemanticsNode
+import androidx.xr.compose.testing.SubspaceTestingActivity
+import androidx.xr.compose.testing.onSubspaceNodeWithTag
+import androidx.xr.compose.testing.setSubspaceContent
+import androidx.xr.compose.unit.DpVolumeSize
+import androidx.xr.compose.unit.Meter.Companion.meters
+import androidx.xr.scenecore.ResizableComponent
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests for [Resizable] modifier.
+ *
+ * TODO(b/354723161): Add tests for minimum and maximum size.
+ */
+@RunWith(AndroidJUnit4::class)
+class ResizableModifierTest {
+ @get:Rule val composeTestRule = createAndroidComposeRule<SubspaceTestingActivity>()
+
+ @Test
+ fun resizable_noComponentByDefault() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(SubspaceModifier.testTag("panel")) { Text(text = "Panel") }
+ }
+
+ assertTrue(
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .fetchSemanticsNode()
+ .components
+ .isNullOrEmpty()
+ )
+ }
+
+ @Test
+ fun resizable_componentIsNotNullAndOnlyContainsSingleResizable() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(SubspaceModifier.testTag("panel").resizable()) { Text(text = "Panel") }
+ }
+
+ assertSingleResizableComponentExists()
+ }
+
+ @Test
+ fun resizable_modifierIsDisabledAndComponentDoesNotExist() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(SubspaceModifier.testTag("panel").resizable(enabled = false)) {
+ Text(text = "Panel")
+ }
+ }
+
+ assertResizableComponentDoesNotExist()
+ }
+
+ @Test
+ fun resizable_modifierDoesNotChangeAndComponentDoesNotUpdate() {
+ composeTestRule.setSubspaceContent {
+ var panelWidth by remember { mutableStateOf(50.dp) }
+ SpatialPanel(
+ SubspaceModifier.testTag("panel").width(panelWidth).resizable(enabled = true)
+ ) {
+ Button(modifier = Modifier.testTag("button"), onClick = { panelWidth += 50.dp }) {
+ Text(text = "Click to change width")
+ }
+ }
+ }
+
+ assertSingleResizableComponentExists()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After recompose we should continue to have the same component.
+ assertSingleResizableComponentExists()
+ }
+
+ @Test
+ fun resizable_modifierEnabledToDisabledAndComponentUpdates() {
+ composeTestRule.setSubspaceContent {
+ var resizableEnabled by remember { mutableStateOf(true) }
+ SpatialPanel(SubspaceModifier.testTag("panel").resizable(enabled = resizableEnabled)) {
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = { resizableEnabled = !resizableEnabled },
+ ) {
+ Text(text = "Click to change resizable")
+ }
+ }
+ }
+
+ assertSingleResizableComponentExists()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After recompose no Components should exist.
+ assertResizableComponentDoesNotExist()
+ }
+
+ @Test
+ fun resizable_modifierOnSizeChangeUpdateAndComponentUpdates() {
+ composeTestRule.setSubspaceContent {
+ var onSizeReturnValue by remember { mutableStateOf(true) }
+ SpatialPanel(
+ SubspaceModifier.testTag("panel")
+ .resizable(enabled = true, onSizeChange = { onSizeReturnValue })
+ ) {
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = { onSizeReturnValue = !onSizeReturnValue },
+ ) {
+ Text(text = "Click to change onSizeChange")
+ }
+ }
+ }
+
+ assertSingleResizableComponentExists()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After recompose we should still have one Component.
+ assertSingleResizableComponentExists()
+ }
+
+ @Test
+ fun resizable_modifierDisableWithOnSizeChangeUpdateAndComponentRemoved() {
+ composeTestRule.setSubspaceContent {
+ var resizableEnabled by remember { mutableStateOf(true) }
+ var onSizeReturnValue by remember { mutableStateOf(true) }
+ SpatialPanel(
+ SubspaceModifier.testTag("panel")
+ .resizable(enabled = resizableEnabled, onSizeChange = { onSizeReturnValue })
+ ) {
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = {
+ resizableEnabled = !resizableEnabled
+ onSizeReturnValue = !onSizeReturnValue
+ },
+ ) {
+ Text(text = "Click to change resizable and onSizeChange")
+ }
+ }
+ }
+
+ assertSingleResizableComponentExists()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After recompose Component should be removed.
+ assertResizableComponentDoesNotExist()
+ }
+
+ @Test
+ fun resizable_modifierEnabledWithOnSizeChangeUpdateAndComponentUpdates() {
+ composeTestRule.setSubspaceContent {
+ var resizableEnabled by remember { mutableStateOf(false) }
+ var onSizeReturnValue by remember { mutableStateOf(true) }
+ SpatialPanel(
+ SubspaceModifier.testTag("panel")
+ .resizable(enabled = resizableEnabled, onSizeChange = { onSizeReturnValue })
+ ) {
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = {
+ resizableEnabled = !resizableEnabled
+ onSizeReturnValue = !onSizeReturnValue
+ },
+ ) {
+ Text(text = "Click to change resizable and onSizeChange")
+ }
+ }
+ }
+
+ assertResizableComponentDoesNotExist()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After recompose Component should exist and be attached.
+ assertSingleResizableComponentExists()
+ }
+
+ @Test
+ fun resizable_modifierDisabledThenEnabledAndComponentUpdates() {
+ composeTestRule.setSubspaceContent {
+ var resizableEnabled by remember { mutableStateOf(true) }
+ SpatialPanel(SubspaceModifier.testTag("panel").resizable(enabled = resizableEnabled)) {
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = { resizableEnabled = !resizableEnabled },
+ ) {
+ Text(text = "Click to change resizable")
+ }
+ }
+ }
+
+ assertSingleResizableComponentExists()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After disabled, recompose Component should not exist.
+ assertResizableComponentDoesNotExist()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After enabled, recompose Component should be attached.
+ assertSingleResizableComponentExists()
+ }
+
+ @Test
+ fun resizable_modifierOnSizeChangeTwiceUpdateAndComponentUpdates() {
+ composeTestRule.setSubspaceContent {
+ var onSizeReturnValue by remember { mutableStateOf(true) }
+ SpatialPanel(
+ SubspaceModifier.testTag("panel")
+ .resizable(enabled = true, onSizeChange = { onSizeReturnValue })
+ ) {
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = { onSizeReturnValue = !onSizeReturnValue },
+ ) {
+ Text(text = "Click to change onSizeChange")
+ }
+ }
+ }
+
+ assertSingleResizableComponentExists()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After recompose should only have one Component.
+ assertSingleResizableComponentExists()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After recompose should only have one Component.
+ assertSingleResizableComponentExists()
+ }
+
+ @Test
+ fun resizable_modifierDisabledThenEnabledWithOnSizeChangeUpdateAndComponentUpdates() {
+ composeTestRule.setSubspaceContent {
+ var resizableEnabled by remember { mutableStateOf(true) }
+ var onSizeReturnValue by remember { mutableStateOf(true) }
+ SpatialPanel(
+ SubspaceModifier.testTag("panel")
+ .resizable(enabled = resizableEnabled, onSizeChange = { onSizeReturnValue })
+ ) {
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = {
+ resizableEnabled = !resizableEnabled
+ onSizeReturnValue = !onSizeReturnValue
+ },
+ ) {
+ Text(text = "Click to change resizabe and onSizeChange")
+ }
+ }
+ }
+
+ assertSingleResizableComponentExists()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After disabled, recompose removes Component.
+ assertResizableComponentDoesNotExist()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After enabled, recompose removes Component.
+ assertSingleResizableComponentExists()
+ }
+
+ @Test
+ fun resizable_modifierEnabledThenDisabledWithOnSizeChangeUpdateAndComponentUpdates() {
+ composeTestRule.setSubspaceContent {
+ var resizableEnabled by remember { mutableStateOf(false) }
+ var onSizeReturnValue by remember { mutableStateOf(true) }
+ SpatialPanel(
+ SubspaceModifier.testTag("panel")
+ .resizable(enabled = resizableEnabled, onSizeChange = { onSizeReturnValue })
+ ) {
+ Button(
+ modifier = Modifier.testTag("button"),
+ onClick = {
+ resizableEnabled = !resizableEnabled
+ onSizeReturnValue = !onSizeReturnValue
+ },
+ ) {
+ Text(text = "Click to change resizabe and onSizeChange")
+ }
+ }
+ }
+
+ assertResizableComponentDoesNotExist()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After enabled, recompose removes Component.
+ assertSingleResizableComponentExists()
+
+ composeTestRule.onNodeWithTag("button").performClick()
+
+ // After disabled, recompose removes Component.
+ assertResizableComponentDoesNotExist()
+ }
+
+ @Test
+ fun resizable_modifierMaxSizeIsSet() {
+ val maxSize = DpVolumeSize(500.dp, 500.dp, 500.dp)
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(SubspaceModifier.testTag("panel").resizable(maximumSize = maxSize)) {}
+ }
+ assertResizableComponentMaxSizeIsSet(size = maxSize)
+ }
+
+ @Test
+ fun resizable_modifierMaxSizeIsNotSet() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(SubspaceModifier.testTag("panel").resizable()) {}
+ }
+ assertResizableComponentMaxSizeIsNotSet()
+ }
+
+ @Test
+ fun resizable_modifierMinSizeIsSet() {
+ val minSize = DpVolumeSize(100.dp, 100.dp, 100.dp)
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(SubspaceModifier.testTag("panel").resizable(minimumSize = minSize)) {}
+ }
+ assertResizableComponentMinSizeIsSet(size = minSize)
+ }
+
+ @Test
+ fun resizable_modifierMinSizeIsNotSet() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(SubspaceModifier.testTag("panel").resizable()) {}
+ }
+ assertResizableComponentMinSizeIsNotSet()
+ }
+
+ @Test
+ fun resizable_columnEntity_noComponentByDefault() {
+ composeTestRule.setSubspaceContent {
+ SpatialColumn(SubspaceModifier.testTag("column")) {
+ SpatialPanel { Text(text = "Column") }
+ }
+ }
+ assertTrue(
+ composeTestRule
+ .onSubspaceNodeWithTag("column")
+ .fetchSemanticsNode()
+ .components
+ .isNullOrEmpty()
+ )
+ }
+
+ @Test
+ fun resizable_columnEntity_noComponentWhenResizableIsEnabled() {
+ composeTestRule.setSubspaceContent {
+ SpatialColumn(SubspaceModifier.testTag("column").resizable()) {
+ SpatialPanel { Text(text = "Column") }
+ }
+ }
+ assertResizableComponentDoesNotExist("column")
+ }
+
+ @Test
+ fun resizable_columnEntity_noComponentWhenResizableIsDisabled() {
+ composeTestRule.setSubspaceContent {
+ SpatialColumn(SubspaceModifier.testTag("column").resizable(false)) {
+ SpatialPanel { Text(text = "Column") }
+ }
+ }
+ assertResizableComponentDoesNotExist("column")
+ }
+
+ @Test
+ fun resizable_rowEntity_noComponentByDefault() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.testTag("row")) { SpatialPanel { Text(text = "Row") } }
+ }
+ assertTrue(
+ composeTestRule
+ .onSubspaceNodeWithTag("row")
+ .fetchSemanticsNode()
+ .components
+ .isNullOrEmpty()
+ )
+ }
+
+ @Test
+ fun resizable_rowEntity_noComponentWhenResizableIsEnabled() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.testTag("row").resizable()) {
+ SpatialPanel { Text(text = "Row") }
+ }
+ }
+ assertResizableComponentDoesNotExist("row")
+ }
+
+ @Test
+ fun resizable_rowEntity_noComponentWhenResizableIsDisabled() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.testTag("row").resizable(false)) {
+ SpatialPanel { Text(text = "Row") }
+ }
+ }
+ assertResizableComponentDoesNotExist("row")
+ }
+
+ @Test
+ fun resizable_volumeEntity_noComponentByDefault() {
+ composeTestRule.setSubspaceContent { Volume(SubspaceModifier.testTag("volume")) {} }
+ assertTrue(
+ composeTestRule
+ .onSubspaceNodeWithTag("volume")
+ .fetchSemanticsNode()
+ .components
+ .isNullOrEmpty()
+ )
+ }
+
+ @Test
+ fun resizable_volumeEntity_noComponentWhenResizableIsEnabled() {
+ composeTestRule.setSubspaceContent {
+ Volume(SubspaceModifier.testTag("volume").resizable()) {}
+ }
+ assertResizableComponentDoesNotExist("volume")
+ }
+
+ @Test
+ fun resizable_volumeEntity_noComponentWhenResizableIsDisabled() {
+ composeTestRule.setSubspaceContent {
+ Volume(SubspaceModifier.testTag("volume").resizable(false)) {}
+ }
+ assertResizableComponentDoesNotExist("volume")
+ }
+
+ private fun assertSingleResizableComponentExists(testTag: String = "panel") {
+ val components =
+ composeTestRule.onSubspaceNodeWithTag(testTag).fetchSemanticsNode().components
+ assertNotNull(components)
+ assertEquals(1, components.size)
+ assertIs<ResizableComponent>(components[0])
+ }
+
+ private fun assertResizableComponentDoesNotExist(testTag: String = "panel") {
+ val components =
+ composeTestRule.onSubspaceNodeWithTag(testTag).fetchSemanticsNode().components
+ assertNotNull(components)
+ assertEquals(0, components.size)
+ }
+
+ private fun assertResizableComponentMaxSizeIsSet(
+ testTag: String = "panel",
+ size: DpVolumeSize
+ ) {
+ val resizableComponent =
+ composeTestRule
+ .onSubspaceNodeWithTag(testTag)
+ .fetchSemanticsNode()
+ .getLastComponent<ResizableComponent>()
+
+ val maxWidth = resizableComponent.maximumSize.width.meters.toDp()
+ val maxHeight = resizableComponent.maximumSize.height.meters.toDp()
+
+ assertEquals(size.width, maxWidth)
+ assertEquals(size.height, maxHeight)
+ }
+
+ private fun assertResizableComponentMaxSizeIsNotSet(testTag: String = "panel") {
+ val resizableComponent =
+ composeTestRule
+ .onSubspaceNodeWithTag(testTag)
+ .fetchSemanticsNode()
+ .getLastComponent<ResizableComponent>()
+
+ val maxWidth = resizableComponent.maximumSize.width.meters.toDp()
+ val maxHeight = resizableComponent.maximumSize.height.meters.toDp()
+
+ assertEquals(Dp.Infinity, maxWidth)
+ assertEquals(Dp.Infinity, maxHeight)
+ }
+
+ private fun assertResizableComponentMinSizeIsSet(
+ testTag: String = "panel",
+ size: DpVolumeSize
+ ) {
+ val resizableComponent =
+ composeTestRule
+ .onSubspaceNodeWithTag(testTag)
+ .fetchSemanticsNode()
+ .getLastComponent<ResizableComponent>()
+
+ val minWidth = resizableComponent.minimumSize.width.meters.toDp()
+ val minHeight = resizableComponent.minimumSize.height.meters.toDp()
+
+ assertEquals(size.width, minWidth)
+ assertEquals(size.height, minHeight)
+ }
+
+ private fun assertResizableComponentMinSizeIsNotSet(testTag: String = "panel") {
+ val resizableComponent =
+ composeTestRule
+ .onSubspaceNodeWithTag(testTag)
+ .fetchSemanticsNode()
+ .getLastComponent<ResizableComponent>()
+
+ val minWidth = resizableComponent.minimumSize.width.meters.toDp()
+ val minHeight = resizableComponent.minimumSize.height.meters.toDp()
+
+ assertEquals(DpVolumeSize.Zero.width, minWidth)
+ assertEquals(DpVolumeSize.Zero.height, minHeight)
+ }
+
+ private inline fun <reified T> SubspaceSemanticsNode.getLastComponent(): T {
+ assertNotNull(components)
+ val component = components!!.last()
+ assertIs<T>(component)
+ return component
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/RotateTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/RotateTest.kt
new file mode 100644
index 0000000..850e9a9
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/RotateTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.compose.material3.Text
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.subspace.SpatialPanel
+import androidx.xr.compose.testing.SubspaceTestingActivity
+import androidx.xr.compose.testing.assertRotationInRootIsEqualTo
+import androidx.xr.compose.testing.onSubspaceNodeWithTag
+import androidx.xr.compose.testing.setSubspaceContent
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Vector3
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [rotate] modifier. */
+@RunWith(AndroidJUnit4::class)
+class RotateTest {
+ @get:Rule val composeTestRule = createAndroidComposeRule<SubspaceTestingActivity>()
+
+ @Test
+ fun rotation_canApplySingleRotation() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(SubspaceModifier.testTag("panel").rotate(90.0f, 0.0f, 0.0f)) {
+ Text(text = "Panel")
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertRotationInRootIsEqualTo(Quaternion(0.70710677f, 0.0f, 0.0f, 0.70710677f))
+ }
+
+ @Test
+ fun rotation_canRotationAcrossTwoAxis() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(
+ SubspaceModifier.testTag("panel").rotate(Vector3(0.0f, 1.0f, 1.0f), 90.0f)
+ ) {
+ Text(text = "Panel")
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertRotationInRootIsEqualTo(Quaternion(0.0f, 0.49999997f, 0.49999997f, 0.70710677f))
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/ScaleModifierTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/ScaleModifierTest.kt
new file mode 100644
index 0000000..8f35ae46
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/ScaleModifierTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.subspace.SpatialPanel
+import androidx.xr.compose.subspace.SubspaceComposable
+import androidx.xr.compose.subspace.node.SubspaceSemanticsNode
+import androidx.xr.compose.testing.SubspaceTestingActivity
+import androidx.xr.compose.testing.onSubspaceNodeWithTag
+import androidx.xr.compose.testing.setSubspaceContent
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNotNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for scale modifiere. */
+@RunWith(AndroidJUnit4::class)
+class ScaleModifierTest {
+ @get:Rule val composeTestRule = createAndroidComposeRule<SubspaceTestingActivity>()
+
+ @Test
+ fun scale_modifierAppliedToEntity() {
+ composeTestRule.setSubspaceContent { PanelContent("panel", 0.5f) }
+
+ val panelNode = assertSingleNode("panel")
+ assertEquals(0.5f, panelNode.scale)
+ }
+
+ @Test
+ fun negativeScale_throwsException() {
+ assertFailsWith<IllegalArgumentException> {
+ composeTestRule.setSubspaceContent { PanelContent("panel", -0.5f) }
+ }
+ }
+
+ private fun assertSingleNode(testTag: String): SubspaceSemanticsNode {
+ val subspaceNode = composeTestRule.onSubspaceNodeWithTag(testTag).fetchSemanticsNode()
+ assertNotNull(subspaceNode)
+ return subspaceNode
+ }
+
+ @SubspaceComposable
+ @Composable
+ private fun PanelContent(testTag: String, scale: Float? = null) {
+ var modifier = SubspaceModifier.testTag(testTag).size(100.dp)
+
+ scale?.let { modifier = modifier.scale(it) }
+
+ SpatialPanel(modifier = modifier) { Text(text = "Panel") }
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/SizeTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/SizeTest.kt
new file mode 100644
index 0000000..0529d73
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/SizeTest.kt
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.compose.material3.Text
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.subspace.SpatialPanel
+import androidx.xr.compose.subspace.SpatialRow
+import androidx.xr.compose.testing.SubspaceTestingActivity
+import androidx.xr.compose.testing.assertDepthIsEqualTo
+import androidx.xr.compose.testing.assertHeightIsEqualTo
+import androidx.xr.compose.testing.assertWidthIsEqualTo
+import androidx.xr.compose.testing.onSubspaceNodeWithTag
+import androidx.xr.compose.testing.setSubspaceContent
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for size modifiers. */
+@RunWith(AndroidJUnit4::class)
+class SizeTest {
+ @get:Rule val composeTestRule = createAndroidComposeRule<SubspaceTestingActivity>()
+
+ @Test
+ fun size_individualModifiers_panelsAreSizedCorrectly() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(
+ SubspaceModifier.testTag("panel").width(20.dp).height(20.dp).depth(20.dp)
+ ) {
+ Text(text = "Panel")
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertWidthIsEqualTo(20.dp)
+ .assertHeightIsEqualTo(20.dp)
+ .assertDepthIsEqualTo(20.dp)
+ }
+
+ @Test
+ fun size_combinedModifier_panelsAreSizedCorrectly() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(SubspaceModifier.testTag("panel").size(20.dp)) { Text(text = "Panel") }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertWidthIsEqualTo(20.dp)
+ .assertHeightIsEqualTo(20.dp)
+ .assertDepthIsEqualTo(20.dp)
+ }
+
+ @Test
+ fun size_combinedModifier_panelsRespectParentSizeConstraints() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.size(10.dp)) {
+ SpatialPanel(SubspaceModifier.testTag("panel").size(20.dp)) { Text(text = "Panel") }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertWidthIsEqualTo(10.dp)
+ .assertHeightIsEqualTo(10.dp)
+ .assertDepthIsEqualTo(10.dp)
+ }
+
+ @Test
+ fun size_individualRequiredModifiers_panelsAreSizedCorrectly() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(
+ SubspaceModifier.testTag("panel")
+ .requiredWidth(20.dp)
+ .requiredHeight(20.dp)
+ .requiredDepth(20.dp)
+ ) {
+ Text(text = "Panel")
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertWidthIsEqualTo(20.dp)
+ .assertHeightIsEqualTo(20.dp)
+ .assertDepthIsEqualTo(20.dp)
+ }
+
+ @Test
+ fun size_combinedRequiredModifier_panelsAreSizedCorrectly() {
+ composeTestRule.setSubspaceContent {
+ SpatialPanel(SubspaceModifier.testTag("panel").requiredSize(20.dp)) {
+ Text(text = "Panel")
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertWidthIsEqualTo(20.dp)
+ .assertHeightIsEqualTo(20.dp)
+ .assertDepthIsEqualTo(20.dp)
+ }
+
+ @Test
+ fun size_combinedRequiredModifier_panelsOverrideParentSizeConstraints() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.size(10.dp)) {
+ SpatialPanel(SubspaceModifier.testTag("panel").requiredSize(20.dp)) {
+ Text(text = "Panel")
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertWidthIsEqualTo(20.dp)
+ .assertHeightIsEqualTo(20.dp)
+ .assertDepthIsEqualTo(20.dp)
+ }
+
+ @Test
+ fun size_individualFillModifiers_panelsAreSizedCorrectly() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.size(20.dp)) {
+ SpatialPanel(
+ SubspaceModifier.testTag("panel").fillMaxWidth().fillMaxHeight().fillMaxDepth()
+ ) {
+ Text(text = "Panel")
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertWidthIsEqualTo(20.dp)
+ .assertHeightIsEqualTo(20.dp)
+ .assertDepthIsEqualTo(20.dp)
+ }
+
+ @Test
+ fun size_individualFillModifiersWithFraction_panelsAreSizedCorrectly() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.size(20.dp)) {
+ SpatialPanel(
+ SubspaceModifier.testTag("panel")
+ .fillMaxWidth(0.5f)
+ .fillMaxHeight(0.5f)
+ .fillMaxDepth(0.5f)
+ ) {
+ Text(text = "Panel")
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertWidthIsEqualTo(10.dp)
+ .assertHeightIsEqualTo(10.dp)
+ .assertDepthIsEqualTo(10.dp)
+ }
+
+ @Test
+ fun size_combinedFillModifier_panelsAreSizedCorrectly() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.size(20.dp)) {
+ SpatialPanel(SubspaceModifier.testTag("panel").fillMaxSize()) {
+ Text(text = "Panel")
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertWidthIsEqualTo(20.dp)
+ .assertHeightIsEqualTo(20.dp)
+ .assertDepthIsEqualTo(20.dp)
+ }
+
+ @Test
+ fun size_combinedFillModifierWithFraction_panelsAreSizedCorrectly() {
+ composeTestRule.setSubspaceContent {
+ SpatialRow(SubspaceModifier.size(20.dp)) {
+ SpatialPanel(SubspaceModifier.testTag("panel").fillMaxSize(0.5f)) {
+ Text(text = "Panel")
+ }
+ }
+ }
+
+ composeTestRule
+ .onSubspaceNodeWithTag("panel")
+ .assertWidthIsEqualTo(10.dp)
+ .assertHeightIsEqualTo(10.dp)
+ .assertDepthIsEqualTo(10.dp)
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/SpatialAlignmentTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/SpatialAlignmentTest.kt
new file mode 100644
index 0000000..a4c5151
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/SpatialAlignmentTest.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.unit.IntVolumeSize
+import androidx.xr.runtime.math.Vector3
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [SpatialAlignment]. */
+@RunWith(AndroidJUnit4::class)
+class SpatialAlignmentTest {
+ @Test
+ fun spatialBiasAlignment_horizontalOffset() {
+ assertThat(SpatialAlignment.CenterLeft.horizontalOffset(width = 100, space = 200))
+ .isEqualTo(-50)
+ assertThat(SpatialAlignment.Center.horizontalOffset(width = 100, space = 200)).isEqualTo(0)
+ assertThat(SpatialAlignment.CenterRight.horizontalOffset(width = 100, space = 200))
+ .isEqualTo(50)
+
+ assertThat(SpatialAlignment.Left.offset(width = 100, space = 200)).isEqualTo(-50)
+ assertThat(SpatialAlignment.CenterHorizontally.offset(width = 100, space = 200))
+ .isEqualTo(0)
+ assertThat(SpatialAlignment.Right.offset(width = 100, space = 200)).isEqualTo(50)
+ }
+
+ @Test
+ fun spatialBiasAlignment_verticalOffset() {
+ assertThat(SpatialAlignment.BottomCenter.verticalOffset(height = 100, space = 200))
+ .isEqualTo(-50)
+ assertThat(SpatialAlignment.Center.verticalOffset(height = 100, space = 200)).isEqualTo(0)
+ assertThat(SpatialAlignment.TopCenter.verticalOffset(height = 100, space = 200))
+ .isEqualTo(50)
+
+ assertThat(SpatialAlignment.Bottom.offset(height = 100, space = 200)).isEqualTo(-50)
+ assertThat(SpatialAlignment.CenterVertically.offset(height = 100, space = 200)).isEqualTo(0)
+ assertThat(SpatialAlignment.Top.offset(height = 100, space = 200)).isEqualTo(50)
+ }
+
+ @Test
+ fun spatialBiasAlignment_depthOffset() {
+ assertThat(SpatialBiasAlignment(0f, 0f, -1f).depthOffset(depth = 100, space = 200))
+ .isEqualTo(-50)
+ assertThat(SpatialBiasAlignment(0f, 0f, 0f).depthOffset(depth = 100, space = 200))
+ .isEqualTo(0)
+ assertThat(SpatialBiasAlignment(0f, 0f, 1f).depthOffset(depth = 100, space = 200))
+ .isEqualTo(50)
+
+ assertThat(SpatialAlignment.Back.offset(depth = 100, space = 200)).isEqualTo(-50)
+ assertThat(SpatialAlignment.CenterDepthwise.offset(depth = 100, space = 200)).isEqualTo(0)
+ assertThat(SpatialAlignment.Front.offset(depth = 100, space = 200)).isEqualTo(50)
+ }
+
+ @Test
+ fun spatialBiasAlignment_position() {
+ val size = IntVolumeSize(100, 100, 100)
+ val space = IntVolumeSize(200, 200, 200)
+ assertThat(SpatialBiasAlignment(-1f, -1f, -1f).position(size, space))
+ .isEqualTo(Vector3(-50f, -50f, -50f))
+ assertThat(SpatialBiasAlignment(0f, 0f, 0f).position(size, space)).isEqualTo(Vector3.Zero)
+ assertThat(SpatialBiasAlignment(1f, 1f, 1f).position(size, space))
+ .isEqualTo(Vector3(50f, 50f, 50f))
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/SpatialShapeTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/SpatialShapeTest.kt
new file mode 100644
index 0000000..0384250c
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/SpatialShapeTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.layout
+
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SpatialShapeTest {
+ @Test
+ fun roundedCorner_zero() {
+ assertThat(
+ SpatialRoundedCornerShape(CornerSize(0))
+ .computeCornerRadius(maxWidth = 100f, maxHeight = 100f, density = Density(1.0f))
+ )
+ .isEqualTo(0f)
+ }
+
+ @Test
+ fun roundedCorner_dp() {
+ assertThat(
+ SpatialRoundedCornerShape(CornerSize(25.dp))
+ .computeCornerRadius(maxWidth = 100f, maxHeight = 200f, density = Density(1.0f))
+ )
+ .isEqualTo(25f)
+ }
+
+ @Test
+ fun roundedCorner_increasedDensity() {
+ // Returns a larger radius scaled with Density.
+ assertThat(
+ SpatialRoundedCornerShape(CornerSize(25.dp))
+ .computeCornerRadius(maxWidth = 100f, maxHeight = 200f, density = Density(2.0f))
+ )
+ .isEqualTo(50f)
+ }
+
+ @Test
+ fun roundedCorner_extraDp() {
+ // Corner radius is capped at 50% of the smallest side (60). Since the set corner size of
+ // 150
+ // is above this, corner radius becomes equal to the cap.
+ assertThat(
+ SpatialRoundedCornerShape(CornerSize(150.dp))
+ .computeCornerRadius(maxWidth = 120f, maxHeight = 200f, density = Density(1.0f))
+ )
+ .isEqualTo(60f)
+ }
+
+ @Test
+ fun roundedCorner_percent() {
+ // Takes 25% of the smallest side.
+ assertThat(
+ SpatialRoundedCornerShape(CornerSize(25))
+ .computeCornerRadius(maxWidth = 100f, maxHeight = 200f, density = Density(1.0f))
+ )
+ .isEqualTo(25f)
+ }
+
+ @Test
+ fun roundedCorner_extraPercent() {
+ // Anything above 50% has no effect, so this computes 50% of the smallest side.
+ assertThat(
+ SpatialRoundedCornerShape(CornerSize(75))
+ .computeCornerRadius(maxWidth = 100f, maxHeight = 200f, density = Density(1.0f))
+ )
+ .isEqualTo(50f)
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutNodeTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutNodeTest.kt
new file mode 100644
index 0000000..8307bbc
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutNodeTest.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.subspace.node
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.subspace.SubspaceComposable
+import androidx.xr.compose.subspace.layout.CoreContentlessEntity
+import androidx.xr.compose.subspace.layout.SubspaceLayout
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.layout.testTag
+import androidx.xr.compose.testing.SubspaceTestingActivity
+import androidx.xr.compose.testing.createFakeSession
+import androidx.xr.compose.testing.onSubspaceNodeWithTag
+import androidx.xr.compose.testing.setSubspaceContent
+import androidx.xr.scenecore.Entity
+import androidx.xr.scenecore.Session
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [SubspaceLayoutNode]. */
+@RunWith(AndroidJUnit4::class)
+class SubspaceLayoutNodeTest {
+ @get:Rule val composeTestRule = createAndroidComposeRule<SubspaceTestingActivity>()
+
+ private lateinit var testSession: Session
+
+ @Before
+ fun setUp() {
+ testSession = createFakeSession(composeTestRule.activity)
+ }
+
+ @Test
+ fun subspaceLayoutNode_shouldParentNodesProperly() {
+ val parentEntity = testSession.createEntity("ParentEntity")
+ composeTestRule.setSubspaceContent {
+ EntityLayout(entity = parentEntity) {
+ EntityLayout(
+ entity = testSession.createEntity("ChildEntity"),
+ modifier = SubspaceModifier.testTag("Child"),
+ )
+ }
+ }
+
+ assertThat(
+ composeTestRule
+ .onSubspaceNodeWithTag("Child")
+ .fetchSemanticsNode()
+ .coreEntity
+ ?.entity
+ ?.getParent()
+ )
+ .isEqualTo(parentEntity)
+ }
+
+ @Test
+ fun subspaceLayoutNode_shouldParentNodesWhenThereIsAGapBetween() {
+ val parentEntity = testSession.createEntity("ParentEntity")
+ composeTestRule.setSubspaceContent {
+ EntityLayout(entity = parentEntity) {
+ EntityLayout(modifier = SubspaceModifier.testTag("Child")) {
+ EntityLayout(
+ entity = testSession.createEntity("GrandChildEntity"),
+ modifier = SubspaceModifier.testTag("GrandChild"),
+ )
+ }
+ }
+ }
+
+ assertThat(composeTestRule.onSubspaceNodeWithTag("Child").fetchSemanticsNode().coreEntity)
+ .isNull()
+ assertThat(
+ composeTestRule
+ .onSubspaceNodeWithTag("GrandChild")
+ .fetchSemanticsNode()
+ .coreEntity
+ ?.entity
+ ?.getParent()
+ )
+ .isEqualTo(parentEntity)
+ }
+
+ @Composable
+ @SubspaceComposable
+ private fun EntityLayout(
+ modifier: SubspaceModifier = SubspaceModifier,
+ entity: Entity? = null,
+ content: @Composable @SubspaceComposable () -> Unit = {},
+ ) {
+ SubspaceLayout(
+ content = content,
+ modifier = modifier,
+ coreEntity = entity?.let { CoreContentlessEntity(it) },
+ ) { _, _ ->
+ layout(0, 0, 0) {}
+ }
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/unit/DpVolumeSizeTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/unit/DpVolumeSizeTest.kt
new file mode 100644
index 0000000..0813537
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/unit/DpVolumeSizeTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.unit
+
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.scenecore.Dimensions
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DpVolumeSizeTest {
+ @Test
+ fun dpVolumeSize_isCreated() {
+ val dpVolumeSize = DpVolumeSize(0.dp, 0.dp, 0.dp)
+
+ assertNotNull(dpVolumeSize)
+ }
+
+ @Test
+ fun dpVolumeSize_toString_returnsString() {
+ val dpVolumeSize = DpVolumeSize(0.dp, 0.dp, 0.dp)
+
+ val toString = dpVolumeSize.toString()
+
+ assertThat(toString).isEqualTo("DpVolumeSize(width=0.0.dp, height=0.0.dp, depth=0.0.dp)")
+ }
+
+ @Test
+ fun toDimensionsInMeter_returnsCorrectDimensions() {
+ val dpVolumeSize = DpVolumeSize(1151.856f.dp, 1151.856f.dp, 1151.856f.dp)
+
+ val dimensions = dpVolumeSize.toDimensionsInMeters()
+
+ assertThat(dimensions).isEqualTo(Dimensions(1f, 1f, 1f))
+ }
+
+ @Test
+ fun dpVolumeSize_fromMeters_returnsCorrectDpVolumeSize() {
+ val dpVolumeSize = Dimensions(1f, 1f, 1f).toDpVolumeSize()
+
+ assertThat(dpVolumeSize).isEqualTo(DpVolumeSize(1151.856f.dp, 1151.856f.dp, 1151.856f.dp))
+ }
+
+ @Test
+ fun dpVolumeSize_zero_returnsCorrectDpVolumeSize() {
+ val zero = DpVolumeSize.Zero
+
+ assertThat(zero).isEqualTo(DpVolumeSize(0f.dp, 0f.dp, 0f.dp))
+ }
+
+ @Test
+ fun toDimensionsInMeters_andFromMeters_returnsCorrectDpVolumeSize() {
+ val testDpVolumeSize = DpVolumeSize(1111.11f.dp, 1111.11f.dp, 1111.11f.dp)
+
+ val dimensions = testDpVolumeSize.toDimensionsInMeters()
+ val fromMetersDpVolumeSize = dimensions.toDpVolumeSize()
+
+ assertThat(fromMetersDpVolumeSize)
+ .isEqualTo(DpVolumeSize(1111.11f.dp, 1111.11f.dp, 1111.11f.dp))
+ }
+}
+
+/**
+ * Converts this [DpVolumeSize] to a [Dimensions] object in meters.
+ *
+ * @return a [Dimensions] object representing the volume size in meters
+ */
+internal fun DpVolumeSize.toDimensionsInMeters(): Dimensions =
+ Dimensions(width.toMeter().value, height.toMeter().value, depth.toMeter().value)
+
+/**
+ * Creates a [DpVolumeSize] from a [Dimensions] object in meters.
+ *
+ * @param dimensions the [Dimensions] object in meters.
+ * @return a [DpVolumeSize] object representing the same volume size in Dp.
+ */
+internal fun Dimensions.toDpVolumeSize(): DpVolumeSize =
+ DpVolumeSize(Meter(width).toDp(), Meter(height).toDp(), Meter(depth).toDp())
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/unit/IntVolumeSizeTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/unit/IntVolumeSizeTest.kt
new file mode 100644
index 0000000..bdc84e2
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/unit/IntVolumeSizeTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.unit
+
+import androidx.compose.ui.unit.Density
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.scenecore.Dimensions
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class IntVolumeSizeTest {
+ private val UNIT_DENSITY = Density(density = 1.0f, fontScale = 1.0f)
+
+ @Test
+ fun intVolumeSize_toString_returnsString() {
+ val intVolumeSize = IntVolumeSize(0, 0, 0)
+
+ val toString = intVolumeSize.toString()
+
+ assertThat(toString).isEqualTo("IntVolumeSize(width=0, height=0, depth=0)")
+ }
+
+ @Test
+ fun toDimensionsInMeters_returnsCorrectDimensions() {
+ val intVolumeSize = IntVolumeSize(10367, 10367, 10367)
+
+ val dimensions = intVolumeSize.toDimensionsInMeters(UNIT_DENSITY)
+
+ assertThat(dimensions.width).isWithin(0.0003f).of(9.0f)
+ assertThat(dimensions.height).isWithin(0.0003f).of(9.0f)
+ assertThat(dimensions.depth).isWithin(0.0003f).of(9.0f)
+ }
+
+ @Test
+ fun toDimensionsInMeters_returnsCorrectDimensions_doubleDensity() {
+ val intVolumeSize = IntVolumeSize(10367, 10367, 10367)
+ val DOUBLE_DENSITY = Density(density = 2.0f, fontScale = 2.0f)
+
+ val dimensions = intVolumeSize.toDimensionsInMeters(DOUBLE_DENSITY)
+
+ // When pixels are twice as dense, we expect the Meters equivalent to be half.
+ assertThat(dimensions.width).isWithin(0.0002f).of(4.5f)
+ assertThat(dimensions.height).isWithin(0.0002f).of(4.5f)
+ assertThat(dimensions.depth).isWithin(0.0002f).of(4.5f)
+ }
+
+ @Test
+ fun intVolumeSize_zero_returnsCorrectIntVolumeSize() {
+ val intVolumeSize = IntVolumeSize.Zero
+
+ assertThat(intVolumeSize).isEqualTo(IntVolumeSize(0, 0, 0))
+ }
+
+ @Test
+ fun intVolumeSize_fromMeters_returnsCorrectIntVolumeSize() {
+ val dimensions = Dimensions(9.0f, 9.0f, 9.0f)
+
+ val intVolumeSize = dimensions.toIntVolumeSize(UNIT_DENSITY)
+
+ assertThat(intVolumeSize).isEqualTo(IntVolumeSize(10367, 10367, 10367))
+ }
+
+ @Test
+ fun toDimensionsInMeters_andFromMeters_returnsCorrectIntVolumeSize() {
+ val intVolumeSize = IntVolumeSize(10000, 10000, 10000)
+
+ val dimensions = intVolumeSize.toDimensionsInMeters(UNIT_DENSITY)
+ val fromMetersIntVolumeSize = dimensions.toIntVolumeSize(UNIT_DENSITY)
+
+ assertThat(fromMetersIntVolumeSize).isEqualTo(IntVolumeSize(10000, 10000, 10000))
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/unit/MeterTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/unit/MeterTest.kt
new file mode 100644
index 0000000..3a8d696
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/unit/MeterTest.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.unit
+
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.compose.unit.Meter.Companion.centimeters
+import androidx.xr.compose.unit.Meter.Companion.meters
+import androidx.xr.compose.unit.Meter.Companion.millimeters
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class MeterTest {
+ private val UNIT_DENSITY = Density(density = 1.0f, fontScale = 1.0f)
+
+ @Test
+ fun meter_toDp() {
+ assertThat(1.meters.toDp()).isEqualTo(1151.856f.dp)
+ }
+
+ @Test
+ fun roundToPx_roundsToNearestPixel() {
+ assertThat(1.meters.roundToPx(UNIT_DENSITY)).isEqualTo(1152)
+ }
+
+ @Test
+ fun roundToPx_roundsToNearestPixel_doubleDensity() {
+ val DOUBLE_DENSITY = Density(density = 2.0f, fontScale = 2.0f)
+
+ // Twice the density, twice the pixels.
+ assertThat(1.meters.roundToPx(DOUBLE_DENSITY)).isEqualTo(2304)
+ }
+
+ @Test
+ fun dp_toMeter() {
+ assertThat(10.dp.toMeter()).isEqualTo(Meter(0.008681641f))
+ assertThat(Dp.Infinity.toMeter()).isEqualTo(Meter.Infinity)
+ assertThat(Dp.Unspecified.toMeter()).isEqualTo(Meter.NaN)
+
+ // Reference: "Hairline elements take up no space, but will draw a single pixel, independent
+ // of the device's resolution and density."
+ // https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Dp#Hairline()
+ //
+ // Currently, hairlines are exactly 0 meters.
+ assertThat(Dp.Hairline.toMeter()).isEqualTo(Meter(0.0f))
+ }
+
+ @Test
+ fun meter_toMm() {
+ assertThat(22.meters.toMm()).isEqualTo(22000f)
+ }
+
+ @Test
+ fun meter_toCm() {
+ assertThat(11.meters.toCm()).isEqualTo(1100f)
+ }
+
+ @Test
+ fun meter_toM() {
+ assertThat(11.meters.toM()).isEqualTo(11f)
+ }
+
+ @Test
+ fun meter_toPx() {
+ assertThat(5.meters.toPx(UNIT_DENSITY)).isEqualTo(5759.28f)
+ }
+
+ @Test
+ fun meter_roundToPx() {
+ assertThat(5.meters.roundToPx(UNIT_DENSITY)).isEqualTo(5759)
+ }
+
+ @Test
+ fun meter_todp() {
+ assertThat(5.meters.toDp()).isEqualTo(5759.28f.dp)
+ }
+
+ @Test
+ fun meter_isSpecified() {
+ assertThat(5.meters.isSpecified).isTrue()
+ assertThat(Meter.NaN.isSpecified).isFalse()
+ }
+
+ @Test
+ fun meter_isFinite() {
+ assertThat(5.meters.isFinite).isTrue()
+ assertThat(Meter.Infinity.isFinite).isFalse()
+ }
+
+ @Test
+ fun meter_fromIntMilli() {
+ val test: Meter = 22.millimeters
+ assertThat(test).isEqualTo(Meter(0.022000002f))
+ }
+
+ @Test
+ fun meter_funFloatMilli() {
+ val test = 22f.millimeters
+ assertThat(test).isEqualTo(Meter(0.022000002f))
+ }
+
+ @Test
+ fun meter_funDoubleMilli() {
+ val test = 22.0.millimeters
+ assertThat(test).isEqualTo(Meter(0.022000002f))
+ }
+
+ @Test
+ fun meter_funIntCm() {
+ val test = 22.centimeters
+ assertThat(test).isEqualTo(Meter(0.22f))
+ }
+
+ @Test
+ fun meter_funFloatCm() {
+ val test = 22f.centimeters
+ assertThat(test).isEqualTo(Meter(0.22f))
+ }
+
+ @Test
+ fun meter_funDoubleCm() {
+ val test = 22.0.centimeters
+ assertThat(test).isEqualTo(Meter(0.22f))
+ }
+
+ @Test
+ fun meter_funIntM() {
+ val test = 22.meters
+ assertThat(test).isEqualTo(Meter(22f))
+ }
+
+ @Test
+ fun meter_funFloatM() {
+ val test = 22f.meters
+ assertThat(test).isEqualTo(Meter(22f))
+ }
+
+ @Test
+ fun meter_funDoubleM() {
+ val test = 22.0.meters
+ assertThat(test).isEqualTo(Meter(22f))
+ }
+
+ @Test
+ fun intTimesMeters() {
+ val test: Meter = 22.meters
+ assertThat(11 * test).isEqualTo(242.meters)
+ }
+
+ @Test
+ fun floatTimesMeters() {
+ val test: Meter = 22.meters
+ assertThat(11f * test).isEqualTo(242.meters)
+ }
+
+ @Test
+ fun doubleTimesMeters() {
+ val test: Meter = 22.meters
+ assertThat(11.0 * test).isEqualTo(242.meters)
+ }
+
+ @Test
+ fun intDivMeters() {
+ val test: Meter = 22.meters
+ assertThat(test / 11).isEqualTo(2.meters)
+ }
+
+ @Test
+ fun floatDivMeters() {
+ val test: Meter = 22.meters
+ assertThat(test / 11f).isEqualTo(2.meters)
+ }
+
+ @Test
+ fun doubleDivMeters() {
+ val test: Meter = 22.meters
+ assertThat(test / 11.0).isEqualTo(2.meters)
+ }
+
+ @Test
+ fun px_toMeter_toPx() {
+ val density = Density(2.789f)
+ assertThat(Meter.fromPixel(28.9f, density).toPx(density)).isWithin(1.0e-5f).of(28.9f)
+ }
+}
diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/unit/VolumeConstraintsTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/unit/VolumeConstraintsTest.kt
new file mode 100644
index 0000000..4fe5493
--- /dev/null
+++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/unit/VolumeConstraintsTest.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.unit
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class VolumeConstraintsTest {
+
+ @Test
+ fun volumeConstraints_hasBoundedWidth_returnsTrue() {
+ val volumeConstraints = VolumeConstraints(0, 0, 0, 0, 0, 0)
+
+ val hasBoundedWidth = volumeConstraints.hasBoundedWidth
+
+ assertThat(hasBoundedWidth).isTrue()
+ }
+
+ @Test
+ fun volumeConstraints_hasBoundedWidth_returnsFalse() {
+ val volumeConstraints = VolumeConstraints(0, VolumeConstraints.INFINITY, 0, 0, 0, 0)
+
+ val hasBoundedWidth = volumeConstraints.hasBoundedWidth
+
+ assertThat(hasBoundedWidth).isFalse()
+ }
+
+ @Test
+ fun volumeConstraints_hasBoundedHeight_returnsTrue() {
+ val volumeConstraints = VolumeConstraints(0, 0, 0, 0, 0, 0)
+
+ val hasBoundedHeight = volumeConstraints.hasBoundedHeight
+
+ assertThat(hasBoundedHeight).isTrue()
+ }
+
+ @Test
+ fun volumeConstraints_hasBoundedHeight_returnsFalse() {
+ val volumeConstraints = VolumeConstraints(0, 0, 0, VolumeConstraints.INFINITY, 0, 0)
+
+ val hasBoundedHeight = volumeConstraints.hasBoundedHeight
+
+ assertThat(hasBoundedHeight).isFalse()
+ }
+
+ @Test
+ fun volumeConstraints_hasBoundedDepth_returnsTrue() {
+ val volumeConstraints = VolumeConstraints(0, 0, 0, 0, 0, 0)
+
+ val hasBoundedDepth = volumeConstraints.hasBoundedDepth
+
+ assertThat(hasBoundedDepth).isTrue()
+ }
+
+ @Test
+ fun volumeConstraints_hasBoundedDepth_returnsFalse() {
+ val volumeConstraints = VolumeConstraints(0, 0, 0, 0, 0, VolumeConstraints.INFINITY)
+
+ val hasBoundedDepth = volumeConstraints.hasBoundedDepth
+
+ assertThat(hasBoundedDepth).isFalse()
+ }
+
+ @Test
+ fun volumeConstraints_toString_returnsString() {
+ val volumeConstraints = VolumeConstraints(0, 0, 0, 0, 0, 0)
+
+ val toString = volumeConstraints.toString()
+
+ assertThat(toString).isEqualTo("width: 0-0, height: 0-0, depth=0-0")
+ }
+
+ @Test
+ fun volumeConstraints_copy_returnsCorrectVolumeConstraints() {
+ val volumeConstraints = VolumeConstraints(0, 0, 0, 0, 0, 0)
+
+ val copyVolumeConstraints = volumeConstraints.copy()
+
+ assertThat(copyVolumeConstraints).isEqualTo(volumeConstraints)
+ }
+
+ @Test
+ fun volumeConstraints_infinity_returnsCorrectVolumeConstraints() {
+ val infinity = VolumeConstraints.INFINITY
+
+ assertThat(infinity).isEqualTo(Int.MAX_VALUE)
+ }
+
+ @Test
+ fun volumeConstraints_constrain_returnsCorrectVolumeConstraints() {
+ val volumeConstraints = VolumeConstraints(1, 2, 1, 2, 1, 2)
+ val otherVolumeConstraints = VolumeConstraints(4, 5, 4, 5, 4, 5)
+
+ val constrainedVolumeConstraints = volumeConstraints.constrain(otherVolumeConstraints)
+
+ assertThat(constrainedVolumeConstraints).isEqualTo(VolumeConstraints(2, 2, 2, 2, 2, 2))
+ }
+
+ @Test
+ fun volumeConstraints_constrainWidth_returnsCorrectWidth() {
+ val volumeConstraints = VolumeConstraints(1, 2, 1, 2, 1, 2)
+
+ val constrainedWidth = volumeConstraints.constrainWidth(3)
+
+ assertThat(constrainedWidth).isEqualTo(2)
+ }
+
+ @Test
+ fun volumeConstraints_constrainHeight_returnsCorrectHeight() {
+ val volumeConstraints = VolumeConstraints(1, 2, 1, 2, 1, 2)
+
+ val constrainedHeight = volumeConstraints.constrainHeight(3)
+
+ assertThat(constrainedHeight).isEqualTo(2)
+ }
+
+ @Test
+ fun volumeConstraints_constrainDepth_returnsCorrectDepth() {
+ val volumeConstraints = VolumeConstraints(1, 2, 1, 2, 1, 2)
+
+ val constrainedDepth = volumeConstraints.constrainDepth(3)
+
+ assertThat(constrainedDepth).isEqualTo(2)
+ }
+
+ @Test
+ fun volumeConstraints_offset_returnsCorrectVolumeConstraints() {
+ val volumeConstraints = VolumeConstraints(1, 2, 1, 2, 1, 2)
+
+ val offsetVolumeConstraints = volumeConstraints.offset(1, 2, 2)
+
+ assertThat(offsetVolumeConstraints).isEqualTo(VolumeConstraints(2, 3, 3, 4, 3, 4))
+ }
+}
diff --git a/xr/compose/material3/OWNERS b/xr/compose/material3/OWNERS
new file mode 100644
index 0000000..533ffc7
--- /dev/null
+++ b/xr/compose/material3/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 1524104
[email protected]
[email protected]
[email protected]
diff --git a/xr/compose/material3/integration-tests/testapp/build.gradle b/xr/compose/material3/integration-tests/testapp/build.gradle
new file mode 100644
index 0000000..1033dd7
--- /dev/null
+++ b/xr/compose/material3/integration-tests/testapp/build.gradle
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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.
+ */
+plugins {
+ id("AndroidXPlugin")
+ id("AndroidXComposePlugin")
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ implementation(libs.kotlinStdlib)
+
+ implementation(project(":appcompat:appcompat"))
+ implementation(project(":activity:activity-compose"))
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:foundation:foundation-layout"))
+ implementation(project(":compose:material3:material3"))
+ implementation("androidx.compose.material:material-icons-core:1.6.8")
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:ui:ui"))
+ implementation(project(":compose:ui:ui-tooling-preview"))
+
+ // Adaptive (CAMAL) dependencies
+ implementation("androidx.compose.material3.adaptive:adaptive:1.1.0-alpha03")
+ implementation("androidx.compose.material3.adaptive:adaptive-layout:1.1.0-alpha03")
+ implementation("androidx.compose.material3.adaptive:adaptive-navigation:1.1.0-alpha03")
+ implementation(project(":compose:material3:material3-adaptive-navigation-suite"))
+
+ // XR Adaptive integration library
+ implementation(project(":xr:compose:material3:material3"))
+ implementation(project(":xr:compose:compose"))
+ implementation(project(":xr:scenecore:scenecore"))
+
+ // TODO(b/374796755): Write unit-tests for this test app
+}
+
+android {
+ compileSdk 35
+ defaultConfig {
+ minSdk 32
+ }
+ namespace "androidx.xr.compose.material3.integration.testapp"
+}
+
+androidx {
+ // TODO: b/326456246
+ optOutJSpecify = true
+}
diff --git a/xr/compose/material3/integration-tests/testapp/src/main/AndroidManifest.xml b/xr/compose/material3/integration-tests/testapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..e486a56
--- /dev/null
+++ b/xr/compose/material3/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<!--
+ ~ Copyright (C) 2024 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
+ ~
+ ~ http://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
+ -->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-permission android:name="android.permission.SCENE_UNDERSTANDING" />
+
+ <application
+ android:label="XR Compose Adaptive Test App"
+ android:supportsRtl="true" tools:ignore="GoogleAppIndexingWarning"
+ android:allowBackup="false"
+ android:enableOnBackInvokedCallback="true">
+
+ <activity
+ android:name=".MainActivity"
+ android:configChanges="uiMode"
+ android:exported="true"
+ android:theme="@android:style/Theme.Material.Light.NoActionBar">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/Destination.kt b/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/Destination.kt
new file mode 100644
index 0000000..3f11715
--- /dev/null
+++ b/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/Destination.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.material3.integration.testapp
+
+internal enum class Destination {
+ Dialog,
+}
+
+internal val Destination.label: String
+ get() =
+ when (this) {
+ Destination.Dialog -> "Dialog"
+ }
diff --git a/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/DetailPane.kt b/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/DetailPane.kt
new file mode 100644
index 0000000..213f2b3
--- /dev/null
+++ b/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/DetailPane.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.material3.integration.testapp
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.window.Dialog
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3Api::class)
+@Composable
+internal fun DetailPane(navigator: ThreePaneScaffoldNavigator<Destination>) {
+ val destination = navigator.currentDestination?.contentKey ?: return
+ val coroutineScope = rememberCoroutineScope()
+ Scaffold(
+ topBar = { TopAppBar(title = { Text("XR Compose Adaptive: ${destination.label}") }) },
+ ) { innerPadding ->
+ Surface(Modifier.fillMaxSize().padding(innerPadding)) {
+ when (destination) {
+ Destination.Dialog -> DetailPaneDialog()
+ }
+ }
+ }
+ BackHandler { coroutineScope.launch { navigator.navigateBack() } }
+}
diff --git a/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/DetailPaneDialog.kt b/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/DetailPaneDialog.kt
new file mode 100644
index 0000000..f34fe02
--- /dev/null
+++ b/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/DetailPaneDialog.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.material3.integration.testapp
+
+import android.widget.Toast
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.Card
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.xr.compose.spatial.SpatialDialog
+
+@Composable
+internal fun DetailPaneDialog() {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ SimpleDialog()
+ MaterialAlertDialog()
+ XrElevatedDialog()
+ }
+}
+
+@Composable
+private fun SimpleDialog() {
+ DialogWithShowButton("Compose UI Dialog") { showDialog ->
+ Dialog(onDismissRequest = { showDialog.value = false }) {
+ Card(Modifier.fillMaxWidth(0.8f).fillMaxHeight(0.5f)) {
+ Text("This is a simple Dialog with a Material Card inside.")
+ }
+ }
+ }
+}
+
+@Composable
+private fun MaterialAlertDialog() {
+ val context = LocalContext.current
+ DialogWithShowButton("Material Alert Dialog") { showDialog ->
+ AlertDialog(
+ onDismissRequest = { showDialog.value = false },
+ text = { Text("This is a Material AlertDialog") },
+ confirmButton = {
+ Button(
+ onClick = {
+ Toast.makeText(context, "Confirm button clicked", Toast.LENGTH_LONG).show()
+ showDialog.value = false
+ },
+ ) {
+ Text("Confirm")
+ }
+ },
+ )
+ }
+}
+
+@Composable
+private fun XrElevatedDialog() {
+ DialogWithShowButton("XR ElevatedDialog") { showDialog ->
+ SpatialDialog(onDismissRequest = { showDialog.value = false }) {
+ Card(Modifier.fillMaxWidth(0.8f).fillMaxHeight(0.5f)) {
+ Text("This is an XR ElevatedDialog with a Material Card inside.")
+ }
+ }
+ }
+}
+
+@Composable
+private fun DialogWithShowButton(
+ text: String,
+ content: @Composable (MutableState<Boolean>) -> Unit,
+) {
+ val showDialog = remember { mutableStateOf(false) }
+ Button(onClick = { showDialog.value = true }) { Text(text) }
+ if (showDialog.value) {
+ content(showDialog)
+ }
+}
diff --git a/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/ListPane.kt b/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/ListPane.kt
new file mode 100644
index 0000000..daa0caa
--- /dev/null
+++ b/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/ListPane.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.material3.integration.testapp
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
+import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class)
+@Composable
+internal fun ListPane(navigator: ThreePaneScaffoldNavigator<Destination>) {
+ val coroutineScope = rememberCoroutineScope()
+ Scaffold(
+ topBar = { TopAppBar(title = { Text("XR Compose Adaptive Sample") }) },
+ ) { innerPadding ->
+ LazyColumn(Modifier.fillMaxSize().padding(innerPadding)) {
+ items(Destination.values()) { destination ->
+ Surface(
+ onClick = {
+ coroutineScope.launch {
+ navigator.navigateTo(
+ ListDetailPaneScaffoldRole.Detail,
+ contentKey = destination,
+ )
+ }
+ },
+ ) {
+ ListItem(headlineContent = { Text(destination.label) })
+ }
+ }
+ }
+ }
+}
diff --git a/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/MainActivity.kt b/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/MainActivity.kt
new file mode 100644
index 0000000..44b8d83
--- /dev/null
+++ b/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/MainActivity.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+// TODO(b/289518597): Remove this SuppressLint
+@file:SuppressLint("NullAnnotationGroup")
+@file:OptIn(
+ ExperimentalMaterial3AdaptiveApi::class,
+ ExperimentalMaterial3Api::class,
+ ExperimentalMaterial3XrApi::class
+)
+
+package androidx.xr.compose.material3.integration.testapp
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.material3.adaptive.layout.AnimatedPane
+import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
+import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
+import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
+import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
+import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
+import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
+import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.xr.compose.material3.EnableXrComponentOverrides
+import androidx.xr.compose.material3.ExperimentalMaterial3XrApi
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent { EnableXrComponentOverrides { Content() } }
+ }
+}
+
+private val navSuiteType = mutableStateOf(NavigationSuiteType.NavigationRail)
+
+@Composable
+private fun Content() {
+ var navSuiteSelectedItem by remember { mutableStateOf(NavSuiteItem.HOME) }
+
+ NavigationSuiteScaffold(
+ navigationSuiteItems = {
+ NavSuiteItem.values().forEach { item ->
+ item(
+ selected = navSuiteSelectedItem == item,
+ onClick = { navSuiteSelectedItem = item },
+ icon = { Icon(item.icon, contentDescription = item.label) },
+ label = { Text(item.label) },
+ )
+ }
+ },
+ layoutType = navSuiteType.value
+ ) {
+ when (navSuiteSelectedItem) {
+ NavSuiteItem.HOME -> {
+ Home()
+ }
+ NavSuiteItem.SETTINGS -> {
+ XrSettingsPane(navSuiteType)
+ }
+ }
+ }
+}
+
+@Composable
+private fun Home() {
+ val navigator: ThreePaneScaffoldNavigator<Destination> =
+ rememberListDetailPaneScaffoldNavigator(
+ initialDestinationHistory =
+ listOf(ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.List))
+ )
+ ListDetailPaneScaffold(
+ directive = navigator.scaffoldDirective,
+ value = navigator.scaffoldValue,
+ listPane = { AnimatedPane { ListPane(navigator) } },
+ detailPane = { AnimatedPane { DetailPane(navigator) } },
+ )
+}
+
+private const val TAG = "MainActivity"
diff --git a/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/NavSuiteItem.kt b/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/NavSuiteItem.kt
new file mode 100644
index 0000000..d40f924
--- /dev/null
+++ b/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/NavSuiteItem.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.material3.integration.testapp
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.ui.graphics.vector.ImageVector
+
+enum class NavSuiteItem {
+ HOME,
+ SETTINGS,
+}
+
+val NavSuiteItem.label: String
+ get() =
+ when (this) {
+ NavSuiteItem.HOME -> "Home"
+ NavSuiteItem.SETTINGS -> "Settings"
+ }
+
+val NavSuiteItem.icon: ImageVector
+ get() =
+ when (this) {
+ NavSuiteItem.HOME -> Icons.Default.Home
+ NavSuiteItem.SETTINGS -> Icons.Default.Settings
+ }
diff --git a/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/XrSettingsPane.kt b/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/XrSettingsPane.kt
new file mode 100644
index 0000000..e1faf48
--- /dev/null
+++ b/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/XrSettingsPane.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.material3.integration.testapp
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.xr.compose.platform.LocalSession
+import androidx.xr.compose.platform.LocalSpatialCapabilities
+
+@Composable
+internal fun XrSettingsPane(navSuiteType: MutableState<NavigationSuiteType>) {
+ Scaffold { innerPadding ->
+ Column(
+ modifier = Modifier.padding(innerPadding),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ ListItem(headlineContent = { XrModeButton() })
+ ListItem(headlineContent = { NavigationSuiteTypeButton(navSuiteType) })
+ }
+ }
+}
+
+@Composable
+private fun XrModeButton() {
+ val session = LocalSession.current
+ val isDeviceXr = session != null
+ val isFullSpaceMode = LocalSpatialCapabilities.current.isSpatialUiEnabled
+
+ Button(
+ modifier = Modifier.fillMaxWidth().height(56.dp).padding(horizontal = 16.dp),
+ enabled = isDeviceXr,
+ onClick = {
+ if (isFullSpaceMode) {
+ session?.requestHomeSpaceMode()
+ } else {
+ session?.requestFullSpaceMode()
+ }
+ }
+ ) {
+ Text(
+ text = if (isDeviceXr) "Toggle FullSpace/HomeSpace Mode" else "XR unsupported",
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ }
+}
+
+@Composable
+private fun NavigationSuiteTypeButton(navSuiteType: MutableState<NavigationSuiteType>) {
+ Button(
+ modifier = Modifier.fillMaxWidth().height(56.dp).padding(horizontal = 16.dp),
+ onClick = {
+ if (navSuiteType.value == NavigationSuiteType.NavigationRail) {
+ navSuiteType.value = NavigationSuiteType.NavigationBar
+ } else {
+ navSuiteType.value = NavigationSuiteType.NavigationRail
+ }
+ }
+ ) {
+ Text(text = "Toggle NavRail/NavBar", style = MaterialTheme.typography.bodyLarge)
+ }
+}
diff --git a/xr/compose/material3/material3/api/current.txt b/xr/compose/material3/material3/api/current.txt
new file mode 100644
index 0000000..1074b90
--- /dev/null
+++ b/xr/compose/material3/material3/api/current.txt
@@ -0,0 +1,40 @@
+// Signature format: 4.0
+package androidx.xr.compose.material3 {
+
+ public final class EnableXrComponentOverridesKt {
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public static void EnableXrComponentOverrides(optional androidx.xr.compose.material3.XrComponentOverrideEnabler overrideEnabler, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
+ @SuppressCompatibility @kotlin.RequiresOptIn(message="This material XR API is experimental and is likely to change or to be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterial3XrApi {
+ }
+
+ public final class NavigationBarKt {
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public static void NavigationBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ }
+
+ public final class NavigationRailKt {
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public static void NavigationRail(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? header, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ }
+
+ @SuppressCompatibility @androidx.xr.compose.material3.ExperimentalMaterial3XrApi @kotlin.jvm.JvmInline public final value class XrComponentOverride {
+ field public static final androidx.xr.compose.material3.XrComponentOverride.Companion Companion;
+ }
+
+ public static final class XrComponentOverride.Companion {
+ method @SuppressCompatibility @androidx.xr.compose.material3.ExperimentalMaterial3XrApi @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public String getNavigationBar();
+ method @SuppressCompatibility @androidx.xr.compose.material3.ExperimentalMaterial3XrApi @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public String getNavigationRail();
+ property @SuppressCompatibility @androidx.xr.compose.material3.ExperimentalMaterial3XrApi @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public String NavigationBar;
+ property @SuppressCompatibility @androidx.xr.compose.material3.ExperimentalMaterial3XrApi @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public String NavigationRail;
+ }
+
+ @SuppressCompatibility @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public interface XrComponentOverrideEnabler {
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public boolean shouldOverrideComponent(androidx.xr.compose.material3.XrComponentOverrideEnablerContext, String component);
+ }
+
+ @SuppressCompatibility @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public sealed interface XrComponentOverrideEnablerContext {
+ method @androidx.compose.runtime.Composable public boolean isSpatializationEnabled();
+ property @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public abstract boolean isSpatializationEnabled;
+ }
+
+}
+
diff --git a/camera/camera-effects-still-portrait/api/res-current.txt b/xr/compose/material3/material3/api/res-current.txt
similarity index 100%
copy from camera/camera-effects-still-portrait/api/res-current.txt
copy to xr/compose/material3/material3/api/res-current.txt
diff --git a/xr/compose/material3/material3/api/restricted_current.txt b/xr/compose/material3/material3/api/restricted_current.txt
new file mode 100644
index 0000000..1074b90
--- /dev/null
+++ b/xr/compose/material3/material3/api/restricted_current.txt
@@ -0,0 +1,40 @@
+// Signature format: 4.0
+package androidx.xr.compose.material3 {
+
+ public final class EnableXrComponentOverridesKt {
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public static void EnableXrComponentOverrides(optional androidx.xr.compose.material3.XrComponentOverrideEnabler overrideEnabler, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
+ @SuppressCompatibility @kotlin.RequiresOptIn(message="This material XR API is experimental and is likely to change or to be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterial3XrApi {
+ }
+
+ public final class NavigationBarKt {
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public static void NavigationBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ }
+
+ public final class NavigationRailKt {
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public static void NavigationRail(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? header, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ }
+
+ @SuppressCompatibility @androidx.xr.compose.material3.ExperimentalMaterial3XrApi @kotlin.jvm.JvmInline public final value class XrComponentOverride {
+ field public static final androidx.xr.compose.material3.XrComponentOverride.Companion Companion;
+ }
+
+ public static final class XrComponentOverride.Companion {
+ method @SuppressCompatibility @androidx.xr.compose.material3.ExperimentalMaterial3XrApi @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public String getNavigationBar();
+ method @SuppressCompatibility @androidx.xr.compose.material3.ExperimentalMaterial3XrApi @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public String getNavigationRail();
+ property @SuppressCompatibility @androidx.xr.compose.material3.ExperimentalMaterial3XrApi @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public String NavigationBar;
+ property @SuppressCompatibility @androidx.xr.compose.material3.ExperimentalMaterial3XrApi @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public String NavigationRail;
+ }
+
+ @SuppressCompatibility @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public interface XrComponentOverrideEnabler {
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public boolean shouldOverrideComponent(androidx.xr.compose.material3.XrComponentOverrideEnablerContext, String component);
+ }
+
+ @SuppressCompatibility @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public sealed interface XrComponentOverrideEnablerContext {
+ method @androidx.compose.runtime.Composable public boolean isSpatializationEnabled();
+ property @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public abstract boolean isSpatializationEnabled;
+ }
+
+}
+
diff --git a/xr/compose/material3/material3/build.gradle b/xr/compose/material3/material3/build.gradle
new file mode 100644
index 0000000..fdf7bf0
--- /dev/null
+++ b/xr/compose/material3/material3/build.gradle
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+
+import androidx.build.KotlinTarget
+import androidx.build.LibraryType
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("AndroidXComposePlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ // Add dependencies here
+ implementation(project(":compose:material3:adaptive:adaptive"))
+ implementation(project(":compose:material3:material3"))
+ implementation(project(":compose:material3:material3-adaptive-navigation-suite"))
+
+ implementation(project(":xr:compose:compose"))
+}
+
+android {
+ compileSdk 35
+ defaultConfig {
+ minSdk 32
+ }
+ namespace "androidx.xr.compose.material3"
+}
+
+androidx {
+ name = "Compose Material3 XR"
+ description = "Compose Material3 components for XR"
+ type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
+ inceptionYear = "2024"
+ kotlinTarget = KotlinTarget.KOTLIN_1_9
+ // TODO: b/326456246
+ optOutJSpecify = true
+}
diff --git a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/EnableXrComponentOverrides.kt b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/EnableXrComponentOverrides.kt
new file mode 100644
index 0000000..898a521
--- /dev/null
+++ b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/EnableXrComponentOverrides.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.material3
+
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.LocalNavigationBarComponentOverride
+import androidx.compose.material3.LocalNavigationRailComponentOverride
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.ProvidedValue
+import androidx.xr.compose.platform.LocalSpatialCapabilities
+
+/**
+ * Clients can wrap their Compose hierarchy in this function to dynamically enable XR components
+ * when in the proper environment.
+ *
+ * The [overrideEnabler] param determines whether each component will use an XR version.
+ */
+@ExperimentalMaterial3XrApi
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+public fun EnableXrComponentOverrides(
+ overrideEnabler: XrComponentOverrideEnabler = DefaultXrComponentOverrideEnabler,
+ content: @Composable () -> Unit,
+) {
+ val context = XrComponentOverrideEnablerContextImpl
+
+ // Override CompositionLocals for all ComponentOverrides, as specified by the provided enabler.
+ val componentOverrides =
+ buildList<ProvidedValue<*>> {
+ with(overrideEnabler) {
+ if (context.shouldOverrideComponent(XrComponentOverride.NavigationRail)) {
+ add(
+ LocalNavigationRailComponentOverride provides
+ XrNavigationRailComponentOverride
+ )
+ }
+ if (context.shouldOverrideComponent(XrComponentOverride.NavigationBar)) {
+ add(
+ LocalNavigationBarComponentOverride provides
+ XrNavigationBarComponentOverride
+ )
+ }
+ }
+ }
+ CompositionLocalProvider(values = componentOverrides.toTypedArray(), content = content)
+}
+
+/** Interface that a client can provide to enable/disable XR overrides on a per-component basis. */
+@ExperimentalMaterial3XrApi
+public interface XrComponentOverrideEnabler {
+ /**
+ * Used to determine whether the XR version of a given component should be used.
+ *
+ * @param component the component that may or may not use the XR version
+ * @return whether the XR version of this component should be used
+ */
+ @Composable
+ @ExperimentalMaterial3XrApi
+ public fun XrComponentOverrideEnablerContext.shouldOverrideComponent(
+ component: XrComponentOverride
+ ): Boolean
+}
+
+/** Information about the current XR environment. */
+@ExperimentalMaterial3XrApi
+public sealed interface XrComponentOverrideEnablerContext {
+ /** Whether the user is in an environment that supports XR spatialization. */
+ @ExperimentalMaterial3XrApi @get:Composable public val isSpatializationEnabled: Boolean
+}
+
+/** The set of Material Components that can be overridden on XR. */
+@ExperimentalMaterial3XrApi
+@JvmInline
+public value class XrComponentOverride private constructor(private val name: String) {
+ public companion object {
+ /** Material3 NavigationRail. */
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3XrApi
+ @ExperimentalMaterial3XrApi
+ public val NavigationRail: XrComponentOverride = XrComponentOverride("NavigationRail")
+
+ /** Material3 NavigationBar. */
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3XrApi
+ @ExperimentalMaterial3XrApi
+ public val NavigationBar: XrComponentOverride = XrComponentOverride("NavigationBar")
+ }
+}
+
+@ExperimentalMaterial3XrApi
+private object XrComponentOverrideEnablerContextImpl : XrComponentOverrideEnablerContext {
+ override val isSpatializationEnabled: Boolean
+ @Composable get() = LocalSpatialCapabilities.current.isSpatialUiEnabled
+}
+
+@OptIn(ExperimentalMaterial3XrApi::class)
+private object DefaultXrComponentOverrideEnabler : XrComponentOverrideEnabler {
+ @Composable
+ override fun XrComponentOverrideEnablerContext.shouldOverrideComponent(
+ component: XrComponentOverride
+ ): Boolean = isSpatializationEnabled
+}
diff --git a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/ExperimentalMaterial3XrApi.kt b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/ExperimentalMaterial3XrApi.kt
new file mode 100644
index 0000000..01e0eae
--- /dev/null
+++ b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/ExperimentalMaterial3XrApi.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.material3
+
+@RequiresOptIn(
+ "This material XR API is experimental and is likely to change or to be removed in the future."
+)
+@Retention(AnnotationRetention.BINARY)
+public annotation class ExperimentalMaterial3XrApi
diff --git a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/NavigationBar.kt b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/NavigationBar.kt
new file mode 100644
index 0000000..a0de7c1
--- /dev/null
+++ b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/NavigationBar.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.material3
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.NavigationBarComponentOverride
+import androidx.compose.material3.NavigationBarComponentOverrideContext
+import androidx.compose.material3.NavigationBarDefaults
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.Surface
+import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.xr.compose.spatial.EdgeOffset
+import androidx.xr.compose.spatial.Orbiter
+import androidx.xr.compose.spatial.OrbiterEdge
+
+/**
+ * <a href="https://m3.material.io/components/navigation-bar/overview" class="external"
+ * target="_blank">Material Design bottom navigation bar</a>.
+ *
+ * XR-specific Navigation bar that shows a Navigation bar in a bottom-aligned [Orbiter].
+ *
+ * Navigation bars offer a persistent and convenient way to switch between primary destinations in
+ * an app.
+ *
+ * [NavigationBar] should contain three to five [NavigationBarItem]s, each representing a singular
+ * destination.
+ *
+ * See [NavigationBarItem] for configuration specific to each item, and not the overall
+ * [NavigationBar] component.
+ *
+ * @param modifier the [Modifier] to be applied to this navigation bar
+ * @param containerColor the color used for the background of this navigation bar. Use
+ * [Color.Transparent] to have no color.
+ * @param contentColor the preferred color for content inside this navigation bar. Defaults to
+ * either the matching content color for [containerColor], or to the current [LocalContentColor]
+ * if [containerColor] is not a color from the theme.
+ * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
+ * overlay is applied on top of the container. A higher tonal elevation value will result in a
+ * darker color in light theme and lighter color in dark theme. See also: [Surface].
+ * @param content the content of this navigation bar, typically 3-5 [NavigationBarItem]s
+ */
+// TODO(brandonjiang): Link to XR-specific NavBar image asset when available
+// TODO(brandonjiang): Add a @sample tag and create a new sample project for XR.
+@ExperimentalMaterial3XrApi
+@Composable
+public fun NavigationBar(
+ modifier: Modifier = Modifier,
+ containerColor: Color = NavigationBarDefaults.containerColor,
+ contentColor: Color = contentColorFor(containerColor),
+ tonalElevation: Dp = NavigationBarDefaults.Elevation,
+ content: @Composable RowScope.() -> Unit
+) {
+ Orbiter(position = OrbiterEdge.Bottom, offset = XrNavigationBarTokens.OrbiterEdgeOffset) {
+ Surface(
+ shape = CircleShape,
+ color = containerColor,
+ contentColor = contentColor,
+ tonalElevation = tonalElevation,
+ modifier = modifier
+ ) {
+ Row(
+ // XR-changed: Original NavigationBar uses fillMaxWidth() and windowInsets,
+ // which do not produce the desired result in XR.
+ modifier =
+ Modifier.width(IntrinsicSize.Min)
+ .heightIn(min = XrNavigationBarTokens.ContainerHeight)
+ .padding(horizontal = XrNavigationBarTokens.HorizontalPadding)
+ .selectableGroup(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center,
+ content = content
+ )
+ }
+ }
+}
+
+private object XrNavigationBarTokens {
+ /** The [EdgeOffset] for NavigationBar Orbiters in Full Space Mode (FSM). */
+ val OrbiterEdgeOffset
+ @Composable get() = EdgeOffset.inner(24.dp)
+
+ val HorizontalPadding = 8.dp
+
+ val ContainerHeight = 80.0.dp
+}
+
+/** [NavigationBarComponentOverride] that uses the XR-specific [NavigationBar]. */
+@ExperimentalMaterial3XrApi
+@OptIn(ExperimentalMaterial3Api::class)
+internal object XrNavigationBarComponentOverride : NavigationBarComponentOverride {
+ @Composable
+ override fun NavigationBarComponentOverrideContext.NavigationBar() {
+ NavigationBar(
+ modifier = modifier,
+ containerColor = containerColor,
+ contentColor = contentColor,
+ tonalElevation = tonalElevation,
+ content = content,
+ )
+ }
+}
diff --git a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/NavigationRail.kt b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/NavigationRail.kt
new file mode 100644
index 0000000..931617c
--- /dev/null
+++ b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/NavigationRail.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.compose.material3
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.NavigationRailComponentOverride
+import androidx.compose.material3.NavigationRailComponentOverrideContext
+import androidx.compose.material3.NavigationRailDefaults
+import androidx.compose.material3.NavigationRailItem
+import androidx.compose.material3.Surface
+import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.xr.compose.spatial.EdgeOffset
+import androidx.xr.compose.spatial.Orbiter
+import androidx.xr.compose.spatial.OrbiterEdge
+
+/**
+ * <a href="https://m3.material.io/components/navigation-rail/overview" class="external"
+ * target="_blank">Material Design bottom navigation rail</a>.
+ *
+ * XR-specific Navigation rail that shows a Navigation rail in a start-aligned [Orbiter].
+ *
+ * Navigation rails provide access to primary destinations in apps when using tablet and desktop
+ * screens.
+ *
+ * The navigation rail should be used to display three to seven app destinations and, optionally, a
+ * [FloatingActionButton] or a logo header. Each destination is typically represented by an icon and
+ * an optional text label.
+ *
+ * [NavigationRail] should contain multiple [NavigationRailItem]s, each representing a singular
+ * destination.
+ *
+ * See [NavigationRailItem] for configuration specific to each item, and not the overall
+ * NavigationRail component.
+ *
+ * @param modifier the [Modifier] to be applied to this navigation rail
+ * @param containerColor the color used for the background of this navigation rail. Use
+ * [Color.Transparent] to have no color.
+ * @param contentColor the preferred color for content inside this navigation rail. Defaults to
+ * either the matching content color for [containerColor], or to the current [LocalContentColor]
+ * if [containerColor] is not a color from the theme.
+ * @param header optional header that may hold a [FloatingActionButton] or a logo
+ * @param content the content of this navigation rail, typically 3-7 [NavigationRailItem]s
+ */
+// TODO(kmost): Link to XR-specific NavRail image asset when available
+// TODO(kmost): Add a @sample tag and create a new sample project for XR.
+@ExperimentalMaterial3XrApi
+@Composable
+public fun NavigationRail(
+ modifier: Modifier = Modifier,
+ containerColor: Color = NavigationRailDefaults.ContainerColor,
+ contentColor: Color = contentColorFor(containerColor),
+ header: @Composable (ColumnScope.() -> Unit)? = null,
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ Orbiter(
+ position = OrbiterEdge.Start,
+ alignment = Alignment.CenterVertically,
+ offset = XrNavigationRailTokens.OrbiterEdgeOffset,
+ ) {
+ Surface(
+ shape = CircleShape,
+ color = containerColor,
+ contentColor = contentColor,
+ modifier = modifier,
+ ) {
+ Column(
+ // XR-changed: Original NavigationRail uses fillMaxHeight() and windowInsets,
+ // which do not produce the desired result in XR.
+ Modifier.widthIn(min = XrNavigationRailTokens.ContainerWidth)
+ .padding(vertical = XrNavigationRailTokens.VerticalPadding)
+ .selectableGroup(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(XrNavigationRailTokens.VerticalPadding),
+ content = content,
+ )
+ }
+ }
+ // Header goes inside a separate Orbiter without an outline shape, as this is generally
+ // a FAB.
+ if (header != null) {
+ Orbiter(
+ position = OrbiterEdge.Start,
+ alignment = Alignment.Top,
+ offset = XrNavigationRailTokens.OrbiterEdgeOffset,
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(XrNavigationRailTokens.VerticalPadding),
+ content = header,
+ )
+ }
+ }
+}
+
+private object XrNavigationRailTokens {
+ /** The [EdgeOffset] for NavigationRail Orbiters in Full Space Mode (FSM). */
+ val OrbiterEdgeOffset
+ @Composable get() = EdgeOffset.inner(24.dp)
+
+ /**
+ * Vertical padding between the contents of the [NavigationRail] and its top/bottom, and
+ * internally between items.
+ *
+ * XR-changed value to match desired UX.
+ */
+ val VerticalPadding: Dp = 20.dp
+
+ val ContainerWidth = 96.0.dp
+}
+
+/** [NavigationRailComponentOverride] that uses the XR-specific [NavigationRail]. */
+@ExperimentalMaterial3XrApi
+@OptIn(ExperimentalMaterial3Api::class)
+internal object XrNavigationRailComponentOverride : NavigationRailComponentOverride {
+ @Composable
+ override fun NavigationRailComponentOverrideContext.NavigationRail() {
+ NavigationRail(
+ modifier = modifier,
+ containerColor = containerColor,
+ contentColor = contentColor,
+ header = header,
+ content = content,
+ )
+ }
+}
diff --git a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/androidx-xr-compose-material3-material3-documentation.md b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/androidx-xr-compose-material3-material3-documentation.md
new file mode 100644
index 0000000..5d053b2
--- /dev/null
+++ b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/androidx-xr-compose-material3-material3-documentation.md
@@ -0,0 +1,7 @@
+# Module root
+
+androidx.xr.compose.material3 material3
+
+# Package androidx.xr.compose.material3
+
+Compose Material3 XR
diff --git a/camera/camera-effects-still-portrait/api/current.txt b/xr/runtime/runtime-openxr/api/current.txt
similarity index 100%
copy from camera/camera-effects-still-portrait/api/current.txt
copy to xr/runtime/runtime-openxr/api/current.txt
diff --git a/camera/camera-effects-still-portrait/api/res-current.txt b/xr/runtime/runtime-openxr/api/res-current.txt
similarity index 100%
copy from camera/camera-effects-still-portrait/api/res-current.txt
copy to xr/runtime/runtime-openxr/api/res-current.txt
diff --git a/xr/runtime/runtime-openxr/api/restricted_current.txt b/xr/runtime/runtime-openxr/api/restricted_current.txt
new file mode 100644
index 0000000..69ed52e
--- /dev/null
+++ b/xr/runtime/runtime-openxr/api/restricted_current.txt
@@ -0,0 +1,109 @@
+// Signature format: 4.0
+package androidx.xr.runtime.openxr {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class AnchorState {
+ ctor public AnchorState();
+ ctor public AnchorState(optional androidx.xr.runtime.internal.TrackingState trackingState, optional androidx.xr.runtime.math.Pose? pose);
+ method public androidx.xr.runtime.internal.TrackingState component1();
+ method public androidx.xr.runtime.math.Pose? component2();
+ method public androidx.xr.runtime.openxr.AnchorState copy(androidx.xr.runtime.internal.TrackingState trackingState, androidx.xr.runtime.math.Pose? pose);
+ method public androidx.xr.runtime.math.Pose? getPose();
+ method public androidx.xr.runtime.internal.TrackingState getTrackingState();
+ property public final androidx.xr.runtime.math.Pose? pose;
+ property public final androidx.xr.runtime.internal.TrackingState trackingState;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface ExportableAnchor extends androidx.xr.runtime.internal.Anchor {
+ method public android.os.IBinder getAnchorToken();
+ method public long getNativePointer();
+ property public abstract android.os.IBinder anchorToken;
+ property public abstract long nativePointer;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class HitData {
+ ctor public HitData(androidx.xr.runtime.math.Pose pose, long id);
+ method public androidx.xr.runtime.math.Pose component1();
+ method public long component2();
+ method public androidx.xr.runtime.openxr.HitData copy(androidx.xr.runtime.math.Pose pose, long id);
+ method public long getId();
+ method public androidx.xr.runtime.math.Pose getPose();
+ property public final long id;
+ property public final androidx.xr.runtime.math.Pose pose;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OpenXrAnchor implements androidx.xr.runtime.openxr.ExportableAnchor {
+ method public void detach();
+ method public android.os.IBinder getAnchorToken();
+ method public long getNativePointer();
+ method public androidx.xr.runtime.internal.Anchor.PersistenceState getPersistenceState();
+ method public androidx.xr.runtime.math.Pose getPose();
+ method public androidx.xr.runtime.internal.TrackingState getTrackingState();
+ method public java.util.UUID? getUuid();
+ method public void persist();
+ method public void update(long xrTime);
+ property public android.os.IBinder anchorToken;
+ property public long nativePointer;
+ property public androidx.xr.runtime.internal.Anchor.PersistenceState persistenceState;
+ property public androidx.xr.runtime.math.Pose pose;
+ property public androidx.xr.runtime.internal.TrackingState trackingState;
+ property public java.util.UUID? uuid;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OpenXrManager implements androidx.xr.runtime.internal.LifecycleManager {
+ method public void configure();
+ method public void create();
+ method public void pause();
+ method public void resume();
+ method public void stop();
+ method public suspend Object? update(kotlin.coroutines.Continuation<? super kotlin.time.ComparableTimeMark>);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OpenXrPerceptionManager implements androidx.xr.runtime.internal.PerceptionManager {
+ method public androidx.xr.runtime.internal.Anchor createAnchor(androidx.xr.runtime.math.Pose pose);
+ method public java.util.List<java.util.UUID> getPersistedAnchorUuids();
+ method public java.util.Collection<androidx.xr.runtime.internal.Trackable> getTrackables();
+ method public java.util.List<androidx.xr.runtime.internal.HitResult> hitTest(androidx.xr.runtime.math.Ray ray);
+ method public androidx.xr.runtime.internal.Anchor loadAnchor(java.util.UUID uuid);
+ method public androidx.xr.runtime.internal.Anchor loadAnchorFromNativePointer(long nativePointer);
+ method public void unpersistAnchor(java.util.UUID uuid);
+ method public void update(long xrTime);
+ property public java.util.Collection<androidx.xr.runtime.internal.Trackable> trackables;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OpenXrPlane implements androidx.xr.runtime.internal.Plane {
+ method public androidx.xr.runtime.internal.Anchor createAnchor(androidx.xr.runtime.math.Pose pose);
+ method public androidx.xr.runtime.math.Pose getCenterPose();
+ method public androidx.xr.runtime.math.Vector2 getExtents();
+ method public androidx.xr.runtime.internal.Plane.Label getLabel();
+ method public androidx.xr.runtime.internal.Plane? getSubsumedBy();
+ method public androidx.xr.runtime.internal.TrackingState getTrackingState();
+ method public androidx.xr.runtime.internal.Plane.Type getType();
+ method public java.util.List<androidx.xr.runtime.math.Vector2> getVertices();
+ method public void update(long xrTime);
+ property public androidx.xr.runtime.math.Pose centerPose;
+ property public androidx.xr.runtime.math.Vector2 extents;
+ property public androidx.xr.runtime.internal.Plane.Label label;
+ property public androidx.xr.runtime.internal.Plane? subsumedBy;
+ property public androidx.xr.runtime.internal.TrackingState trackingState;
+ property public androidx.xr.runtime.internal.Plane.Type type;
+ property public java.util.List<androidx.xr.runtime.math.Vector2> vertices;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OpenXrRuntime implements androidx.xr.runtime.internal.Runtime {
+ method public androidx.xr.runtime.openxr.OpenXrManager getLifecycleManager();
+ method public androidx.xr.runtime.openxr.OpenXrPerceptionManager getPerceptionManager();
+ property public androidx.xr.runtime.openxr.OpenXrManager lifecycleManager;
+ property public androidx.xr.runtime.openxr.OpenXrPerceptionManager perceptionManager;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OpenXrRuntimeFactory implements androidx.xr.runtime.internal.RuntimeFactory {
+ ctor public OpenXrRuntimeFactory();
+ method public androidx.xr.runtime.internal.Runtime createRuntime(android.app.Activity activity);
+ field public static final androidx.xr.runtime.openxr.OpenXrRuntimeFactory.Companion Companion;
+ }
+
+ public static final class OpenXrRuntimeFactory.Companion {
+ }
+
+}
+
diff --git a/xr/runtime/runtime-openxr/build.gradle b/xr/runtime/runtime-openxr/build.gradle
new file mode 100644
index 0000000..5991b63
--- /dev/null
+++ b/xr/runtime/runtime-openxr/build.gradle
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+
+import androidx.build.AndroidXConfig
+import androidx.build.KotlinTarget
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ api(libs.kotlinCoroutinesCore)
+ api(project(":xr:runtime:runtime"))
+
+ implementation("androidx.annotation:annotation:1.8.1")
+
+ androidTestImplementation(libs.kotlinCoroutinesTest)
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.truth)
+ androidTestImplementation("androidx.appcompat:appcompat:1.2.0")
+ androidTestImplementation(project(":kruth:kruth"))
+}
+
+android {
+ namespace = "androidx.xr.runtime.openxr"
+ sourceSets.main {
+ resources.srcDirs += "src/main/resources"
+ jniLibs.srcDirs += new File(AndroidXConfig.getPrebuiltsRoot(project), "androidx/xr/android")
+ }
+ defaultConfig {
+ consumerProguardFiles 'proguard-rules.pro'
+ }
+ sourceSets.androidTest {
+ jniLibs.srcDirs += new File(AndroidXConfig.getPrebuiltsRoot(project), "androidx/xr/androidTest")
+ }
+}
+
+androidx {
+ name = "XR OpenXR Runtime"
+ type = LibraryType.PUBLISHED_LIBRARY
+ inceptionYear = "2024"
+ description = "An implementation of the XR runtime using OpenXR."
+ kotlinTarget = KotlinTarget.KOTLIN_1_9
+ // TODO: b/326456246
+ optOutJSpecify = true
+}
diff --git a/xr/runtime/runtime-openxr/proguard-rules.pro b/xr/runtime/runtime-openxr/proguard-rules.pro
new file mode 100644
index 0000000..7c4c9f0
--- /dev/null
+++ b/xr/runtime/runtime-openxr/proguard-rules.pro
@@ -0,0 +1,5 @@
+# Prevent the OpenXR classes from being obfuscated as they are created from native code.
+-keep class androidx.xr.runtime.openxr.** { *; }
+-keep class androidx.xr.runtime.openxr.**$* { *; }
+-keep class * extends androidx.xr.runtime.openxr.** { *; }
+-keep class * extends androidx.xr.runtime.openxr.**$* { *; }
diff --git a/xr/runtime/runtime-openxr/src/androidTest/AndroidManifest.xml b/xr/runtime/runtime-openxr/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..5f3ccf2
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<!--
+ Copyright 2024 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
+
+ http://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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <application>
+ <activity
+ android:name="android.app.Activity"
+ android:theme="@style/Theme.AppCompat"
+ android:exported="true"/>
+ </application>
+</manifest>
diff --git a/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/AnchorStateTest.kt b/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/AnchorStateTest.kt
new file mode 100644
index 0000000..341824cd
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/AnchorStateTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.xr.runtime.internal.TrackingState
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AnchorStateTest {
+
+ @Test
+ fun constructor_noArguments_returnsZeroVectorAndIdentityQuaternion() {
+ val pose = AnchorState().pose!!
+
+ assertThat(pose.translation.x).isEqualTo(0)
+ assertThat(pose.translation.y).isEqualTo(0)
+ assertThat(pose.translation.z).isEqualTo(0)
+ assertThat(pose.rotation.x).isEqualTo(0)
+ assertThat(pose.rotation.y).isEqualTo(0)
+ assertThat(pose.rotation.z).isEqualTo(0)
+ assertThat(pose.rotation.w).isEqualTo(1)
+ }
+
+ @Test
+ fun constructor_noArguments_returnsPausedTrackingState() {
+ val underTest = AnchorState()
+
+ assertThat(underTest.trackingState).isEqualTo(TrackingState.Paused)
+ }
+
+ @Test
+ fun constructor_TrackingAndNullPose_throwsIllegalArgumentException() {
+ assertFailsWith<IllegalArgumentException> {
+ AnchorState(trackingState = TrackingState.Tracking, pose = null)
+ }
+ }
+
+ @Test
+ fun constructor_PausedAndNullPose_throwsIllegalArgumentException() {
+ assertFailsWith<IllegalArgumentException> {
+ AnchorState(trackingState = TrackingState.Paused, pose = null)
+ }
+ }
+
+ @Test
+ fun fromOpenXrLocationFlags_IncorrectBitPositions_throwsIllegalArgumentException() {
+ assertFailsWith<IllegalArgumentException> {
+ TrackingState.fromOpenXrLocationFlags(0x0000FFFF)
+ }
+ }
+
+ @Test
+ fun fromOpenXrLocationFlags_OnlyValidBitsFlipped_returnsPausedTrackingState() {
+ val trackingState = TrackingState.fromOpenXrLocationFlags(0x00000003) // 0b...0011
+
+ assertThat(trackingState).isEqualTo(TrackingState.Paused)
+ }
+
+ @Test
+ fun fromOpenXrLocationFlags_ValidAndTrackingBitsFlipped_returnsTrackingTrackingState() {
+ val trackingState = TrackingState.fromOpenXrLocationFlags(0x0000000F) // 0b...1111
+
+ assertThat(trackingState).isEqualTo(TrackingState.Tracking)
+ }
+
+ @Test
+ fun fromOpenXrLocationFlags_OnlyTrackingBitsFlipped_returnsStoppedTrackingState() {
+ val trackingState = TrackingState.fromOpenXrLocationFlags(0x0000000C) // 0b...1100
+
+ assertThat(trackingState).isEqualTo(TrackingState.Stopped)
+ }
+
+ @Test
+ fun fromOpenXrLocationFlags_OneTrackingAndValidBitFlipped_returnsStoppedTrackingState() {
+ val trackingState = TrackingState.fromOpenXrLocationFlags(0x0000000A) // 0b...1010
+
+ assertThat(trackingState).isEqualTo(TrackingState.Stopped)
+ }
+}
diff --git a/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/OpenXrAnchorTest.kt b/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/OpenXrAnchorTest.kt
new file mode 100644
index 0000000..9ff67e5
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/OpenXrAnchorTest.kt
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import android.app.Activity
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.xr.runtime.internal.Anchor
+import androidx.xr.runtime.internal.TrackingState
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Vector3
+import com.google.common.truth.Truth.assertThat
+import java.util.UUID
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// TODO - b/382119583: Remove the @SdkSuppress annotation once "androidx.xr.runtime.openxr.test"
+// supports a
+// lower SDK version.
+@SdkSuppress(minSdkVersion = 29)
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class OpenXrAnchorTest {
+
+ companion object {
+ init {
+ System.loadLibrary("androidx.xr.runtime.openxr.test")
+ }
+ }
+
+ @get:Rule val activityRule = ActivityScenarioRule(Activity::class.java)
+
+ lateinit private var openXrManager: OpenXrManager
+ lateinit private var xrResources: XrResources
+ lateinit private var underTest: OpenXrAnchor
+
+ @Before
+ fun setUp() {
+ xrResources = XrResources()
+ underTest = OpenXrAnchor(nativePointer = 1, xrResources = xrResources)
+ xrResources.addUpdatable(underTest as Updatable)
+ }
+
+ @Test
+ fun update_updatesPose() = initOpenXrManagerAndRunTest {
+ val xrTime = 50L * 1_000_000 // 50 milliseconds in nanoseconds.
+ check(underTest.pose == Pose())
+
+ underTest.update(xrTime)
+
+ // TODO - b/346615429: Define values here using the stub's Kotlin API. For the time being
+ // they
+ // come from `kPose` defined in //third_party/jetpack_xr_natives/openxr/openxr_stub.cc
+ assertThat(underTest.pose)
+ .isEqualTo(Pose(Vector3(0f, 0f, 2.0f), Quaternion(0f, 1.0f, 0f, 1.0f)))
+ }
+
+ @Test
+ fun update_updatesTrackingState() = initOpenXrManagerAndRunTest {
+ val xrTime = 50L * 1_000_000 // 50 milliseconds in nanoseconds.
+ check(underTest.trackingState == TrackingState.Paused)
+
+ underTest.update(xrTime)
+
+ // TODO - b/346615429: Define values here using the stub's Kotlin API. For the time being
+ // they
+ // come from the tracking state corresponding to `kLocationFlags` defined in
+ // //third_party/jetpack_xr_natives/openxr/openxr_stub.cc
+ assertThat(underTest.trackingState).isEqualTo(TrackingState.Tracking)
+ }
+
+ @Test
+ fun persist_updatesUuidAndPersistenceState() = initOpenXrManagerAndRunTest {
+ check(underTest.persistenceState == Anchor.PersistenceState.NotPersisted)
+ check(underTest.uuid == null)
+
+ underTest.persist()
+
+ assertThat(underTest.persistenceState).isEqualTo(Anchor.PersistenceState.Pending)
+ // TODO - b/346615429: Define values here using the stub's Kotlin API. For the time being
+ // they
+ // come from `kUuid` defined in //third_party/jetpack_xr_natives/openxr/openxr_stub.cc
+ assertThat(underTest.uuid)
+ .isEqualTo(UUID.fromString("01020304-0506-0708-090a-0b0c0d0e0f10"))
+ }
+
+ @Test
+ fun persist_calledTwice_doesNotChangeUuidAndState() = initOpenXrManagerAndRunTest {
+ val xrTime = 50L * 1_000_000 // 50 milliseconds in nanoseconds.
+ underTest.persist()
+ underTest.update(xrTime)
+ check(underTest.uuid != null)
+ check(underTest.persistenceState == Anchor.PersistenceState.Persisted)
+
+ underTest.persist()
+
+ assertThat(underTest.persistenceState).isEqualTo(Anchor.PersistenceState.Persisted)
+ }
+
+ @Test
+ fun update_updatesPersistenceState() = initOpenXrManagerAndRunTest {
+ val xrTime = 50L * 1_000_000 // 50 milliseconds in nanoseconds.
+ underTest.persist()
+ check(underTest.persistenceState == Anchor.PersistenceState.Pending)
+
+ underTest.update(xrTime)
+
+ assertThat(underTest.persistenceState).isEqualTo(Anchor.PersistenceState.Persisted)
+ }
+
+ @Test
+ fun detach_removesAnchorFromXrResources() = initOpenXrManagerAndRunTest {
+ check(xrResources.updatables.contains(underTest))
+
+ underTest.detach()
+
+ assertThat(xrResources.updatables).doesNotContain(underTest)
+ }
+
+ @Test
+ fun fromOpenXrPersistenceState_returnsCorrectPersistenceStateValues() {
+ // XR_ANCHOR_PERSIST_STATE_PERSIST_NOT_REQUESTED_ANDROID
+ assertThat(Anchor.PersistenceState.fromOpenXrPersistenceState(0))
+ .isEqualTo(Anchor.PersistenceState.NotPersisted)
+ // XR_ANCHOR_PERSIST_STATE_PERSIST_PENDING_ANDROID
+ assertThat(Anchor.PersistenceState.fromOpenXrPersistenceState(1))
+ .isEqualTo(Anchor.PersistenceState.Pending)
+ // XR_ANCHOR_PERSIST_STATE_PERSISTED_ANDROID
+ assertThat(Anchor.PersistenceState.fromOpenXrPersistenceState(2))
+ .isEqualTo(Anchor.PersistenceState.Persisted)
+ }
+
+ private fun initOpenXrManagerAndRunTest(testBody: () -> Unit) {
+ activityRule.scenario.onActivity {
+ val timeSource = OpenXrTimeSource()
+ val perceptionManager = OpenXrPerceptionManager(timeSource)
+ openXrManager = OpenXrManager(it, perceptionManager, timeSource)
+ openXrManager.create()
+ openXrManager.resume()
+
+ testBody()
+
+ // Pause and stop the OpenXR manager here in lieu of an @After method to ensure that the
+ // calls to the OpenXR manager are coming from the same thread.
+ openXrManager.pause()
+ openXrManager.stop()
+ }
+ }
+}
diff --git a/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/OpenXrManagerTest.kt b/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/OpenXrManagerTest.kt
new file mode 100644
index 0000000..d1a36c5
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/OpenXrManagerTest.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import android.app.Activity
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// TODO - b/382119583: Remove the @SdkSuppress annotation once "androidx.xr.runtime.openxr.test"
+// supports a
+// lower SDK version.
+@SdkSuppress(minSdkVersion = 29)
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class OpenXrManagerTest {
+
+ companion object {
+ init {
+ System.loadLibrary("androidx.xr.runtime.openxr.test")
+ }
+ }
+
+ @get:Rule val activityRule = ActivityScenarioRule(Activity::class.java)
+
+ private lateinit var underTest: OpenXrManager
+ private lateinit var perceptionManager: OpenXrPerceptionManager
+ private lateinit var timeSource: OpenXrTimeSource
+
+ @Before
+ fun setUp() {
+ timeSource = OpenXrTimeSource()
+ perceptionManager = OpenXrPerceptionManager(timeSource)
+ }
+
+ @Test
+ fun create_initializesNativeOpenXrManager() = initOpenXrManagerAndRunTest {
+ check(underTest.nativePointer == 0L)
+
+ underTest.create()
+
+ assertThat(underTest.nativePointer).isGreaterThan(0)
+ }
+
+ @Test
+ fun create_afterStop_initializesNativeOpenXrManager() = initOpenXrManagerAndRunTest {
+ underTest.create()
+ underTest.stop()
+ check(underTest.nativePointer == 0L)
+
+ underTest.create()
+
+ assertThat(underTest.nativePointer).isGreaterThan(0L)
+ }
+
+ // TODO: b/344962771 - Add a more meaningful test once we can use the update() method.
+ @Test
+ fun resume_doesNotThrowIllegalStateException() = initOpenXrManagerAndRunTest {
+ underTest.create()
+
+ underTest.resume()
+ }
+
+ @Test
+ fun resume_afterStopAndCreate_doesNotThrowIllegalStateException() =
+ initOpenXrManagerAndRunTest {
+ underTest.create()
+ underTest.stop()
+ check(underTest.nativePointer == 0L)
+ underTest.create()
+
+ underTest.resume()
+ }
+
+ @Test
+ fun update_updatesPerceptionManager() = initOpenXrManagerAndRunTest {
+ runTest {
+ underTest.create()
+ underTest.resume()
+ check(perceptionManager.trackables.isEmpty())
+
+ underTest.update()
+
+ assertThat(perceptionManager.trackables).isNotEmpty()
+ }
+ }
+
+ @Test
+ // TODO - b/346615429: Control the values returned by the OpenXR stub instead of relying on the
+ // stub's current implementation.
+ fun update_returnsTimeMarkFromTimeSource() = initOpenXrManagerAndRunTest {
+ runTest {
+ underTest.create()
+ underTest.resume()
+
+ // The OpenXR stub returns a different value for each call to [OpenXrTimeSource::read]
+ // in
+ // increments of 1000ns when `xrConvertTimespecTimeToTimeKHR` is executed. The first
+ // call
+ // returns 1000ns and is the value associated with [timeMark]. The second call returns
+ // 2000ns
+ // and is the value associated with [AbstractLongTimeSource::zero], which is calculated
+ // automatically with the first call to [OpenXrTimeSource::markNow].
+ // Note that this is just an idiosyncrasy of the test stub and not how OpenXR works in
+ // practice,
+ // where the second call would return an almost identical value to the first call's
+ // value.
+ val timeMark = underTest.update()
+
+ // The third call happens with the call to [elapsedNow] and returns 3000ns. Thus, the
+ // elapsed
+ // time is 3000ns (i.e. "now") - 1000ns (i.e. "the start time") = 2000ns.
+ assertThat(timeMark.elapsedNow().inWholeNanoseconds).isEqualTo(2000L)
+ }
+ }
+
+ // TODO: b/344962771 - Add a more meaningful test once we can use the update() method.
+ @Test
+ fun pause_doesNotThrowIllegalStateException() = initOpenXrManagerAndRunTest {
+ underTest.create()
+ underTest.resume()
+
+ underTest.pause()
+ }
+
+ @Test
+ fun pause_afterStop_throwsIllegalStateException() = initOpenXrManagerAndRunTest {
+ underTest.create()
+ underTest.stop()
+
+ assertThrows(IllegalStateException::class.java) { underTest.pause() }
+ }
+
+ @Test
+ fun stop_destroysNativeOpenXrManager() = initOpenXrManagerAndRunTest {
+ underTest.create()
+ check(underTest.nativePointer != 0L)
+
+ underTest.stop()
+
+ assertThat(underTest.nativePointer).isEqualTo(0L)
+ }
+
+ private fun initOpenXrManagerAndRunTest(testBody: () -> Unit) {
+ activityRule.scenario.onActivity {
+ underTest = OpenXrManager(it, perceptionManager, timeSource)
+
+ testBody()
+
+ // Stop the OpenXR manager here in lieu of an @After method to ensure that the
+ // calls to the OpenXR manager are coming from the same thread.
+ underTest.stop()
+ }
+ }
+}
diff --git a/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/OpenXrPerceptionManagerTest.kt b/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/OpenXrPerceptionManagerTest.kt
new file mode 100644
index 0000000..55f35b7
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/OpenXrPerceptionManagerTest.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import android.app.Activity
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Ray
+import androidx.xr.runtime.math.Vector3
+import com.google.common.truth.Truth.assertThat
+import java.util.UUID
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// TODO - b/382119583: Remove the @SdkSuppress annotation once "androidx.xr.runtime.openxr.test"
+// supports a
+// lower SDK version.
+@SdkSuppress(minSdkVersion = 29)
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class OpenXrPerceptionManagerTest {
+
+ companion object {
+ init {
+ System.loadLibrary("androidx.xr.runtime.openxr.test")
+ }
+
+ const val XR_TIME = 50L * 1_000_000 // 50 milliseconds in nanoseconds.
+ }
+
+ @get:Rule val activityRule = ActivityScenarioRule(Activity::class.java)
+
+ lateinit var openXrManager: OpenXrManager
+ lateinit var underTest: OpenXrPerceptionManager
+
+ @Before
+ fun setUp() {
+ underTest = OpenXrPerceptionManager(OpenXrTimeSource())
+ }
+
+ @After
+ fun tearDown() {
+ underTest.clear()
+ }
+
+ @Test
+ fun createAnchor_returnsAnchorWithTheGivenPose() = initOpenXrManagerAndRunTest {
+ underTest.update(XR_TIME)
+
+ // TODO - b/346615429: Define values here using the stub's Kotlin API. For the time being
+ // they
+ // come from `kPose` defined in //third_party/jetpack_xr_natives/openxr/openxr_stub.cc
+ val pose = Pose(Vector3(0f, 0f, 2.0f), Quaternion(0f, 1.0f, 0f, 1.0f))
+ val anchor = underTest.createAnchor(pose)
+
+ assertThat(anchor.pose).isEqualTo(pose)
+ }
+
+ @Test
+ fun detachAnchor_removesAnchorWhenItDetaches() = initOpenXrManagerAndRunTest {
+ underTest.update(XR_TIME)
+
+ val anchor = underTest.createAnchor(Pose())
+ check(underTest.xrResources.updatables.contains(anchor as Updatable))
+
+ anchor.detach()
+
+ assertThat(underTest.xrResources.updatables).doesNotContain(anchor as Updatable)
+ }
+
+ @Test
+ fun update_updatesTrackables() = initOpenXrManagerAndRunTest {
+ // TODO: b/345314278 -- Add more meaningful tests once trackables are implemented properly
+ // and a
+ // fake perception library can be used mock trackables.
+ underTest.update(XR_TIME)
+
+ assertThat(underTest.trackables).hasSize(1)
+ // TODO - b/346615429: Define values here using the stub's Kotlin API. For the time being
+ // they
+ // come from `kPose` defined in //third_party/jetpack_xr_natives/openxr/openxr_stub.cc
+ assertThat((underTest.trackables.first() as OpenXrPlane).centerPose)
+ .isEqualTo(Pose(Vector3(0f, 0f, 2.0f), Quaternion(0f, 1.0f, 0f, 1.0f)))
+ }
+
+ @Test
+ fun hitTest_returnsHitResults() = initOpenXrManagerAndRunTest {
+ underTest.update(XR_TIME)
+ check(underTest.trackables.isNotEmpty())
+ val trackable = underTest.trackables.first() as OpenXrPlane
+
+ // TODO: b/345314278 -- Add more meaningful tests once trackables are implemented properly
+ // and a
+ // fake perception library can be used to mock trackables.
+ val hitResults = underTest.hitTest(Ray(Vector3(4f, 3f, 2f), Vector3(2f, 1f, 0f)))
+
+ assertThat(hitResults).hasSize(1)
+ // TODO - b/346615429: Define values here using the stub's Kotlin API. For the time being
+ // they
+ // come from `kPose` defined in //third_party/jetpack_xr_natives/openxr/openxr_stub.cc
+ assertThat(hitResults.first().hitPose)
+ .isEqualTo(Pose(Vector3(0f, 0f, 2.0f), Quaternion(0f, 1.0f, 0f, 1.0f)))
+ assertThat(hitResults.first().trackable).isEqualTo(trackable)
+ assertThat(hitResults.first().distance).isEqualTo(5f) // sqrt((4-0)^2 + (3-0)^2 + (2-2)^2)
+ }
+
+ @Test
+ fun getPersistedAnchorUuids_returnsStubUuid() = initOpenXrManagerAndRunTest {
+ // TODO - b/346615429: Define values here using the stub's Kotlin API. For the time being
+ // they
+ // come from `kUuid` defined in //third_party/jetpack_xr_natives/openxr/openxr_stub.cc
+ assertThat(underTest.getPersistedAnchorUuids())
+ .containsExactly(UUID.fromString("01020304-0506-0708-090a-0b0c0d0e0f10"))
+ }
+
+ @Test
+ fun loadAnchor_returnsAnchorWithGivenUuidAndPose() = initOpenXrManagerAndRunTest {
+ // The stub doesn't care about the UUID, so we can use any UUID.
+ val uuid = UUID.randomUUID()
+ val anchor = underTest.loadAnchor(uuid)
+
+ assertThat(anchor.uuid).isEqualTo(uuid)
+ // TODO - b/346615429: Define values here using the stub's Kotlin API. For the time being
+ // they
+ // come from `kPose` defined in //third_party/jetpack_xr_natives/openxr/openxr_stub.cc
+ assertThat(anchor.pose)
+ .isEqualTo(Pose(Vector3(0f, 0f, 2.0f), Quaternion(0f, 1.0f, 0f, 1.0f)))
+ }
+
+ @Test
+ fun loadAnchorFromNativePointer_returnsAnchorWithGivenNativePointer() =
+ initOpenXrManagerAndRunTest {
+ val anchor = underTest.loadAnchorFromNativePointer(123L) as OpenXrAnchor
+ assertThat(anchor.nativePointer).isEqualTo(123L)
+
+ // TODO - b/346615429: Define values here using the stub's Kotlin API. For the time
+ // being they
+ // come from `kPose` defined in
+ // //third_party/arcore/androidx/native/openxr/openxr_stub.cc
+ assertThat(anchor.pose)
+ .isEqualTo(Pose(Vector3(0f, 0f, 2.0f), Quaternion(0f, 1.0f, 0f, 1.0f)))
+ }
+
+ @Test
+ fun unpersistAnchor_doesNotThrowIllegalStateException() = initOpenXrManagerAndRunTest {
+ underTest.unpersistAnchor(UUID.randomUUID())
+ }
+
+ @Test
+ fun clear_clearXrResources() = initOpenXrManagerAndRunTest {
+ underTest.update(XR_TIME)
+ underTest.createAnchor(Pose())
+ check(underTest.trackables.isNotEmpty())
+ check(underTest.xrResources.trackablesMap.isNotEmpty())
+ check(underTest.xrResources.updatables.isNotEmpty())
+
+ underTest.clear()
+
+ assertThat(underTest.trackables).isEmpty()
+ assertThat(underTest.xrResources.trackablesMap).isEmpty()
+ assertThat(underTest.xrResources.updatables).isEmpty()
+ }
+
+ private fun initOpenXrManagerAndRunTest(testBody: () -> Unit) {
+ activityRule.scenario.onActivity {
+ val timeSource = OpenXrTimeSource()
+ openXrManager = OpenXrManager(it, underTest, timeSource)
+ openXrManager.create()
+ openXrManager.resume()
+
+ testBody()
+
+ // Pause and stop the OpenXR manager here in lieu of an @After method to ensure that the
+ // calls to the OpenXR manager are coming from the same thread.
+ openXrManager.pause()
+ openXrManager.stop()
+ }
+ }
+}
diff --git a/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/OpenXrPlaneTest.kt b/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/OpenXrPlaneTest.kt
new file mode 100644
index 0000000..6eaf785
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/OpenXrPlaneTest.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import android.app.Activity
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.xr.runtime.internal.Plane
+import androidx.xr.runtime.internal.TrackingState
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Vector2
+import androidx.xr.runtime.math.Vector3
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// TODO - b/382119583: Remove the @SdkSuppress annotation once "androidx.xr.runtime.openxr.test"
+// supports a
+// lower SDK version.
+@SdkSuppress(minSdkVersion = 29)
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class OpenXrPlaneTest {
+
+ companion object {
+ init {
+ System.loadLibrary("androidx.xr.runtime.openxr.test")
+ }
+ }
+
+ @get:Rule val activityRule = ActivityScenarioRule(Activity::class.java)
+
+ private val planeId = 1L
+
+ lateinit private var openXrManager: OpenXrManager
+ lateinit private var xrResources: XrResources
+ lateinit private var underTest: OpenXrPlane
+
+ @Before
+ fun setUp() {
+ xrResources = XrResources()
+ underTest =
+ OpenXrPlane(planeId, Plane.Type.HorizontalUpwardFacing, OpenXrTimeSource(), xrResources)
+ xrResources.addTrackable(planeId, underTest)
+ xrResources.addUpdatable(underTest as Updatable)
+ }
+
+ @After
+ fun tearDown() {
+ xrResources.clear()
+ }
+
+ @Test
+ fun createAnchor_addsAnchor() = initOpenXrManagerAndRunTest {
+ check(xrResources.updatables.size == 1)
+ check(xrResources.updatables.contains(underTest))
+
+ val anchor = underTest.createAnchor(Pose())
+
+ assertThat(xrResources.updatables).containsExactly(underTest, anchor as Updatable)
+ }
+
+ @Test
+ fun detachAnchor_removesAnchorWhenItDetaches() = initOpenXrManagerAndRunTest {
+ val anchor = underTest.createAnchor(Pose())
+ check(xrResources.updatables.size == 2)
+ check(xrResources.updatables.contains(underTest))
+ check(xrResources.updatables.contains(anchor as Updatable))
+
+ anchor.detach()
+
+ assertThat(xrResources.updatables).containsExactly(underTest)
+ }
+
+ @Test
+ fun update_updatesTrackingState() = initOpenXrManagerAndRunTest {
+ val xrTime = 50L * 1_000_000 // 50 milliseconds in nanoseconds.
+ check(underTest.trackingState.equals(TrackingState.Paused))
+
+ underTest.update(xrTime)
+
+ assertThat(underTest.trackingState).isEqualTo(TrackingState.Tracking)
+ }
+
+ @Test
+ fun update_updatesCenterPose() = initOpenXrManagerAndRunTest {
+ val xrTime = 50L * 1_000_000 // 50 milliseconds in nanoseconds.
+ check(underTest.centerPose == Pose())
+
+ underTest.update(xrTime)
+
+ // TODO - b/346615429: Define values here using the stub's Kotlin API. For the time being
+ // they
+ // come from `kPose` defined in //third_party/jetpack_xr_natives/openxr/openxr_stub.cc
+ assertThat(underTest.centerPose)
+ .isEqualTo(Pose(Vector3(0f, 0f, 2.0f), Quaternion(0f, 1.0f, 0f, 1.0f)))
+ }
+
+ @Test
+ fun update_updatesExtents() = initOpenXrManagerAndRunTest {
+ val xrTime = 50L * 1_000_000 // 50 milliseconds in nanoseconds.
+ check(underTest.centerPose == Pose())
+
+ underTest.update(xrTime)
+
+ // TODO - b/346615429: Define values here using the stub's Kotlin API. For the time being
+ // they
+ // come from `kPose` defined in //third_party/jetpack_xr_natives/openxr/openxr_stub.cc
+ assertThat(underTest.extents).isEqualTo(Vector2(1.0f, 2.0f))
+ }
+
+ @Test
+ fun update_updatesVertices() = initOpenXrManagerAndRunTest {
+ val xrTime = 50L * 1_000_000 // 50 milliseconds in nanoseconds.
+ check(underTest.vertices.isEmpty())
+
+ underTest.update(xrTime)
+
+ // TODO - b/346615429: Define values here using the stub's Kotlin API. For the time being
+ // they
+ // come from `kVertices` defined in //third_party/jetpack_xr_natives/openxr/openxr_stub.cc
+ assertThat(underTest.vertices.size).isEqualTo(4)
+ assertThat(underTest.vertices[0]).isEqualTo(Vector2(2.0f, 0.0f))
+ assertThat(underTest.vertices[1]).isEqualTo(Vector2(2.0f, 2.0f))
+ assertThat(underTest.vertices[2]).isEqualTo(Vector2(0.0f, 0.0f))
+ assertThat(underTest.vertices[3]).isEqualTo(Vector2(0.0f, 2.0f))
+ }
+
+ @Test
+ fun update_updatesSubsumedBy() = initOpenXrManagerAndRunTest {
+ val xrTime = 50L * 1_000_000 // 50 milliseconds in nanoseconds.
+ val planeSubsumedId = 67890L
+ val planeSubsumed: OpenXrPlane =
+ OpenXrPlane(
+ planeSubsumedId,
+ Plane.Type.HorizontalUpwardFacing,
+ OpenXrTimeSource(),
+ xrResources,
+ )
+ xrResources.addTrackable(planeSubsumedId, planeSubsumed)
+ xrResources.addUpdatable(planeSubsumed as Updatable)
+ check(planeSubsumed.subsumedBy == null)
+ check(xrResources.trackablesMap.containsKey(planeId))
+
+ planeSubsumed.update(xrTime)
+
+ assertThat(planeSubsumed.subsumedBy).isEqualTo(underTest)
+ }
+
+ @Test
+ fun update_noSubsumedByPlanes_setsSubsumedByToNull() = initOpenXrManagerAndRunTest {
+ val xrTime = 50L * 1_000_000 // 50 milliseconds in nanoseconds.
+ check(underTest.subsumedBy == null)
+
+ underTest.update(xrTime)
+
+ assertThat(underTest.subsumedBy).isNull()
+ }
+
+ @Test
+ fun fromOpenXrType_withValidValue_convertsType() {
+ val planeType: Plane.Type = Plane.Type.fromOpenXrType(0)
+
+ assertThat(planeType).isEqualTo(Plane.Type.HorizontalDownwardFacing)
+ }
+
+ @Test
+ fun fromOpenXrType_withInvalidValue_throwsIllegalArgumentException() {
+ assertFailsWith<IllegalArgumentException> { Plane.Type.fromOpenXrType(3) }
+ }
+
+ @Test
+ fun fromOpenXrLabel_withValidValue_convertsLabel() {
+ val planeLabel: Plane.Label = Plane.Label.fromOpenXrLabel(0)
+
+ assertThat(planeLabel).isEqualTo(Plane.Label.Unknown)
+ }
+
+ @Test
+ fun fromOpenXrLabel_withInvalidValue_throwsIllegalArgumentException() {
+ assertFailsWith<IllegalArgumentException> { Plane.Label.fromOpenXrLabel(5) }
+ }
+
+ private fun initOpenXrManagerAndRunTest(testBody: () -> Unit) {
+ activityRule.scenario.onActivity {
+ val timeSource = OpenXrTimeSource()
+ val perceptionManager = OpenXrPerceptionManager(timeSource)
+ openXrManager = OpenXrManager(it, perceptionManager, timeSource)
+ openXrManager.create()
+ openXrManager.resume()
+
+ testBody()
+
+ // Pause and stop the OpenXR manager here in lieu of an @After method to ensure that the
+ // calls to the OpenXR manager are coming from the same thread.
+ openXrManager.pause()
+ openXrManager.stop()
+ }
+ }
+}
diff --git a/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/OpenXrRuntimeFactoryTest.kt b/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/OpenXrRuntimeFactoryTest.kt
new file mode 100644
index 0000000..a52a506
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/OpenXrRuntimeFactoryTest.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import android.app.Activity
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.xr.runtime.internal.RuntimeFactory
+import com.google.common.truth.Truth.assertThat
+import java.util.ServiceLoader
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// TODO - b/382119583: Remove the @SdkSuppress annotation once "androidx.xr.runtime.openxr.test"
+// supports a
+// lower SDK version.
+@SdkSuppress(minSdkVersion = 29)
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class OpenXrRuntimeFactoryTest {
+
+ companion object {
+ init {
+ System.loadLibrary("androidx.xr.runtime.openxr.test")
+ }
+ }
+
+ @get:Rule val activityRule = ActivityScenarioRule(Activity::class.java)
+
+ @Test
+ fun class_isDiscoverableViaServiceLoader() {
+ assertThat(ServiceLoader.load(RuntimeFactory::class.java).iterator().next())
+ .isInstanceOf(OpenXrRuntimeFactory::class.java)
+ }
+
+ @Test
+ fun createRuntime_createsOpenXrRuntime() {
+ activityRule.scenario.onActivity {
+ val underTest = OpenXrRuntimeFactory()
+
+ assertThat(underTest.createRuntime(it)).isInstanceOf(OpenXrRuntime::class.java)
+ }
+ }
+}
diff --git a/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/OpenXrTimeSourceTest.kt b/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/OpenXrTimeSourceTest.kt
new file mode 100644
index 0000000..b69c475
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/OpenXrTimeSourceTest.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import android.app.Activity
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// TODO - b/382119583: Remove the @SdkSuppress annotation once "androidx.xr.runtime.openxr.test"
+// supports a
+// lower SDK version.
+@SdkSuppress(minSdkVersion = 29)
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class OpenXrTimeSourceTest {
+
+ companion object {
+ init {
+ System.loadLibrary("androidx.xr.runtime.openxr.test")
+ }
+ }
+
+ @get:Rule val activityRule = ActivityScenarioRule(Activity::class.java)
+
+ private lateinit var underTest: OpenXrTimeSource
+ private lateinit var openXrManager: OpenXrManager
+
+ @Before
+ fun setUp() {
+ underTest = OpenXrTimeSource()
+ }
+
+ @Test
+ // TODO - b/346615429: Control the values returned by the OpenXR stub instead of relying on the
+ // stub's current implementation.
+ fun read_usesTheOpenXrClock() = initOpenXrManagerAndRunTest {
+ // The OpenXR stub returns a different value for each call to [OpenXrTimeSource::read] in
+ // increments of 1000ns when `xrConvertTimespecTimeToTimeKHR` is executed. The first call
+ // returns 1000ns and is the value associated with [timeMark]. The second call returns
+ // 2000ns
+ // and is the value associated with [AbstractLongTimeSource::zero], which is calculated
+ // automatically with the first call to [OpenXrTimeSource::markNow].
+ // Note that this is just an idiosyncrasy of the test stub and not how OpenXR works in
+ // practice,
+ // where the second call would return an almost identical value to the first call's value.
+ val timeMark = underTest.markNow()
+
+ // The third call happens with the call to [elapsedNow] and returns 3000ns. Thus, the
+ // elapsed
+ // time is 3000ns (i.e. "now") - 1000ns (i.e. "the start time") = 2000ns.
+ assertThat(timeMark.elapsedNow().inWholeNanoseconds).isEqualTo(2000L)
+ }
+
+ @Test
+ // TODO - b/346615429: Control the values returned by the OpenXR stub instead of relying on the
+ // stub's current implementation.
+ fun getXrTime_returnsTheOpenXrTime() = initOpenXrManagerAndRunTest {
+ // The OpenXR stub returns a different value for each call to [OpenXrTimeSource::read] in
+ // increments of 1000ns when `xrConvertTimespecTimeToTimeKHR` is executed. The first call
+ // returns 1000ns and is the value associated with [firstTimeMark]. The second call returns
+ // 2000ns and is the value associated with [AbstractLongTimeSource::zero], which is
+ // calculated
+ // automatically with the first call to [OpenXrTimeSource::markNow].
+ // Note that this is just an idiosyncrasy of the test stub and not how OpenXR works in
+ // practice,
+ // where the second call would return an almost identical value to the first call's value.
+ val firstTimeMark = underTest.getXrTime(underTest.markNow())
+ // The third call returns 3000ns and is the value associated with [secondTimeMark].
+ val secondTimeMark = underTest.getXrTime(underTest.markNow())
+ // The fourth call returns 4000ns and is the value associated with [thirdTimeMark].
+ val thirdTimeMark = underTest.getXrTime(underTest.markNow())
+
+ assertThat(secondTimeMark).isEqualTo(firstTimeMark + 2000L)
+ assertThat(thirdTimeMark).isEqualTo(firstTimeMark + 3000L)
+ }
+
+ private fun initOpenXrManagerAndRunTest(testBody: () -> Unit) {
+ activityRule.scenario.onActivity {
+ openXrManager = OpenXrManager(it, OpenXrPerceptionManager(underTest), underTest)
+ openXrManager.create()
+ openXrManager.resume()
+
+ testBody()
+
+ // Pause and stop the OpenXR manager here in lieu of an @After method to ensure that the
+ // calls to the OpenXR manager are coming from the same thread.
+ openXrManager.pause()
+ openXrManager.stop()
+ }
+ }
+}
diff --git a/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/PlaneStateTest.kt b/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/PlaneStateTest.kt
new file mode 100644
index 0000000..f4a4686
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/androidTest/kotlin/androidx/xr/runtime/openxr/PlaneStateTest.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.xr.runtime.internal.TrackingState
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PlaneStateTest {
+
+ @Test
+ fun constructor_noArguments_returnsZeroVectorAndIdentityQuaternion() {
+ val underTest = PlaneState()
+
+ assertThat(underTest.centerPose.translation.x).isEqualTo(0)
+ assertThat(underTest.centerPose.translation.y).isEqualTo(0)
+ assertThat(underTest.centerPose.translation.z).isEqualTo(0)
+ assertThat(underTest.centerPose.rotation.x).isEqualTo(0)
+ assertThat(underTest.centerPose.rotation.y).isEqualTo(0)
+ assertThat(underTest.centerPose.rotation.z).isEqualTo(0)
+ assertThat(underTest.centerPose.rotation.w).isEqualTo(1)
+ assertThat(underTest.extents.x).isEqualTo(0)
+ assertThat(underTest.extents.y).isEqualTo(0)
+ assertThat(underTest.vertices.size).isEqualTo(0)
+ }
+
+ @Test
+ fun fromOpenXrTrackingState_withValidValue_convertsTrackingState() {
+ val trackingState: TrackingState = TrackingState.fromOpenXrTrackingState(0)
+
+ assertThat(trackingState).isEqualTo(TrackingState.Paused)
+ }
+
+ @Test
+ fun fromOpenXrTrackingState_withInvalidValue_throwsIllegalArgumentException() {
+ assertFailsWith<IllegalArgumentException> { TrackingState.fromOpenXrTrackingState(3) }
+ }
+}
diff --git a/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/AnchorState.kt b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/AnchorState.kt
new file mode 100644
index 0000000..1295d25
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/AnchorState.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.internal.TrackingState
+import androidx.xr.runtime.math.Pose
+
+/**
+ * Represents the current state of an [Anchor] instance's mutable fields.
+ *
+ * @property trackingState the [TrackingState] value describing if the anchor is being updated.
+ * @property pose the pose of the center of the detected anchor. Can be null iff the tracking state
+ * is [TrackingState.Stopped].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public data class AnchorState(
+ val trackingState: TrackingState = TrackingState.Paused,
+ val pose: Pose? = Pose(),
+) {
+ init {
+ require(pose != null || trackingState == TrackingState.Stopped) {
+ "Pose cannot be null if tracking state is not STOPPED."
+ }
+ }
+}
+
+/**
+ * Create a [TrackingState] inferred from the [XrSpaceLocationFlags] returned with the anchor
+ * location data. The following rules are used to determine the [TrackingState]:
+ * * If both valid and tracking bits are flipped, return [TrackingState.Tracking]
+ * * If both valid bits are flipped, but not both tracking bits, return [TrackingState.Paused]
+ * * Any other combination of flipped bits (i.e. both valid bits are not flipped), return
+ * [TrackingState.Stopped]
+ */
+internal fun TrackingState.Companion.fromOpenXrLocationFlags(flags: Int): TrackingState {
+ val VALID_MASK = 0x00000001 or 0x00000002
+ val TRACKING_MASK = VALID_MASK or 0x00000004 or 0x00000008
+
+ require(flags or TRACKING_MASK == TRACKING_MASK) { "Invalid location flag bits." }
+
+ return when {
+ (flags and TRACKING_MASK) == TRACKING_MASK -> TrackingState.Tracking
+ (flags and VALID_MASK) == VALID_MASK -> TrackingState.Paused
+ else -> TrackingState.Stopped
+ }
+}
diff --git a/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/ExportableAnchor.kt b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/ExportableAnchor.kt
new file mode 100644
index 0000000..002f7f0
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/ExportableAnchor.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import android.os.IBinder
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.internal.Anchor
+
+/** Wraps the minimum necessary information to export an anchor to another Jetpack XR module. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface ExportableAnchor : Anchor {
+ /* nativePointer to the [XrSpace] instance that backs this anchor */
+ public val nativePointer: Long
+
+ /* anchorToken is a Binder reference of the anchor, it can be used to import the anchor by an
+ * OpenXR session. */
+ public val anchorToken: IBinder
+}
diff --git a/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/HitData.kt b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/HitData.kt
new file mode 100644
index 0000000..19227cb
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/HitData.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.math.Pose
+
+/**
+ * Data associated with a hit result.
+ *
+ * @property pose The pose of the hit result.
+ * @property id The id of the trackable that was hit. It is the address of the native object.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public data class HitData(val pose: Pose, val id: Long) {}
diff --git a/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrAnchor.kt b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrAnchor.kt
new file mode 100644
index 0000000..42bf15b
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrAnchor.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import android.os.IBinder
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.internal.Anchor
+import androidx.xr.runtime.internal.TrackingState
+import androidx.xr.runtime.math.Pose
+import java.nio.ByteBuffer
+import java.util.UUID
+
+/** Wraps the native [XrSpace] with the [Anchor] interface. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class OpenXrAnchor
+internal constructor(
+ override public val nativePointer: Long,
+ private val xrResources: XrResources,
+ loadedUuid: UUID? = null,
+) : ExportableAnchor, Updatable {
+
+ override public val anchorToken: IBinder by lazy { nativeGetAnchorToken(nativePointer) }
+
+ override var pose: Pose = Pose()
+ private set
+
+ override var trackingState: TrackingState = TrackingState.Paused
+ private set
+
+ override var persistenceState: Anchor.PersistenceState = Anchor.PersistenceState.NotPersisted
+ private set
+
+ override var uuid: UUID? = loadedUuid
+ private set
+
+ override fun persist() {
+ if (
+ persistenceState == Anchor.PersistenceState.Persisted ||
+ persistenceState == Anchor.PersistenceState.Pending
+ ) {
+ return
+ }
+ val uuidBytes =
+ checkNotNull(nativePersistAnchor(nativePointer)) { "Failed to persist anchor." }
+ UUIDFromByteArray(uuidBytes)?.let {
+ uuid = it
+ persistenceState = Anchor.PersistenceState.Pending
+ }
+ }
+
+ override fun detach() {
+ check(nativeDestroyAnchor(nativePointer)) { "Failed to destroy anchor." }
+ xrResources.removeUpdatable(this)
+ }
+
+ override fun update(xrTime: Long) {
+ val anchorState: AnchorState =
+ nativeGetAnchorState(nativePointer, xrTime)
+ ?: throw IllegalStateException(
+ "Could not retrieve data for anchor. Is the anchor valid?"
+ )
+
+ trackingState = anchorState.trackingState
+ anchorState.pose?.let { pose = it }
+ if (uuid != null && persistenceState == Anchor.PersistenceState.Pending) {
+ persistenceState = nativeGetPersistenceState(uuid!!)
+ }
+ }
+
+ internal companion object {
+ internal fun UUIDFromByteArray(bytes: ByteArray?): UUID? {
+ if (bytes == null || bytes.size != 16) {
+ return null
+ }
+ val longBytes = ByteBuffer.wrap(bytes)
+ val mostSignificantBits = longBytes.long
+ val leastSignificantBits = longBytes.long
+ return UUID(mostSignificantBits, leastSignificantBits)
+ }
+ }
+
+ private external fun nativeGetAnchorState(nativePointer: Long, timestampNs: Long): AnchorState?
+
+ private external fun nativeGetAnchorToken(nativePointer: Long): IBinder
+
+ private external fun nativePersistAnchor(nativePointer: Long): ByteArray?
+
+ private external fun nativeGetPersistenceState(uuid: UUID): Anchor.PersistenceState
+
+ private external fun nativeDestroyAnchor(nativePointer: Long): Boolean
+}
+
+internal fun Anchor.PersistenceState.Companion.fromOpenXrPersistenceState(
+ value: Int
+): Anchor.PersistenceState =
+ when (value) {
+ 0 ->
+ Anchor.PersistenceState
+ .NotPersisted // XR_ANCHOR_PERSIST_STATE_PERSIST_NOT_REQUESTED_ANDROID
+ 1 -> Anchor.PersistenceState.Pending // XR_ANCHOR_PERSIST_STATE_PERSIST_PENDING_ANDROID
+ 2 -> Anchor.PersistenceState.Persisted // XR_ANCHOR_PERSIST_STATE_PERSISTED_ANDROID
+ else -> {
+ throw IllegalArgumentException("Invalid persistence state value.")
+ }
+ }
diff --git a/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrManager.kt b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrManager.kt
new file mode 100644
index 0000000..396bb6d
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrManager.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import android.app.Activity
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.internal.LifecycleManager
+import kotlin.time.ComparableTimeMark
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.delay
+
+/** Manages the lifecycle of an OpenXR session. */
+@Suppress("NotCloseable")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class OpenXrManager
+internal constructor(
+ private val activity: Activity,
+ private val perceptionManager: OpenXrPerceptionManager,
+ internal val timeSource: OpenXrTimeSource,
+) : LifecycleManager {
+
+ /**
+ * A pointer to the native OpenXrManager. Only valid after [create] and before [stop] have been
+ * called.
+ */
+ internal var nativePointer: Long = 0L
+ private set
+
+ override fun create() {
+ nativePointer = nativeGetPointer()
+ }
+
+ override fun configure() {}
+
+ override fun resume() {
+ check(nativeInit(activity))
+ }
+
+ override suspend fun update(): ComparableTimeMark {
+ // TODO: b/345314364 - Implement this method properly once the native manager supports it.
+ // Currently the native manager handles this via an internal looping mechanism.
+ val now = timeSource.markNow()
+ perceptionManager.update(timeSource.getXrTime(now))
+ // Block the call for a time that is appropriate for OpenXR devices.
+ // TODO: b/359871229 - Implement dynamic delay. We start with a fixed 20ms delay as it is
+ // a nice round number that produces a reasonable frame rate @50 Hz, but this value may need
+ // to
+ // be adjusted in the future.
+ delay(20.milliseconds)
+ return now
+ }
+
+ override fun pause() {
+ check(nativePause())
+ }
+
+ override fun stop() {
+ nativeDeInit()
+ nativePointer = 0L
+ perceptionManager.clear()
+ }
+
+ private external fun nativeGetPointer(): Long
+
+ private external fun nativeInit(activity: Activity): Boolean
+
+ private external fun nativeDeInit(): Boolean
+
+ private external fun nativePause(): Boolean
+}
diff --git a/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrPerceptionManager.kt b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrPerceptionManager.kt
new file mode 100644
index 0000000..f390279
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrPerceptionManager.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.internal.Anchor
+import androidx.xr.runtime.internal.HitResult
+import androidx.xr.runtime.internal.PerceptionManager
+import androidx.xr.runtime.internal.Plane
+import androidx.xr.runtime.internal.Trackable
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Ray
+import androidx.xr.runtime.math.Vector3
+import java.util.Arrays
+import java.util.UUID
+
+/** Implementation of the perception capabilities of a runtime using OpenXR. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class OpenXrPerceptionManager
+internal constructor(private val timeSource: OpenXrTimeSource) : PerceptionManager {
+
+ override fun createAnchor(pose: Pose): Anchor {
+ val nativeAnchor = nativeCreateAnchor(pose, lastUpdateXrTime)
+ check(nativeAnchor != 0L) { "Failed to create anchor." }
+ val anchor = OpenXrAnchor(nativeAnchor, xrResources)
+ anchor.update(lastUpdateXrTime)
+ xrResources.addUpdatable(anchor as Updatable)
+ return anchor
+ }
+
+ // TODO: b/345315434 - Implement this method correctly once we have the ability to conduct
+ // hit tests in the native OpenXrManager.
+ override fun hitTest(ray: Ray): List<HitResult> {
+ val hitData =
+ nativeHitTest(
+ maxResults = 5,
+ ray.origin.x,
+ ray.origin.y,
+ ray.origin.z,
+ ray.direction.x,
+ ray.direction.y,
+ ray.direction.z,
+ lastUpdateXrTime,
+ )
+ return Arrays.asList(*hitData).toList().map { toHitResult(it, ray.origin) }
+ }
+
+ override fun getPersistedAnchorUuids(): List<UUID> {
+ val anchorUuids = nativeGetPersistedAnchorUuids()
+ return Arrays.asList(*anchorUuids)
+ .toList()
+ .map { OpenXrAnchor.UUIDFromByteArray(it) }
+ .filterNotNull()
+ }
+
+ override fun loadAnchor(uuid: UUID): Anchor {
+ val nativeAnchor = nativeLoadAnchor(uuid)
+ check(nativeAnchor != 0L) { "Failed to load anchor." }
+ val anchor = OpenXrAnchor(nativeAnchor, xrResources, loadedUuid = uuid)
+ anchor.update(lastUpdateXrTime)
+ xrResources.addUpdatable(anchor as Updatable)
+ return anchor
+ }
+
+ override fun loadAnchorFromNativePointer(nativePointer: Long): Anchor {
+ val anchor = OpenXrAnchor(nativePointer, xrResources)
+ anchor.update(lastUpdateXrTime)
+ xrResources.addUpdatable(anchor as Updatable)
+ return anchor
+ }
+
+ override fun unpersistAnchor(uuid: UUID) {
+ check(nativeUnpersistAnchor(uuid)) { "Failed to unpersist anchor." }
+ }
+
+ internal val xrResources = XrResources()
+ override val trackables: Collection<Trackable> = xrResources.trackablesMap.values
+
+ private var lastUpdateXrTime: Long = 0L
+
+ /**
+ * Updates the perception manager.
+ *
+ * @param xrTime the number of nanoseconds since the start of the OpenXR epoch.
+ */
+ public fun update(xrTime: Long) {
+ val planes = nativeGetPlanes()
+ // Add new planes to the list of trackables.
+ for (plane in planes) {
+ if (xrResources.trackablesMap.containsKey(plane)) continue
+
+ val planeTypeInt = nativeGetPlaneType(plane, xrTime)
+ check(planeTypeInt >= 0) { "Failed to get plane type." }
+
+ val trackable =
+ OpenXrPlane(plane, Plane.Type.fromOpenXrType(planeTypeInt), timeSource, xrResources)
+ xrResources.addTrackable(plane, trackable)
+ xrResources.addUpdatable(trackable as Updatable)
+ }
+
+ for (updatable in xrResources.updatables) {
+ updatable.update(xrTime)
+ }
+
+ lastUpdateXrTime = xrTime
+ }
+
+ internal fun clear() {
+ xrResources.clear()
+ }
+
+ private fun toHitResult(hitData: HitData, origin: Vector3): HitResult {
+ val trackable =
+ xrResources.trackablesMap[hitData.id]
+ ?: throw IllegalStateException("Trackable not found.")
+
+ return HitResult(
+ distance = (hitData.pose.translation - origin).length,
+ hitPose = hitData.pose,
+ trackable = trackable,
+ )
+ }
+
+ private external fun nativeCreateAnchor(pose: Pose, timestampNs: Long): Long
+
+ private external fun nativeGetPlanes(): LongArray
+
+ private external fun nativeGetPlaneType(planeId: Long, timestampNs: Long): Int
+
+ private external fun nativeHitTest(
+ maxResults: Int,
+ originX: Float,
+ originY: Float,
+ originZ: Float,
+ directionX: Float,
+ directionY: Float,
+ directionZ: Float,
+ timestampNs: Long,
+ ): Array<HitData>
+
+ private external fun nativeGetPersistedAnchorUuids(): Array<ByteArray>
+
+ private external fun nativeLoadAnchor(uuid: UUID): Long
+
+ private external fun nativeUnpersistAnchor(uuid: UUID): Boolean
+}
diff --git a/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrPlane.kt b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrPlane.kt
new file mode 100644
index 0000000..93c1e7b
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrPlane.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.internal.Anchor
+import androidx.xr.runtime.internal.Plane
+import androidx.xr.runtime.internal.TrackingState
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Vector2
+
+/** Wraps the native [XrTrackableANDROID] with the [Plane] interface. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class OpenXrPlane
+internal constructor(
+ internal val planeId: Long,
+ override val type: Plane.Type,
+ internal val timeSource: OpenXrTimeSource,
+ private val xrResources: XrResources,
+) : Plane, Updatable {
+ override var label: Plane.Label = Plane.Label.Unknown
+ private set
+
+ override var centerPose: Pose = Pose()
+ private set
+
+ override var vertices: List<Vector2> = emptyList()
+ private set
+
+ override var extents: Vector2 = Vector2.Zero
+ private set
+
+ override var subsumedBy: Plane? = null
+ private set
+
+ override var trackingState: TrackingState = TrackingState.Paused
+ private set
+
+ override fun createAnchor(pose: Pose): Anchor {
+ val xrTime = timeSource.getXrTime(timeSource.markNow())
+ val anchorNativePointer = nativeCreateAnchorForPlane(planeId, pose, xrTime)
+ val anchor: Anchor = OpenXrAnchor(anchorNativePointer, xrResources)
+ xrResources.addUpdatable(anchor as Updatable)
+ return anchor
+ }
+
+ override fun update(xrTime: Long) {
+ val planeState: PlaneState =
+ nativeGetPlaneState(planeId, xrTime)
+ ?: throw IllegalStateException("Could latest plane state. Is the plane ID valid?")
+ label = planeState.label
+ trackingState = planeState.trackingState
+ centerPose = planeState.centerPose
+ extents = planeState.extents
+ vertices = planeState.vertices.toList()
+
+ if (planeState.subsumedByPlaneId != 0L) {
+ subsumedBy = xrResources.trackablesMap[planeState.subsumedByPlaneId] as OpenXrPlane?
+ }
+ }
+
+ private external fun nativeGetPlaneState(planeId: Long, timestampNs: Long): PlaneState?
+
+ private external fun nativeCreateAnchorForPlane(
+ planeId: Long,
+ pose: Pose,
+ timeStampNs: Long,
+ ): Long
+}
+
+/** Create a [Plane.Type] from an integer value corresponding to an [XrPlaneTypeANDROID]. */
+internal fun Plane.Type.Companion.fromOpenXrType(type: Int): Plane.Type =
+ when (type) {
+ 0 -> Plane.Type.HorizontalDownwardFacing // XR_PLANE_TYPE_HORIZONTAL_DOWNWARD_FACING_ANDROID
+ 1 -> Plane.Type.HorizontalUpwardFacing // XR_PLANE_TYPE_HORIZONTAL_UPWARD_FACING_ANDROID
+ 2 -> Plane.Type.Vertical // XR_PLANE_TYPE_VERTICAL_ANDROID
+ else -> {
+ throw IllegalArgumentException("Invalid plane type.")
+ }
+ }
+
+/** Create a [Plane.Label] from an integer value corresponding to an [XrPlaneLabelANDROID]. */
+internal fun Plane.Label.Companion.fromOpenXrLabel(label: Int): Plane.Label =
+ when (label) {
+ 0 -> Plane.Label.Unknown // XR_PLANE_LABEL_UNKNOWN_ANDROID
+ 1 -> Plane.Label.Wall // XR_PLANE_LABEL_WALL_ANDROID
+ 2 -> Plane.Label.Floor // XR_PLANE_LABEL_FLOOR_ANDROID
+ 3 -> Plane.Label.Ceiling // XR_PLANE_LABEL_CEILING_ANDROID
+ 4 -> Plane.Label.Table // XR_PLANE_LABEL_TABLE_ANDROID
+ else -> {
+ throw IllegalArgumentException("Invalid plane label.")
+ }
+ }
diff --git a/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrRuntime.kt b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrRuntime.kt
new file mode 100644
index 0000000..28d43265
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrRuntime.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.internal.Runtime
+
+/**
+ * Implementation of the [Runtime] interface using OpenXR.
+ *
+ * @property lifecycleManager that manages the lifecycle of the OpenXR session.
+ * @property perceptionManager that manages the perception capabilities of a runtime using OpenXR.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class OpenXrRuntime
+internal constructor(
+ override val lifecycleManager: OpenXrManager,
+ override val perceptionManager: OpenXrPerceptionManager,
+) : Runtime {}
diff --git a/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrRuntimeFactory.kt b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrRuntimeFactory.kt
new file mode 100644
index 0000000..474cc9c
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrRuntimeFactory.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import android.app.Activity
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.internal.Runtime
+import androidx.xr.runtime.internal.RuntimeFactory
+
+/** Factory for creating instances of [OpenXrRuntime]. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class OpenXrRuntimeFactory() : RuntimeFactory {
+ public companion object {
+ init {
+ try {
+ System.loadLibrary("androidx.xr.runtime.openxr")
+ } catch (e: UnsatisfiedLinkError) {
+ // TODO: b/344962771 - Use Flogger instead of println.
+ println("Failed to load library: $e")
+ }
+ }
+ }
+
+ override fun createRuntime(activity: Activity): Runtime {
+ val timeSource = OpenXrTimeSource()
+ val perceptionManager = OpenXrPerceptionManager(timeSource)
+ return OpenXrRuntime(
+ OpenXrManager(activity, perceptionManager, timeSource),
+ perceptionManager
+ )
+ }
+}
diff --git a/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrTimeSource.kt b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrTimeSource.kt
new file mode 100644
index 0000000..679ebe0
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/OpenXrTimeSource.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import kotlin.time.AbstractLongTimeSource
+import kotlin.time.ComparableTimeMark
+import kotlin.time.DurationUnit
+
+/** A time source that uses the native OpenXR time as the clock. */
+internal class OpenXrTimeSource : AbstractLongTimeSource(DurationUnit.NANOSECONDS) {
+ // Should be initialized when the base class initializes its [zero] field.
+ private var zeroXrTime: Long? = null
+ private lateinit var zeroTimeMark: ComparableTimeMark
+
+ override fun read(): Long {
+ val reading = nativeGetXrTimeNow()
+ zeroXrTime = zeroXrTime ?: reading
+ return reading
+ }
+
+ override fun markNow(): ComparableTimeMark {
+ val timeMark = super.markNow()
+ zeroTimeMark = if (::zeroTimeMark.isInitialized) zeroTimeMark else timeMark
+ return timeMark
+ }
+
+ /**
+ * Returns the XrTime corresponding to [timeMark]. This calculation is only valid if [timeMark]
+ * was created with this [TimeSource].
+ */
+ internal fun getXrTime(timeMark: ComparableTimeMark): Long {
+ check(zeroXrTime != null && ::zeroTimeMark.isInitialized)
+ val elapsedNanos = (timeMark - zeroTimeMark).inWholeNanoseconds
+ return zeroXrTime!! + elapsedNanos
+ }
+
+ private external fun nativeGetXrTimeNow(): Long
+}
diff --git a/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/PlaneState.kt b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/PlaneState.kt
new file mode 100644
index 0000000..d7bbcb1
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/PlaneState.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import androidx.xr.runtime.internal.Plane
+import androidx.xr.runtime.internal.TrackingState
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Vector2
+
+/**
+ * Represents the current state of a [Plane] instance's mutable fields.
+ *
+ * @property trackingState the [TrackingState] value describing if the plane is being updated.
+ * @property label the [Plane.Label] associated with the plane.
+ * @property centerPose the pose of the center of the detected plane. The pose's transformed +Y axis
+ * will be point normal out of the plane, with the +X and +Z axes orienting the extents of the
+ * bounding rectangle.
+ * @property extents the dimensions of the detected plane.
+ * @property vertices the 2D vertices of a convex polygon approximating the detected plane,
+ * @property subsumedByPlaneId the OpenXR handle of the plane that subsumed this plane.
+ */
+internal data class PlaneState(
+ val trackingState: TrackingState = TrackingState.Paused,
+ val label: Plane.Label = Plane.Label.Unknown,
+ val centerPose: Pose = Pose(),
+ val extents: Vector2 = Vector2.Zero,
+ val vertices: Array<Vector2> = emptyArray(),
+ val subsumedByPlaneId: Long = 0,
+) {}
+
+internal fun TrackingState.Companion.fromOpenXrTrackingState(trackingState: Int): TrackingState =
+ when (trackingState) {
+ 0 -> TrackingState.Paused // XR_TRACKING_STATE_PAUSED_ANDROID
+ 1 -> TrackingState.Stopped // XR_TRACKING_STATE_STOPPED_ANDROID
+ 2 -> TrackingState.Tracking // XR_TRACKING_STATE_TRACKING_ANDROID
+ else -> {
+ throw IllegalArgumentException("Invalid tracking state.")
+ }
+ }
diff --git a/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/Updatable.kt b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/Updatable.kt
new file mode 100644
index 0000000..3bc3505
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/Updatable.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+/** Describes an entity that can be updated across time. */
+internal interface Updatable {
+ /**
+ * Updates the entity retrieving its state at [xrTime].
+ *
+ * @param xrTime the number of nanoseconds since the start of the OpenXR epoch.
+ */
+ fun update(xrTime: Long)
+}
diff --git a/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/XrResources.kt b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/XrResources.kt
new file mode 100644
index 0000000..b485ce0
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/main/kotlin/androidx/xr/runtime/openxr/XrResources.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.openxr
+
+import android.annotation.SuppressLint
+import androidx.xr.runtime.internal.Trackable
+import java.util.concurrent.CopyOnWriteArrayList
+
+/** Object that holds resources that are used in the XR session. */
+internal class XrResources {
+ /** Map of native trackable pointer to [Trackable]. */
+ @SuppressLint("BanConcurrentHashMap")
+ private val _trackablesMap = java.util.concurrent.ConcurrentHashMap<Long, Trackable>()
+ val trackablesMap: Map<Long, Trackable> = _trackablesMap
+
+ /** List of [Updatable]s that are updated every frame. */
+ private val _updatables = CopyOnWriteArrayList<Updatable>()
+ val updatables: List<Updatable> = _updatables
+
+ internal fun addTrackable(trackableId: Long, trackable: Trackable) {
+ _trackablesMap[trackableId] = trackable
+ }
+
+ internal fun removeTrackable(trackableId: Long) {
+ _trackablesMap.remove(trackableId)
+ }
+
+ internal fun addUpdatable(updatable: Updatable) {
+ _updatables.add(updatable)
+ }
+
+ internal fun removeUpdatable(updatable: Updatable) {
+ _updatables.remove(updatable)
+ }
+
+ internal fun clear() {
+ _trackablesMap.clear()
+ _updatables.clear()
+ }
+}
diff --git a/xr/runtime/runtime-openxr/src/main/resources/META-INF/services/androidx.xr.runtime.internal.RuntimeFactory b/xr/runtime/runtime-openxr/src/main/resources/META-INF/services/androidx.xr.runtime.internal.RuntimeFactory
new file mode 100644
index 0000000..a780b6e
--- /dev/null
+++ b/xr/runtime/runtime-openxr/src/main/resources/META-INF/services/androidx.xr.runtime.internal.RuntimeFactory
@@ -0,0 +1 @@
+androidx.xr.runtime.openxr.OpenXrRuntimeFactory
\ No newline at end of file
diff --git a/xr/runtime/runtime-testing/api/current.txt b/xr/runtime/runtime-testing/api/current.txt
new file mode 100644
index 0000000..598558a
--- /dev/null
+++ b/xr/runtime/runtime-testing/api/current.txt
@@ -0,0 +1,14 @@
+// Signature format: 4.0
+package androidx.xr.runtime.testing.math {
+
+ public final class MathAssertions {
+ method public static void assertPose(androidx.xr.runtime.math.Pose actual, androidx.xr.runtime.math.Pose expected);
+ method public static void assertPose(androidx.xr.runtime.math.Pose actual, androidx.xr.runtime.math.Pose expected, optional float epsilon);
+ method public static void assertRotation(androidx.xr.runtime.math.Quaternion actual, androidx.xr.runtime.math.Quaternion expected);
+ method public static void assertRotation(androidx.xr.runtime.math.Quaternion actual, androidx.xr.runtime.math.Quaternion expected, optional float epsilon);
+ method public static void assertVector3(androidx.xr.runtime.math.Vector3 actual, androidx.xr.runtime.math.Vector3 expected);
+ method public static void assertVector3(androidx.xr.runtime.math.Vector3 actual, androidx.xr.runtime.math.Vector3 expected, optional float epsilon);
+ }
+
+}
+
diff --git a/camera/camera-effects-still-portrait/api/res-current.txt b/xr/runtime/runtime-testing/api/res-current.txt
similarity index 100%
copy from camera/camera-effects-still-portrait/api/res-current.txt
copy to xr/runtime/runtime-testing/api/res-current.txt
diff --git a/xr/runtime/runtime-testing/api/restricted_current.txt b/xr/runtime/runtime-testing/api/restricted_current.txt
new file mode 100644
index 0000000..531c744
--- /dev/null
+++ b/xr/runtime/runtime-testing/api/restricted_current.txt
@@ -0,0 +1,144 @@
+// Signature format: 4.0
+package androidx.xr.runtime.testing {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface AnchorHolder {
+ method public void detachAnchor(androidx.xr.runtime.internal.Anchor anchor);
+ method public void persistAnchor(androidx.xr.runtime.internal.Anchor anchor);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class FakeLifecycleManager implements androidx.xr.runtime.internal.LifecycleManager {
+ ctor public FakeLifecycleManager();
+ method public void allowOneMoreCallToUpdate();
+ method public void configure();
+ method public void create();
+ method public androidx.xr.runtime.testing.FakeLifecycleManager.State getState();
+ method public kotlin.time.TestTimeSource getTimeSource();
+ method public void pause();
+ method public void resume();
+ method public void stop();
+ method public suspend Object? update(kotlin.coroutines.Continuation<? super kotlin.time.ComparableTimeMark>);
+ property public final androidx.xr.runtime.testing.FakeLifecycleManager.State state;
+ property public final kotlin.time.TestTimeSource timeSource;
+ }
+
+ public enum FakeLifecycleManager.State {
+ enum_constant public static final androidx.xr.runtime.testing.FakeLifecycleManager.State INITIALIZED;
+ enum_constant public static final androidx.xr.runtime.testing.FakeLifecycleManager.State NOT_INITIALIZED;
+ enum_constant public static final androidx.xr.runtime.testing.FakeLifecycleManager.State PAUSED;
+ enum_constant public static final androidx.xr.runtime.testing.FakeLifecycleManager.State RESUMED;
+ enum_constant public static final androidx.xr.runtime.testing.FakeLifecycleManager.State STOPPED;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class FakePerceptionManager implements androidx.xr.runtime.testing.AnchorHolder androidx.xr.runtime.internal.PerceptionManager {
+ ctor public FakePerceptionManager();
+ method public void addHitResult(androidx.xr.runtime.internal.HitResult hitResult);
+ method public void addTrackable(androidx.xr.runtime.internal.Trackable trackable);
+ method public void clearHitResults();
+ method public void clearTrackables();
+ method public androidx.xr.runtime.internal.Anchor createAnchor(androidx.xr.runtime.math.Pose pose);
+ method public void detachAnchor(androidx.xr.runtime.internal.Anchor anchor);
+ method public java.util.List<androidx.xr.runtime.internal.Anchor> getAnchors();
+ method public java.util.List<java.util.UUID> getPersistedAnchorUuids();
+ method public java.util.List<androidx.xr.runtime.internal.Trackable> getTrackables();
+ method public java.util.List<androidx.xr.runtime.internal.HitResult> hitTest(androidx.xr.runtime.math.Ray ray);
+ method public androidx.xr.runtime.internal.Anchor loadAnchor(java.util.UUID uuid);
+ method public androidx.xr.runtime.internal.Anchor loadAnchorFromNativePointer(long nativePointer);
+ method public void persistAnchor(androidx.xr.runtime.internal.Anchor anchor);
+ method public void unpersistAnchor(java.util.UUID uuid);
+ property public final java.util.List<androidx.xr.runtime.internal.Anchor> anchors;
+ property public java.util.List<androidx.xr.runtime.internal.Trackable> trackables;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class FakeRuntime implements androidx.xr.runtime.internal.Runtime {
+ ctor public FakeRuntime(androidx.xr.runtime.testing.FakeLifecycleManager lifecycleManager, androidx.xr.runtime.testing.FakePerceptionManager perceptionManager);
+ method public androidx.xr.runtime.testing.FakeLifecycleManager component1();
+ method public androidx.xr.runtime.testing.FakePerceptionManager component2();
+ method public androidx.xr.runtime.testing.FakeRuntime copy(androidx.xr.runtime.testing.FakeLifecycleManager lifecycleManager, androidx.xr.runtime.testing.FakePerceptionManager perceptionManager);
+ method public androidx.xr.runtime.testing.FakeLifecycleManager getLifecycleManager();
+ method public androidx.xr.runtime.testing.FakePerceptionManager getPerceptionManager();
+ property public androidx.xr.runtime.testing.FakeLifecycleManager lifecycleManager;
+ property public androidx.xr.runtime.testing.FakePerceptionManager perceptionManager;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class FakeRuntimeAnchor implements androidx.xr.runtime.internal.Anchor {
+ ctor public FakeRuntimeAnchor(androidx.xr.runtime.math.Pose pose, optional androidx.xr.runtime.testing.AnchorHolder? anchorHolder);
+ method public void detach();
+ method public androidx.xr.runtime.testing.AnchorHolder? getAnchorHolder();
+ method public androidx.xr.runtime.internal.Anchor.PersistenceState getPersistenceState();
+ method public androidx.xr.runtime.math.Pose getPose();
+ method public androidx.xr.runtime.internal.TrackingState getTrackingState();
+ method public java.util.UUID? getUuid();
+ method public boolean isAttached();
+ method public void persist();
+ method public void setPersistenceState(androidx.xr.runtime.internal.Anchor.PersistenceState);
+ method public void setPose(androidx.xr.runtime.math.Pose);
+ method public void setTrackingState(androidx.xr.runtime.internal.TrackingState);
+ method public void setUuid(java.util.UUID?);
+ property public final androidx.xr.runtime.testing.AnchorHolder? anchorHolder;
+ property public final boolean isAttached;
+ property public androidx.xr.runtime.internal.Anchor.PersistenceState persistenceState;
+ property public androidx.xr.runtime.math.Pose pose;
+ property public androidx.xr.runtime.internal.TrackingState trackingState;
+ property public java.util.UUID? uuid;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class FakeRuntimeFactory implements androidx.xr.runtime.internal.RuntimeFactory {
+ ctor public FakeRuntimeFactory();
+ method public androidx.xr.runtime.testing.FakeRuntime createRuntime(android.app.Activity activity);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class FakeRuntimePlane implements androidx.xr.runtime.testing.AnchorHolder androidx.xr.runtime.internal.Plane {
+ ctor public FakeRuntimePlane();
+ ctor public FakeRuntimePlane(optional androidx.xr.runtime.internal.Plane.Type type, optional androidx.xr.runtime.internal.Plane.Label label, optional androidx.xr.runtime.internal.TrackingState trackingState, optional androidx.xr.runtime.math.Pose centerPose, optional androidx.xr.runtime.math.Vector2 extents, optional java.util.List<androidx.xr.runtime.math.Vector2> vertices, optional androidx.xr.runtime.internal.Plane? subsumedBy, optional java.util.Collection<androidx.xr.runtime.internal.Anchor> anchors);
+ method public androidx.xr.runtime.internal.Anchor createAnchor(androidx.xr.runtime.math.Pose pose);
+ method public void detachAnchor(androidx.xr.runtime.internal.Anchor anchor);
+ method public java.util.Collection<androidx.xr.runtime.internal.Anchor> getAnchors();
+ method public androidx.xr.runtime.math.Pose getCenterPose();
+ method public androidx.xr.runtime.math.Vector2 getExtents();
+ method public androidx.xr.runtime.internal.Plane.Label getLabel();
+ method public androidx.xr.runtime.internal.Plane? getSubsumedBy();
+ method public androidx.xr.runtime.internal.TrackingState getTrackingState();
+ method public androidx.xr.runtime.internal.Plane.Type getType();
+ method public java.util.List<androidx.xr.runtime.math.Vector2> getVertices();
+ method public void persistAnchor(androidx.xr.runtime.internal.Anchor anchor);
+ method public void setCenterPose(androidx.xr.runtime.math.Pose);
+ method public void setExtents(androidx.xr.runtime.math.Vector2);
+ method public void setSubsumedBy(androidx.xr.runtime.internal.Plane?);
+ method public void setTrackingState(androidx.xr.runtime.internal.TrackingState);
+ method public void setVertices(java.util.List<androidx.xr.runtime.math.Vector2>);
+ property public final java.util.Collection<androidx.xr.runtime.internal.Anchor> anchors;
+ property public androidx.xr.runtime.math.Pose centerPose;
+ property public androidx.xr.runtime.math.Vector2 extents;
+ property public androidx.xr.runtime.internal.Plane.Label label;
+ property public androidx.xr.runtime.internal.Plane? subsumedBy;
+ property public androidx.xr.runtime.internal.TrackingState trackingState;
+ property public androidx.xr.runtime.internal.Plane.Type type;
+ property public java.util.List<androidx.xr.runtime.math.Vector2> vertices;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class FakeStateExtender implements androidx.xr.runtime.StateExtender {
+ ctor public FakeStateExtender();
+ method public suspend Object? extend(androidx.xr.runtime.CoreState coreState, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public java.util.List<androidx.xr.runtime.CoreState> getExtended();
+ method public void initialize(androidx.xr.runtime.internal.Runtime runtime);
+ method public boolean isInitialized();
+ method public void setInitialized(boolean);
+ property public final java.util.List<androidx.xr.runtime.CoreState> extended;
+ property public final boolean isInitialized;
+ }
+
+}
+
+package androidx.xr.runtime.testing.math {
+
+ public final class MathAssertions {
+ method public static void assertPose(androidx.xr.runtime.math.Pose actual, androidx.xr.runtime.math.Pose expected);
+ method public static void assertPose(androidx.xr.runtime.math.Pose actual, androidx.xr.runtime.math.Pose expected, optional float epsilon);
+ method public static void assertRotation(androidx.xr.runtime.math.Quaternion actual, androidx.xr.runtime.math.Quaternion expected);
+ method public static void assertRotation(androidx.xr.runtime.math.Quaternion actual, androidx.xr.runtime.math.Quaternion expected, optional float epsilon);
+ method public static void assertVector3(androidx.xr.runtime.math.Vector3 actual, androidx.xr.runtime.math.Vector3 expected);
+ method public static void assertVector3(androidx.xr.runtime.math.Vector3 actual, androidx.xr.runtime.math.Vector3 expected, optional float epsilon);
+ }
+
+}
+
diff --git a/xr/runtime/runtime-testing/build.gradle b/xr/runtime/runtime-testing/build.gradle
new file mode 100644
index 0000000..4b037ce
--- /dev/null
+++ b/xr/runtime/runtime-testing/build.gradle
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+
+import androidx.build.KotlinTarget
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ api(libs.kotlinCoroutinesCore)
+ api(project(":xr:runtime:runtime"))
+
+ implementation("androidx.annotation:annotation:1.8.1")
+ implementation(libs.truth)
+
+ testImplementation(libs.kotlinCoroutinesTest)
+ testImplementation(libs.junit)
+ testImplementation(libs.robolectric)
+ testImplementation(libs.testExtJunit)
+ testImplementation(libs.testRunner)
+ testImplementation(project(":kruth:kruth"))
+}
+
+android {
+ namespace = "androidx.xr.runtime.testing"
+ sourceSets.main.resources.srcDirs += "src/main/resources"
+}
+
+androidx {
+ name = "XR Fake Runtime"
+ type = LibraryType.PUBLISHED_TEST_LIBRARY
+ inceptionYear = "2024"
+ description = "A fake implementation of the XR runtime used in unit tests."
+ kotlinTarget = KotlinTarget.KOTLIN_1_9
+ // TODO: b/326456246
+ optOutJSpecify = true
+}
diff --git a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/AnchorHolder.kt b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/AnchorHolder.kt
new file mode 100644
index 0000000..454d3f1
--- /dev/null
+++ b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/AnchorHolder.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.testing
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.internal.Anchor
+
+/** Object that holds [Anchor] instances. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface AnchorHolder {
+
+ /** Notifies the [AnchorHolder] that the given [Anchor] has been persisted. */
+ public fun persistAnchor(anchor: Anchor)
+
+ /**
+ * Detaches the given [Anchor] from this trackable. Single [Anchor] instances rely on this
+ * function to remove themselves from the [AnchorHolder].
+ */
+ public fun detachAnchor(anchor: Anchor)
+}
diff --git a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeLifecycleManager.kt b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeLifecycleManager.kt
new file mode 100644
index 0000000..67bdac7
--- /dev/null
+++ b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeLifecycleManager.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.testing
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.internal.LifecycleManager
+import kotlin.time.ComparableTimeMark
+import kotlin.time.TestTimeSource
+import kotlinx.coroutines.sync.Semaphore
+
+/** Test-only implementation of [LifecycleManager] used to validate state transitions. */
+@Suppress("NotCloseable")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class FakeLifecycleManager : LifecycleManager {
+
+ /** Set of possible states of the runtime. */
+ public enum class State {
+ NOT_INITIALIZED,
+ INITIALIZED,
+ RESUMED,
+ PAUSED,
+ STOPPED,
+ }
+
+ /** The current state of the runtime. */
+ public var state: FakeLifecycleManager.State = State.NOT_INITIALIZED
+ private set
+
+ /** The time source used for this runtime. */
+ public val timeSource: TestTimeSource = TestTimeSource()
+
+ private val semaphore = Semaphore(1)
+
+ override fun create() {
+ check(state == State.NOT_INITIALIZED)
+ state = State.INITIALIZED
+ }
+
+ override fun configure() {
+ check(state == State.INITIALIZED || state == State.RESUMED || state == State.PAUSED)
+ }
+
+ override fun resume() {
+ check(state == State.INITIALIZED || state == State.PAUSED)
+ state = State.RESUMED
+ }
+
+ /**
+ * Retrieves the latest timemark. The first call to this method will execute immediately.
+ * Subsequent calls will be blocked until [allowOneMoreCallToUpdate] is called.
+ */
+ override suspend fun update(): ComparableTimeMark {
+ check(state == State.RESUMED)
+ semaphore.acquire()
+ return timeSource.markNow()
+ }
+
+ /**
+ * Allows an additional call to [update] to not be blocked. Requires that [update] has been
+ * called exactly once before each call to this method. Failure to do so will result in an
+ * [IllegalStateException].
+ */
+ public fun allowOneMoreCallToUpdate() {
+ semaphore.release()
+ }
+
+ override fun pause() {
+ check(state == State.RESUMED)
+ state = State.PAUSED
+ }
+
+ override fun stop() {
+ check(state == State.PAUSED || state == State.INITIALIZED)
+ state = State.STOPPED
+ }
+}
diff --git a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakePerceptionManager.kt b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakePerceptionManager.kt
new file mode 100644
index 0000000..facb1fc
--- /dev/null
+++ b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakePerceptionManager.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.testing
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.internal.Anchor
+import androidx.xr.runtime.internal.HitResult
+import androidx.xr.runtime.internal.PerceptionManager
+import androidx.xr.runtime.internal.Trackable
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Ray
+import java.util.UUID
+
+/** Test-only implementation of [PerceptionManager] used to validate state transitions. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class FakePerceptionManager : PerceptionManager, AnchorHolder {
+
+ public val anchors: MutableList<Anchor> = mutableListOf<Anchor>()
+ override val trackables: MutableList<Trackable> = mutableListOf<Trackable>()
+
+ private val hitResults = mutableListOf<HitResult>()
+ private val anchorUuids = mutableListOf<UUID>()
+
+ override fun createAnchor(pose: Pose): Anchor {
+ // TODO: b/349862231 - Modify it once detach is implemented.
+ val anchor = FakeRuntimeAnchor(pose, this)
+ anchors.add(anchor)
+ return anchor
+ }
+
+ override fun hitTest(ray: Ray): MutableList<HitResult> = hitResults
+
+ override fun getPersistedAnchorUuids(): List<UUID> = anchorUuids
+
+ override fun loadAnchor(uuid: UUID): Anchor {
+ check(anchorUuids.contains(uuid)) { "Anchor is not persisted." }
+ return FakeRuntimeAnchor(Pose(), this)
+ }
+
+ override fun unpersistAnchor(uuid: UUID) {
+ anchorUuids.remove(uuid)
+ }
+
+ override fun persistAnchor(anchor: Anchor) {
+ anchorUuids.add(anchor.uuid!!)
+ }
+
+ override fun loadAnchorFromNativePointer(nativePointer: Long): Anchor {
+ return FakeRuntimeAnchor(Pose(), this)
+ }
+
+ override fun detachAnchor(anchor: Anchor) {
+ anchors.remove(anchor)
+ anchor.uuid?.let { anchorUuids.remove(it) }
+ }
+
+ /** Adds a [HitResult] to the list that is returned when calling [hitTest] with any pose. */
+ public fun addHitResult(hitResult: HitResult) {
+ hitResults.add(hitResult)
+ }
+
+ /** Removes all [HitResult] instances passed to [addHitResult]. */
+ public fun clearHitResults() {
+ hitResults.clear()
+ }
+
+ /** Adds a [Trackable] to the list that is returned when calling [trackables]. */
+ public fun addTrackable(trackable: Trackable) {
+ trackables.add(trackable)
+ }
+
+ /** Removes all [Trackable] instances passed to [addTrackable]. */
+ public fun clearTrackables() {
+ trackables.clear()
+ }
+}
diff --git a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRuntime.kt b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRuntime.kt
new file mode 100644
index 0000000..72770b3
--- /dev/null
+++ b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRuntime.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.testing
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.internal.Runtime
+
+/** Test-only implementation of [Runtime] */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public data class FakeRuntime(
+ override val lifecycleManager: FakeLifecycleManager,
+ override val perceptionManager: FakePerceptionManager,
+) : Runtime {}
diff --git a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRuntimeAnchor.kt b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRuntimeAnchor.kt
new file mode 100644
index 0000000..895330a
--- /dev/null
+++ b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRuntimeAnchor.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.testing
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.internal.Anchor as RuntimeAnchor
+import androidx.xr.runtime.internal.TrackingState
+import androidx.xr.runtime.math.Pose
+import java.util.UUID
+
+/** Test-only implementation of [RuntimeAnchor] */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class FakeRuntimeAnchor(
+ override var pose: Pose,
+ public val anchorHolder: AnchorHolder? = null,
+) : RuntimeAnchor {
+ override var trackingState: TrackingState = TrackingState.Tracking
+
+ override var persistenceState: RuntimeAnchor.PersistenceState =
+ RuntimeAnchor.PersistenceState.NotPersisted
+
+ override var uuid: UUID? = null
+
+ /** Whether the anchor is attached to an [AnchorHolder] */
+ public var isAttached: Boolean = anchorHolder != null
+ private set
+
+ override fun persist() {
+ uuid = UUID.randomUUID()
+ persistenceState = RuntimeAnchor.PersistenceState.Persisted
+ anchorHolder?.persistAnchor(this)
+ }
+
+ override fun detach() {
+ if (anchorHolder != null) {
+ anchorHolder.detachAnchor(this)
+ isAttached = false
+ }
+ }
+}
diff --git a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRuntimeFactory.kt b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRuntimeFactory.kt
new file mode 100644
index 0000000..823f7a8
--- /dev/null
+++ b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRuntimeFactory.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.testing
+
+import android.app.Activity
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.internal.Runtime
+import androidx.xr.runtime.internal.RuntimeFactory
+
+/** Factory for creating test-only instances of [Runtime]. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class FakeRuntimeFactory() : RuntimeFactory {
+ override fun createRuntime(activity: Activity): FakeRuntime =
+ FakeRuntime(FakeLifecycleManager(), FakePerceptionManager())
+}
diff --git a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRuntimePlane.kt b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRuntimePlane.kt
new file mode 100644
index 0000000..8252e2c
--- /dev/null
+++ b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRuntimePlane.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.testing
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.internal.Anchor as RuntimeAnchor
+import androidx.xr.runtime.internal.Plane as RuntimePlane
+import androidx.xr.runtime.internal.TrackingState
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Vector2
+
+/** Test-only implementation of [Plane] */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class FakeRuntimePlane(
+ override val type: RuntimePlane.Type = RuntimePlane.Type.HorizontalUpwardFacing,
+ override val label: RuntimePlane.Label = RuntimePlane.Label.Floor,
+ override var trackingState: TrackingState = TrackingState.Tracking,
+ override var centerPose: Pose = Pose(),
+ override var extents: Vector2 = Vector2.Zero,
+ override var vertices: List<Vector2> = emptyList(),
+ override var subsumedBy: RuntimePlane? = null,
+ public val anchors: MutableCollection<RuntimeAnchor> = mutableListOf(),
+) : RuntimePlane, AnchorHolder {
+
+ override fun createAnchor(pose: Pose): RuntimeAnchor {
+ val anchor = FakeRuntimeAnchor(pose, this)
+ anchors.add(anchor)
+ return anchor
+ }
+
+ override fun detachAnchor(anchor: RuntimeAnchor) {
+ anchors.remove(anchor)
+ }
+
+ override fun persistAnchor(anchor: RuntimeAnchor) {}
+}
diff --git a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeStateExtender.kt b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeStateExtender.kt
new file mode 100644
index 0000000..5b36ca1b
--- /dev/null
+++ b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeStateExtender.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.testing
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.CoreState
+import androidx.xr.runtime.StateExtender
+import androidx.xr.runtime.internal.Runtime
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class FakeStateExtender() : StateExtender {
+
+ /** Whether the [StateExtender] has been initialized or not. */
+ public var isInitialized: Boolean = false
+
+ /** List of [CoreState] instances that have been extended. */
+ public val extended: MutableList<CoreState> = mutableListOf<CoreState>()
+
+ override fun initialize(runtime: Runtime) {
+ isInitialized = true
+ }
+
+ override suspend fun extend(coreState: CoreState) {
+ extended.add(coreState)
+ }
+}
diff --git a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/math/MathAssertions.kt b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/math/MathAssertions.kt
new file mode 100644
index 0000000..805d221
--- /dev/null
+++ b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/math/MathAssertions.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+@file:JvmName("MathAssertions")
+
+package androidx.xr.runtime.testing.math
+
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Vector3
+import com.google.common.truth.Truth.assertThat
+
+/**
+ * Asserts that two [Vector3]s are equal.
+ *
+ * @param actual the actual [Vector3] to compare
+ * @param expected the expected [Vector3] to compare
+ * @param epsilon the maximum difference allowed between the two [Vector3] objects
+ */
+@JvmOverloads
+public fun assertVector3(actual: Vector3, expected: Vector3, epsilon: Float = 1e-5f) {
+ assertThat(actual.x).isWithin(epsilon).of(expected.x)
+ assertThat(actual.y).isWithin(epsilon).of(expected.y)
+ assertThat(actual.z).isWithin(epsilon).of(expected.z)
+}
+
+/**
+ * Asserts that two [Quaternion]s are equal.
+ *
+ * @param actual the actual [Quaternion] to compare
+ * @param expected the expected [Quaternion] to compare
+ * @param epsilon the maximum difference allowed between the two [Quaternion] objects
+ */
+@JvmOverloads
+public fun assertRotation(actual: Quaternion, expected: Quaternion, epsilon: Float = 1e-5f) {
+ val dot = Math.abs(actual.toNormalized().dot(expected.toNormalized()))
+ assertThat(dot).isWithin(epsilon).of(1.0f)
+}
+
+/**
+ * Asserts that two [Pose]s are equal.
+ *
+ * @param actual the actual [Pose] to compare
+ * @param expected the expected [Pose] to compare
+ * @param epsilon the maximum difference allowed between the two [Pose] objects
+ */
+@JvmOverloads
+public fun assertPose(actual: Pose, expected: Pose, epsilon: Float = 1e-5f) {
+ assertVector3(actual.translation, expected.translation, epsilon)
+ assertRotation(actual.rotation, expected.rotation, epsilon)
+}
diff --git a/xr/runtime/runtime-testing/src/main/resources/META-INF/services/androidx.xr.runtime.StateExtender b/xr/runtime/runtime-testing/src/main/resources/META-INF/services/androidx.xr.runtime.StateExtender
new file mode 100644
index 0000000..4b3f581
--- /dev/null
+++ b/xr/runtime/runtime-testing/src/main/resources/META-INF/services/androidx.xr.runtime.StateExtender
@@ -0,0 +1 @@
+androidx.xr.runtime.testing.FakeStateExtender
diff --git a/xr/runtime/runtime-testing/src/main/resources/META-INF/services/androidx.xr.runtime.internal.RuntimeFactory b/xr/runtime/runtime-testing/src/main/resources/META-INF/services/androidx.xr.runtime.internal.RuntimeFactory
new file mode 100644
index 0000000..d7d1e9d
--- /dev/null
+++ b/xr/runtime/runtime-testing/src/main/resources/META-INF/services/androidx.xr.runtime.internal.RuntimeFactory
@@ -0,0 +1 @@
+androidx.xr.runtime.testing.FakeRuntimeFactory
\ No newline at end of file
diff --git a/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeLifecycleManagerTest.kt b/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeLifecycleManagerTest.kt
new file mode 100644
index 0000000..cebbd6e
--- /dev/null
+++ b/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeLifecycleManagerTest.kt
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.testing
+
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class FakeLifecycleManagerTest {
+
+ lateinit var underTest: FakeLifecycleManager
+
+ @Before
+ fun setUp() {
+ underTest = FakeLifecycleManager()
+ }
+
+ @Test
+ fun create_setsStateToInitialized() {
+ underTest.create()
+
+ assertThat(underTest.state).isEqualTo(FakeLifecycleManager.State.INITIALIZED)
+ }
+
+ @Test
+ fun create_calledTwice_throwsIllegalStateException() {
+ underTest.create()
+
+ assertFailsWith<IllegalStateException> { underTest.create() }
+ }
+
+ @Test
+ fun create_afterResume_throwsIllegalStateException() {
+ underTest.create()
+ underTest.resume()
+
+ assertFailsWith<IllegalStateException> { underTest.create() }
+ }
+
+ @Test
+ fun create_afterPause_throwsIllegalStateException() {
+ underTest.create()
+ underTest.resume()
+ underTest.pause()
+
+ assertFailsWith<IllegalStateException> { underTest.create() }
+ }
+
+ @Test
+ fun create_afterStop_throwsIllegalStateException() {
+ underTest.create()
+ underTest.stop()
+
+ assertFailsWith<IllegalStateException> { underTest.create() }
+ }
+
+ @Test
+ fun configure_beforeCreate_throwsIllegalStateException() {
+ assertFailsWith<IllegalStateException> { underTest.configure() }
+ }
+
+ @Test
+ fun configure_afterStop_throwsIllegalStateException() {
+ underTest.create()
+ underTest.stop()
+
+ assertFailsWith<IllegalStateException> { underTest.configure() }
+ }
+
+ @Test
+ fun resume_afterCreate_setsStateToResumed() {
+ underTest.create()
+
+ underTest.resume()
+
+ assertThat(underTest.state).isEqualTo(FakeLifecycleManager.State.RESUMED)
+ }
+
+ @Test
+ fun resume_calledTwice_throwsIllegalStateException() {
+ underTest.create()
+ underTest.resume()
+
+ assertFailsWith<IllegalStateException> { underTest.resume() }
+ }
+
+ @Test
+ fun resume_beforeCreate_throwsIllegalStateException() {
+ assertFailsWith<IllegalStateException> { underTest.resume() }
+ }
+
+ @Test
+ fun resume_afterStop_throwsIllegalStateException() {
+ underTest.create()
+ underTest.stop()
+
+ assertFailsWith<IllegalStateException> { underTest.resume() }
+ }
+
+ @Test
+ fun update_beforeCreate_throwsIllegalStateException() = runTest {
+ assertFailsWith<IllegalStateException> { underTest.update() }
+ }
+
+ @Test
+ fun update_afterCreate_throwsIllegalStateException() = runTest {
+ underTest.create()
+
+ assertFailsWith<IllegalStateException> { underTest.update() }
+ }
+
+ @Test
+ fun update_afterPause_throwsIllegalStateException() = runTest {
+ underTest.create()
+ underTest.resume()
+ underTest.pause()
+
+ assertFailsWith<IllegalStateException> { underTest.update() }
+ }
+
+ @Test
+ fun update_afterStop_throwsIllegalStateException() = runTest {
+ underTest.create()
+ underTest.stop()
+
+ assertFailsWith<IllegalStateException> { underTest.update() }
+ }
+
+ @Test
+ fun update_returnsTimeMarkFromTimeSource() = runTest {
+ val testDuration = 5.seconds
+ underTest.create()
+ underTest.resume()
+
+ val timeMark = underTest.update()
+ check(timeMark.elapsedNow().inWholeSeconds == 0L)
+ underTest.timeSource += testDuration
+
+ assertThat(timeMark.elapsedNow()).isEqualTo(testDuration)
+ }
+
+ @Test
+ fun update_calledTwiceAfterAllowOneMoreCallToUpdate_resumesExecution() = runTest {
+ val testDuration = 5.seconds
+ underTest.create()
+ underTest.resume()
+
+ val firstTimeMark = underTest.update()
+ underTest.timeSource += testDuration
+ underTest.allowOneMoreCallToUpdate()
+ val secondTimeMark = underTest.update()
+
+ assertThat(secondTimeMark - firstTimeMark).isEqualTo(testDuration)
+ }
+
+ @Test
+ fun pause_afterResume_setsStateToPaused() {
+ underTest.create()
+ underTest.resume()
+
+ underTest.pause()
+
+ assertThat(underTest.state).isEqualTo(FakeLifecycleManager.State.PAUSED)
+ }
+
+ @Test
+ fun pause_calledTwice_throwsIllegalStateException() {
+ underTest.create()
+ underTest.resume()
+ underTest.pause()
+
+ assertFailsWith<IllegalStateException> { underTest.pause() }
+ }
+
+ @Test
+ fun pause_beforeCreate_throwsIllegalStateException() {
+ assertFailsWith<IllegalStateException> { underTest.pause() }
+ }
+
+ @Test
+ fun pause_afterCreate_throwsIllegalStateException() {
+ underTest.create()
+
+ assertFailsWith<IllegalStateException> { underTest.pause() }
+ }
+
+ @Test
+ fun pause_afterStop_throwsIllegalStateException() {
+ underTest.create()
+ underTest.stop()
+
+ assertFailsWith<IllegalStateException> { underTest.pause() }
+ }
+
+ @Test
+ fun stop_afterCreate_setsStateToStopped() {
+ underTest.create()
+
+ underTest.stop()
+
+ assertThat(underTest.state).isEqualTo(FakeLifecycleManager.State.STOPPED)
+ }
+
+ @Test
+ fun stop_afterPause_setsStateToStopped() {
+ underTest.create()
+ underTest.resume()
+ underTest.pause()
+
+ underTest.stop()
+
+ assertThat(underTest.state).isEqualTo(FakeLifecycleManager.State.STOPPED)
+ }
+
+ @Test
+ fun stop_calledTwice_throwsIllegalStateException() {
+ underTest.create()
+ underTest.stop()
+
+ assertFailsWith<IllegalStateException> { underTest.stop() }
+ }
+
+ @Test
+ fun stop_beforeCreate_throwsIllegalStateException() {
+ assertFailsWith<IllegalStateException> { underTest.stop() }
+ }
+
+ @Test
+ fun stop_afterResume_throwsIllegalStateException() {
+ underTest.create()
+ underTest.resume()
+
+ assertFailsWith<IllegalStateException> { underTest.stop() }
+ }
+}
diff --git a/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakePerceptionManagerTest.kt b/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakePerceptionManagerTest.kt
new file mode 100644
index 0000000..1cee2e0
--- /dev/null
+++ b/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakePerceptionManagerTest.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.testing
+
+import androidx.xr.runtime.internal.Anchor
+import androidx.xr.runtime.internal.HitResult
+import androidx.xr.runtime.internal.Trackable
+import androidx.xr.runtime.internal.TrackingState
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Ray
+import androidx.xr.runtime.math.Vector3
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class FakePerceptionManagerTest {
+
+ lateinit var underTest: FakePerceptionManager
+
+ @Before
+ fun setUp() {
+ underTest = FakePerceptionManager()
+ }
+
+ @Test
+ fun createAnchor_addsAnchorToAnchors() {
+ val anchor = underTest.createAnchor(Pose())
+
+ assertThat(underTest.anchors).containsExactly(anchor)
+ }
+
+ @Test
+ fun detachAnchor_removesAnchorFromAnchors() {
+ val anchor = underTest.createAnchor(Pose())
+ check(underTest.anchors.contains(anchor))
+
+ anchor.detach()
+
+ assertThat(underTest.anchors).isEmpty()
+ }
+
+ @Test
+ fun createAnchor_returnsAnchorWithTheGivenPose() {
+ val pose = Pose(translation = Vector3(1f, 2f, 3f))
+
+ val anchor = underTest.createAnchor(pose)
+
+ assertThat(anchor.pose).isEqualTo(pose)
+ }
+
+ @Test
+ fun createAnchor_returnsAnchorWithTrackingStateTracking() {
+ val anchor = underTest.createAnchor(Pose())
+
+ assertThat(anchor.trackingState).isEqualTo(TrackingState.Tracking)
+ }
+
+ @Test
+ fun detach_removesAnchorFromAnchors() {
+ val anchor = underTest.createAnchor(Pose())
+ check(underTest.anchors.contains(anchor))
+
+ anchor.detach()
+
+ assertThat(underTest.anchors).isEmpty()
+ }
+
+ @Test
+ fun hitTest_returnsAddedHitResult() {
+ val ray = Ray(Vector3(1f, 2f, 3f), Vector3(4f, 5f, 6f))
+ val hitResult = HitResult(distance = 1f, Pose(), createStubTrackable())
+ underTest.addHitResult(hitResult)
+
+ assertThat(underTest.hitTest(ray)).containsExactly(hitResult)
+ }
+
+ @Test
+ fun clearHitResults_removesAllHitResults() {
+ val ray = Ray(Vector3(1f, 2f, 3f), Vector3(4f, 5f, 6f))
+ val hitResult = HitResult(distance = 1f, Pose(), createStubTrackable())
+ underTest.addHitResult(hitResult)
+
+ underTest.clearHitResults()
+
+ assertThat(underTest.hitTest(ray)).isEmpty()
+ }
+
+ @Test
+ fun addTrackable_addsTrackableToTrackables() {
+ val trackable = createStubTrackable()
+
+ underTest.addTrackable(trackable)
+
+ assertThat(underTest.trackables).containsExactly(trackable)
+ }
+
+ @Test
+ fun clearTrackables_removesAllTrackables() {
+ val trackable = createStubTrackable()
+ underTest.addTrackable(trackable)
+
+ underTest.clearTrackables()
+
+ assertThat(underTest.trackables).isEmpty()
+ }
+
+ @Test
+ fun getPersistedAnchorUuids_returnsOneUuid() {
+ val anchor = underTest.createAnchor(Pose())
+ anchor.persist()
+
+ assertThat(underTest.getPersistedAnchorUuids()).containsExactly(anchor.uuid!!)
+ }
+
+ @Test
+ fun loadAnchor_returnsNewAnchor() {
+ val persistedAnchor = underTest.createAnchor(Pose())
+ persistedAnchor.persist()
+
+ val loadedAnchor = underTest.loadAnchor(persistedAnchor.uuid!!)
+
+ assertThat(loadedAnchor.pose).isEqualTo(Pose())
+ }
+
+ @Test
+ fun unpersistAnchor_removesAnchorFromAnchorUuids() {
+ val anchor = underTest.createAnchor(Pose())
+ anchor.persist()
+ check(underTest.getPersistedAnchorUuids().contains(anchor.uuid!!))
+
+ underTest.unpersistAnchor(anchor.uuid!!)
+
+ assertThat(underTest.getPersistedAnchorUuids()).isEmpty()
+ }
+
+ private fun createStubTrackable() =
+ object : Trackable, AnchorHolder {
+ override fun createAnchor(pose: Pose): Anchor = underTest.createAnchor(pose)
+
+ override fun detachAnchor(anchor: Anchor) {
+ underTest.detachAnchor(anchor)
+ }
+
+ override fun persistAnchor(anchor: Anchor) {
+ underTest.persistAnchor(anchor)
+ }
+
+ override val trackingState = TrackingState.Tracking
+ }
+}
diff --git a/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeRuntimeAnchorTest.kt b/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeRuntimeAnchorTest.kt
new file mode 100644
index 0000000..bdf1cfc
--- /dev/null
+++ b/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeRuntimeAnchorTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.testing
+
+import androidx.xr.runtime.internal.Anchor
+import androidx.xr.runtime.math.Pose
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class FakeRuntimeAnchorTest {
+
+ @Test
+ fun constructor_anchorHolderNotNull_isAttached() {
+ val underTest = FakeRuntimeAnchor(Pose(), FakeRuntimePlane())
+
+ assertThat(underTest.isAttached).isTrue()
+ }
+
+ @Test
+ fun constructor_anchorHolderNull_isNotAttached() {
+ val underTest = FakeRuntimeAnchor(Pose())
+
+ assertThat(underTest.isAttached).isFalse()
+ }
+
+ @Test
+ fun persist_setsUuidToRandomValueAndPersistenceStateToPersisted() {
+ val underTest = FakeRuntimeAnchor(Pose())
+ check(underTest.uuid == null)
+ check(underTest.persistenceState == Anchor.PersistenceState.NotPersisted)
+
+ underTest.persist()
+
+ assertThat(underTest.uuid).isNotNull()
+ assertThat(underTest.persistenceState).isEqualTo(Anchor.PersistenceState.Persisted)
+ }
+
+ @Test
+ fun detach_attachedBecomesFalse() {
+ val underTest = FakeRuntimeAnchor(Pose(), FakeRuntimePlane())
+ check(underTest.isAttached.equals(true))
+
+ underTest.detach()
+
+ assertThat(underTest.isAttached).isFalse()
+ }
+}
diff --git a/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeRuntimeFactoryTest.kt b/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeRuntimeFactoryTest.kt
new file mode 100644
index 0000000..23e6c9b
--- /dev/null
+++ b/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeRuntimeFactoryTest.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.testing
+
+import android.app.Activity
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.runtime.internal.RuntimeFactory
+import com.google.common.truth.Truth.assertThat
+import java.util.ServiceLoader
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class FakeRuntimeFactoryTest {
+
+ @Test
+ fun class_isDiscoverableViaServiceLoader() {
+ assertThat(ServiceLoader.load(RuntimeFactory::class.java).iterator().next())
+ .isInstanceOf(FakeRuntimeFactory::class.java)
+ }
+
+ @Test
+ fun createRuntime_createsFakeRuntime() {
+ val underTest = FakeRuntimeFactory()
+
+ assertThat(underTest.createRuntime(Activity())).isInstanceOf(FakeRuntime::class.java)
+ }
+}
diff --git a/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeStateExtenderTest.kt b/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeStateExtenderTest.kt
new file mode 100644
index 0000000..8998c0b
--- /dev/null
+++ b/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeStateExtenderTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.testing
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.runtime.CoreState
+import androidx.xr.runtime.StateExtender
+import com.google.common.truth.Truth.assertThat
+import java.util.ServiceLoader
+import kotlin.time.ComparableTimeMark
+import kotlin.time.TestTimeSource
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class FakeStateExtenderTest {
+
+ lateinit var underTest: FakeStateExtender
+
+ @Before
+ fun setUp() {
+ underTest = FakeStateExtender()
+ }
+
+ @Test
+ fun class_isDiscoverableViaServiceLoader() {
+ assertThat(ServiceLoader.load(StateExtender::class.java).iterator().next())
+ .isInstanceOf(FakeStateExtender::class.java)
+ }
+
+ @Test
+ fun initialize_setsInitializedToTrue() {
+ check(underTest.isInitialized == false)
+
+ underTest.initialize(FakeRuntime(FakeLifecycleManager(), FakePerceptionManager()))
+
+ assertThat(underTest.isInitialized).isTrue()
+ }
+
+ @Test
+ fun extend_addsStateToExtended(): Unit = runBlocking {
+ check(underTest.extended.isEmpty())
+ val state = createStubCoreState(TestTimeSource().markNow())
+
+ underTest.extend(state)
+
+ assertThat(underTest.extended).containsExactly(state)
+ }
+
+ private fun createStubCoreState(timeMark: ComparableTimeMark): CoreState {
+ return CoreState(timeMark)
+ }
+}
diff --git a/xr/runtime/runtime/api/current.txt b/xr/runtime/runtime/api/current.txt
new file mode 100644
index 0000000..e89eee5
--- /dev/null
+++ b/xr/runtime/runtime/api/current.txt
@@ -0,0 +1,297 @@
+// Signature format: 4.0
+package androidx.xr.runtime.math {
+
+ public final class MathHelper {
+ method public static float clamp(float x, float min, float max);
+ method public static float lerp(float a, float b, float t);
+ method public static float toDegrees(float angleInRadians);
+ method public static float toRadians(float angleInDegrees);
+ }
+
+ public final class Matrix4 {
+ ctor public Matrix4(androidx.xr.runtime.math.Matrix4 other);
+ ctor public Matrix4(float[] dataToCopy);
+ method public androidx.xr.runtime.math.Matrix4 copy(optional float[] data);
+ method public static androidx.xr.runtime.math.Matrix4 fromPose(androidx.xr.runtime.math.Pose pose);
+ method public static androidx.xr.runtime.math.Matrix4 fromQuaternion(androidx.xr.runtime.math.Quaternion quaternion);
+ method public static androidx.xr.runtime.math.Matrix4 fromScale(androidx.xr.runtime.math.Vector3 scale);
+ method public static androidx.xr.runtime.math.Matrix4 fromScale(float scale);
+ method public static androidx.xr.runtime.math.Matrix4 fromTranslation(androidx.xr.runtime.math.Vector3 translation);
+ method public static androidx.xr.runtime.math.Matrix4 fromTrs(androidx.xr.runtime.math.Vector3 translation, androidx.xr.runtime.math.Quaternion rotation, androidx.xr.runtime.math.Vector3 scale);
+ method public float[] getData();
+ method public androidx.xr.runtime.math.Matrix4 getInverse();
+ method public androidx.xr.runtime.math.Pose getPose();
+ method public androidx.xr.runtime.math.Quaternion getRotation();
+ method public androidx.xr.runtime.math.Vector3 getScale();
+ method public androidx.xr.runtime.math.Vector3 getTranslation();
+ method public androidx.xr.runtime.math.Matrix4 getTranspose();
+ method public boolean isTrs();
+ method public operator androidx.xr.runtime.math.Matrix4 times(androidx.xr.runtime.math.Matrix4 other);
+ property public final float[] data;
+ property public final androidx.xr.runtime.math.Matrix4 inverse;
+ property public final boolean isTrs;
+ property public final androidx.xr.runtime.math.Pose pose;
+ property public final androidx.xr.runtime.math.Quaternion rotation;
+ property public final androidx.xr.runtime.math.Vector3 scale;
+ property public final androidx.xr.runtime.math.Vector3 translation;
+ property public final androidx.xr.runtime.math.Matrix4 transpose;
+ field public static final androidx.xr.runtime.math.Matrix4.Companion Companion;
+ field public static final androidx.xr.runtime.math.Matrix4 Identity;
+ field public static final androidx.xr.runtime.math.Matrix4 Zero;
+ }
+
+ public static final class Matrix4.Companion {
+ method public androidx.xr.runtime.math.Matrix4 fromPose(androidx.xr.runtime.math.Pose pose);
+ method public androidx.xr.runtime.math.Matrix4 fromQuaternion(androidx.xr.runtime.math.Quaternion quaternion);
+ method public androidx.xr.runtime.math.Matrix4 fromScale(androidx.xr.runtime.math.Vector3 scale);
+ method public androidx.xr.runtime.math.Matrix4 fromScale(float scale);
+ method public androidx.xr.runtime.math.Matrix4 fromTranslation(androidx.xr.runtime.math.Vector3 translation);
+ method public androidx.xr.runtime.math.Matrix4 fromTrs(androidx.xr.runtime.math.Vector3 translation, androidx.xr.runtime.math.Quaternion rotation, androidx.xr.runtime.math.Vector3 scale);
+ property public final androidx.xr.runtime.math.Matrix4 Identity;
+ property public final androidx.xr.runtime.math.Matrix4 Zero;
+ }
+
+ public final class Pose {
+ ctor public Pose();
+ ctor public Pose(androidx.xr.runtime.math.Pose other);
+ ctor public Pose(optional androidx.xr.runtime.math.Vector3 translation);
+ ctor public Pose(optional androidx.xr.runtime.math.Vector3 translation, optional androidx.xr.runtime.math.Quaternion rotation);
+ method public infix androidx.xr.runtime.math.Pose compose(androidx.xr.runtime.math.Pose other);
+ method public androidx.xr.runtime.math.Pose copy();
+ method public androidx.xr.runtime.math.Pose copy(optional androidx.xr.runtime.math.Vector3 translation);
+ method public androidx.xr.runtime.math.Pose copy(optional androidx.xr.runtime.math.Vector3 translation, optional androidx.xr.runtime.math.Quaternion rotation);
+ method public static float distance(androidx.xr.runtime.math.Pose lhs, androidx.xr.runtime.math.Pose rhs);
+ method public static androidx.xr.runtime.math.Pose fromLookAt(androidx.xr.runtime.math.Vector3 eye, androidx.xr.runtime.math.Vector3 target);
+ method public static androidx.xr.runtime.math.Pose fromLookAt(androidx.xr.runtime.math.Vector3 eye, androidx.xr.runtime.math.Vector3 target, optional androidx.xr.runtime.math.Vector3 up);
+ method public inline androidx.xr.runtime.math.Vector3 getBackward();
+ method public inline androidx.xr.runtime.math.Vector3 getDown();
+ method public inline androidx.xr.runtime.math.Vector3 getForward();
+ method public androidx.xr.runtime.math.Pose getInverse();
+ method public inline androidx.xr.runtime.math.Vector3 getLeft();
+ method public inline androidx.xr.runtime.math.Vector3 getRight();
+ method public androidx.xr.runtime.math.Quaternion getRotation();
+ method public androidx.xr.runtime.math.Vector3 getTranslation();
+ method public inline androidx.xr.runtime.math.Vector3 getUp();
+ method public static androidx.xr.runtime.math.Pose lerp(androidx.xr.runtime.math.Pose start, androidx.xr.runtime.math.Pose end, float ratio);
+ method public androidx.xr.runtime.math.Pose rotate(androidx.xr.runtime.math.Quaternion rotation);
+ method public infix androidx.xr.runtime.math.Vector3 transformPoint(androidx.xr.runtime.math.Vector3 point);
+ method public infix androidx.xr.runtime.math.Vector3 transformVector(androidx.xr.runtime.math.Vector3 vector);
+ method public androidx.xr.runtime.math.Pose translate(androidx.xr.runtime.math.Vector3 translation);
+ property public final inline androidx.xr.runtime.math.Vector3 backward;
+ property public final inline androidx.xr.runtime.math.Vector3 down;
+ property public final inline androidx.xr.runtime.math.Vector3 forward;
+ property public final androidx.xr.runtime.math.Pose inverse;
+ property public final inline androidx.xr.runtime.math.Vector3 left;
+ property public final inline androidx.xr.runtime.math.Vector3 right;
+ property public final androidx.xr.runtime.math.Quaternion rotation;
+ property public final androidx.xr.runtime.math.Vector3 translation;
+ property public final inline androidx.xr.runtime.math.Vector3 up;
+ field public static final androidx.xr.runtime.math.Pose.Companion Companion;
+ field public static final androidx.xr.runtime.math.Pose Identity;
+ }
+
+ public static final class Pose.Companion {
+ method public float distance(androidx.xr.runtime.math.Pose lhs, androidx.xr.runtime.math.Pose rhs);
+ method public androidx.xr.runtime.math.Pose fromLookAt(androidx.xr.runtime.math.Vector3 eye, androidx.xr.runtime.math.Vector3 target);
+ method public androidx.xr.runtime.math.Pose fromLookAt(androidx.xr.runtime.math.Vector3 eye, androidx.xr.runtime.math.Vector3 target, optional androidx.xr.runtime.math.Vector3 up);
+ method public androidx.xr.runtime.math.Pose lerp(androidx.xr.runtime.math.Pose start, androidx.xr.runtime.math.Pose end, float ratio);
+ property public final androidx.xr.runtime.math.Pose Identity;
+ }
+
+ public final class Quaternion {
+ ctor public Quaternion();
+ ctor public Quaternion(androidx.xr.runtime.math.Quaternion other);
+ ctor public Quaternion(optional float x);
+ ctor public Quaternion(optional float x, optional float y);
+ ctor public Quaternion(optional float x, optional float y, optional float z);
+ ctor public Quaternion(optional float x, optional float y, optional float z, optional float w);
+ method public static float angle(androidx.xr.runtime.math.Quaternion start, androidx.xr.runtime.math.Quaternion end);
+ method public androidx.xr.runtime.math.Quaternion copy();
+ method public androidx.xr.runtime.math.Quaternion copy(optional float x);
+ method public androidx.xr.runtime.math.Quaternion copy(optional float x, optional float y);
+ method public androidx.xr.runtime.math.Quaternion copy(optional float x, optional float y, optional float z);
+ method public androidx.xr.runtime.math.Quaternion copy(optional float x, optional float y, optional float z, optional float w);
+ method public operator androidx.xr.runtime.math.Quaternion div(float c);
+ method public inline infix float dot(androidx.xr.runtime.math.Quaternion other);
+ method public static float dot(androidx.xr.runtime.math.Quaternion lhs, androidx.xr.runtime.math.Quaternion rhs);
+ method public static androidx.xr.runtime.math.Quaternion fromAxisAngle(androidx.xr.runtime.math.Vector3 axis, float degrees);
+ method public static androidx.xr.runtime.math.Quaternion fromEulerAngles(androidx.xr.runtime.math.Vector3 eulerAngles);
+ method public static androidx.xr.runtime.math.Quaternion fromEulerAngles(float pitch, float yaw, float roll);
+ method public static androidx.xr.runtime.math.Quaternion fromLookTowards(androidx.xr.runtime.math.Vector3 forward, androidx.xr.runtime.math.Vector3 up);
+ method public static androidx.xr.runtime.math.Quaternion fromRotation(androidx.xr.runtime.math.Quaternion start, androidx.xr.runtime.math.Quaternion end);
+ method public static androidx.xr.runtime.math.Quaternion fromRotation(androidx.xr.runtime.math.Vector3 start, androidx.xr.runtime.math.Vector3 end);
+ method public kotlin.Pair<androidx.xr.runtime.math.Vector3,java.lang.Float> getAxisAngle();
+ method public androidx.xr.runtime.math.Vector3 getEulerAngles();
+ method public inline androidx.xr.runtime.math.Quaternion getInverse();
+ method public float getW();
+ method public float getX();
+ method public float getY();
+ method public float getZ();
+ method public static androidx.xr.runtime.math.Quaternion lerp(androidx.xr.runtime.math.Quaternion start, androidx.xr.runtime.math.Quaternion end, float ratio);
+ method public inline operator androidx.xr.runtime.math.Quaternion minus(androidx.xr.runtime.math.Quaternion other);
+ method public inline operator androidx.xr.runtime.math.Quaternion plus(androidx.xr.runtime.math.Quaternion other);
+ method public static androidx.xr.runtime.math.Quaternion slerp(androidx.xr.runtime.math.Quaternion start, androidx.xr.runtime.math.Quaternion end, float ratio);
+ method public inline operator androidx.xr.runtime.math.Quaternion times(androidx.xr.runtime.math.Quaternion other);
+ method public inline operator androidx.xr.runtime.math.Vector3 times(androidx.xr.runtime.math.Vector3 src);
+ method public operator androidx.xr.runtime.math.Quaternion times(float c);
+ method public androidx.xr.runtime.math.Quaternion toNormalized();
+ method public inline operator androidx.xr.runtime.math.Quaternion unaryMinus();
+ property public final kotlin.Pair<androidx.xr.runtime.math.Vector3,java.lang.Float> axisAngle;
+ property public final androidx.xr.runtime.math.Vector3 eulerAngles;
+ property public final inline androidx.xr.runtime.math.Quaternion inverse;
+ property public final float w;
+ property public final float x;
+ property public final float y;
+ property public final float z;
+ field public static final androidx.xr.runtime.math.Quaternion.Companion Companion;
+ field public static final androidx.xr.runtime.math.Quaternion Identity;
+ }
+
+ public static final class Quaternion.Companion {
+ method public float angle(androidx.xr.runtime.math.Quaternion start, androidx.xr.runtime.math.Quaternion end);
+ method public float dot(androidx.xr.runtime.math.Quaternion lhs, androidx.xr.runtime.math.Quaternion rhs);
+ method public androidx.xr.runtime.math.Quaternion fromAxisAngle(androidx.xr.runtime.math.Vector3 axis, float degrees);
+ method public androidx.xr.runtime.math.Quaternion fromEulerAngles(androidx.xr.runtime.math.Vector3 eulerAngles);
+ method public androidx.xr.runtime.math.Quaternion fromEulerAngles(float pitch, float yaw, float roll);
+ method public androidx.xr.runtime.math.Quaternion fromLookTowards(androidx.xr.runtime.math.Vector3 forward, androidx.xr.runtime.math.Vector3 up);
+ method public androidx.xr.runtime.math.Quaternion fromRotation(androidx.xr.runtime.math.Quaternion start, androidx.xr.runtime.math.Quaternion end);
+ method public androidx.xr.runtime.math.Quaternion fromRotation(androidx.xr.runtime.math.Vector3 start, androidx.xr.runtime.math.Vector3 end);
+ method public androidx.xr.runtime.math.Quaternion lerp(androidx.xr.runtime.math.Quaternion start, androidx.xr.runtime.math.Quaternion end, float ratio);
+ method public androidx.xr.runtime.math.Quaternion slerp(androidx.xr.runtime.math.Quaternion start, androidx.xr.runtime.math.Quaternion end, float ratio);
+ property public final androidx.xr.runtime.math.Quaternion Identity;
+ }
+
+ public final class Ray {
+ ctor public Ray();
+ ctor public Ray(androidx.xr.runtime.math.Ray other);
+ ctor public Ray(optional androidx.xr.runtime.math.Vector3 origin, optional androidx.xr.runtime.math.Vector3 direction);
+ method public androidx.xr.runtime.math.Vector3 getDirection();
+ method public androidx.xr.runtime.math.Vector3 getOrigin();
+ property public final androidx.xr.runtime.math.Vector3 direction;
+ property public final androidx.xr.runtime.math.Vector3 origin;
+ }
+
+ public final class Vector2 {
+ ctor public Vector2();
+ ctor public Vector2(androidx.xr.runtime.math.Vector2 other);
+ ctor public Vector2(optional float x);
+ ctor public Vector2(optional float x, optional float y);
+ method public static androidx.xr.runtime.math.Vector2 abs(androidx.xr.runtime.math.Vector2 vector);
+ method public static float angularDistance(androidx.xr.runtime.math.Vector2 vector1, androidx.xr.runtime.math.Vector2 vector2);
+ method public androidx.xr.runtime.math.Vector2 clamp(androidx.xr.runtime.math.Vector2 min, androidx.xr.runtime.math.Vector2 max);
+ method public inline androidx.xr.runtime.math.Vector2 copy();
+ method public inline androidx.xr.runtime.math.Vector2 copy(optional float x);
+ method public inline androidx.xr.runtime.math.Vector2 copy(optional float x, optional float y);
+ method public inline infix float cross(androidx.xr.runtime.math.Vector2 other);
+ method public static float distance(androidx.xr.runtime.math.Vector2 vector1, androidx.xr.runtime.math.Vector2 vector2);
+ method public inline operator androidx.xr.runtime.math.Vector2 div(androidx.xr.runtime.math.Vector2 other);
+ method public inline operator androidx.xr.runtime.math.Vector2 div(float c);
+ method public inline infix float dot(androidx.xr.runtime.math.Vector2 other);
+ method public inline float getLength();
+ method public inline float getLengthSquared();
+ method public float getX();
+ method public float getY();
+ method public static androidx.xr.runtime.math.Vector2 lerp(androidx.xr.runtime.math.Vector2 start, androidx.xr.runtime.math.Vector2 end, float ratio);
+ method public inline operator androidx.xr.runtime.math.Vector2 minus(androidx.xr.runtime.math.Vector2 other);
+ method public operator androidx.xr.runtime.math.Vector2 plus(androidx.xr.runtime.math.Vector2 other);
+ method public inline operator androidx.xr.runtime.math.Vector2 times(androidx.xr.runtime.math.Vector2 other);
+ method public inline operator androidx.xr.runtime.math.Vector2 times(float c);
+ method public androidx.xr.runtime.math.Vector2 toNormalized();
+ method public inline operator androidx.xr.runtime.math.Vector2 unaryMinus();
+ property public final inline float length;
+ property public final inline float lengthSquared;
+ property public final float x;
+ property public final float y;
+ field public static final androidx.xr.runtime.math.Vector2.Companion Companion;
+ field public static final androidx.xr.runtime.math.Vector2 Down;
+ field public static final androidx.xr.runtime.math.Vector2 Left;
+ field public static final androidx.xr.runtime.math.Vector2 One;
+ field public static final androidx.xr.runtime.math.Vector2 Right;
+ field public static final androidx.xr.runtime.math.Vector2 Up;
+ field public static final androidx.xr.runtime.math.Vector2 Zero;
+ }
+
+ public static final class Vector2.Companion {
+ method public androidx.xr.runtime.math.Vector2 abs(androidx.xr.runtime.math.Vector2 vector);
+ method public float angularDistance(androidx.xr.runtime.math.Vector2 vector1, androidx.xr.runtime.math.Vector2 vector2);
+ method public float distance(androidx.xr.runtime.math.Vector2 vector1, androidx.xr.runtime.math.Vector2 vector2);
+ method public androidx.xr.runtime.math.Vector2 lerp(androidx.xr.runtime.math.Vector2 start, androidx.xr.runtime.math.Vector2 end, float ratio);
+ property public final androidx.xr.runtime.math.Vector2 Down;
+ property public final androidx.xr.runtime.math.Vector2 Left;
+ property public final androidx.xr.runtime.math.Vector2 One;
+ property public final androidx.xr.runtime.math.Vector2 Right;
+ property public final androidx.xr.runtime.math.Vector2 Up;
+ property public final androidx.xr.runtime.math.Vector2 Zero;
+ }
+
+ public final class Vector3 {
+ ctor public Vector3();
+ ctor public Vector3(androidx.xr.runtime.math.Vector3 other);
+ ctor public Vector3(optional float x);
+ ctor public Vector3(optional float x, optional float y);
+ ctor public Vector3(optional float x, optional float y, optional float z);
+ method public static androidx.xr.runtime.math.Vector3 abs(androidx.xr.runtime.math.Vector3 vector);
+ method public static float angleBetween(androidx.xr.runtime.math.Vector3 vector1, androidx.xr.runtime.math.Vector3 vector2);
+ method public androidx.xr.runtime.math.Vector3 clamp(androidx.xr.runtime.math.Vector3 min, androidx.xr.runtime.math.Vector3 max);
+ method public androidx.xr.runtime.math.Vector3 copy();
+ method public androidx.xr.runtime.math.Vector3 copy(optional float x);
+ method public androidx.xr.runtime.math.Vector3 copy(optional float x, optional float y);
+ method public androidx.xr.runtime.math.Vector3 copy(optional float x, optional float y, optional float z);
+ method public infix androidx.xr.runtime.math.Vector3 cross(androidx.xr.runtime.math.Vector3 other);
+ method public static float distance(androidx.xr.runtime.math.Vector3 vector1, androidx.xr.runtime.math.Vector3 vector2);
+ method public operator androidx.xr.runtime.math.Vector3 div(androidx.xr.runtime.math.Vector3 other);
+ method public operator androidx.xr.runtime.math.Vector3 div(float c);
+ method public infix float dot(androidx.xr.runtime.math.Vector3 other);
+ method public static androidx.xr.runtime.math.Vector3 fromValue(float value);
+ method public inline float getLength();
+ method public inline float getLengthSquared();
+ method public float getX();
+ method public float getY();
+ method public float getZ();
+ method public static androidx.xr.runtime.math.Vector3 lerp(androidx.xr.runtime.math.Vector3 start, androidx.xr.runtime.math.Vector3 end, float ratio);
+ method public static androidx.xr.runtime.math.Vector3 max(androidx.xr.runtime.math.Vector3 a, androidx.xr.runtime.math.Vector3 b);
+ method public static androidx.xr.runtime.math.Vector3 min(androidx.xr.runtime.math.Vector3 a, androidx.xr.runtime.math.Vector3 b);
+ method public operator androidx.xr.runtime.math.Vector3 minus(androidx.xr.runtime.math.Vector3 other);
+ method public operator androidx.xr.runtime.math.Vector3 plus(androidx.xr.runtime.math.Vector3 other);
+ method public static androidx.xr.runtime.math.Vector3 projectOnPlane(androidx.xr.runtime.math.Vector3 vector, androidx.xr.runtime.math.Vector3 planeNormal);
+ method public operator androidx.xr.runtime.math.Vector3 times(androidx.xr.runtime.math.Vector3 other);
+ method public operator androidx.xr.runtime.math.Vector3 times(float c);
+ method public androidx.xr.runtime.math.Vector3 toNormalized();
+ method public operator androidx.xr.runtime.math.Vector3 unaryMinus();
+ property public final inline float length;
+ property public final inline float lengthSquared;
+ property public final float x;
+ property public final float y;
+ property public final float z;
+ field public static final androidx.xr.runtime.math.Vector3 Backward;
+ field public static final androidx.xr.runtime.math.Vector3.Companion Companion;
+ field public static final androidx.xr.runtime.math.Vector3 Down;
+ field public static final androidx.xr.runtime.math.Vector3 Forward;
+ field public static final androidx.xr.runtime.math.Vector3 Left;
+ field public static final androidx.xr.runtime.math.Vector3 One;
+ field public static final androidx.xr.runtime.math.Vector3 Right;
+ field public static final androidx.xr.runtime.math.Vector3 Up;
+ field public static final androidx.xr.runtime.math.Vector3 Zero;
+ }
+
+ public static final class Vector3.Companion {
+ method public androidx.xr.runtime.math.Vector3 abs(androidx.xr.runtime.math.Vector3 vector);
+ method public float angleBetween(androidx.xr.runtime.math.Vector3 vector1, androidx.xr.runtime.math.Vector3 vector2);
+ method public float distance(androidx.xr.runtime.math.Vector3 vector1, androidx.xr.runtime.math.Vector3 vector2);
+ method public androidx.xr.runtime.math.Vector3 fromValue(float value);
+ method public androidx.xr.runtime.math.Vector3 lerp(androidx.xr.runtime.math.Vector3 start, androidx.xr.runtime.math.Vector3 end, float ratio);
+ method public androidx.xr.runtime.math.Vector3 max(androidx.xr.runtime.math.Vector3 a, androidx.xr.runtime.math.Vector3 b);
+ method public androidx.xr.runtime.math.Vector3 min(androidx.xr.runtime.math.Vector3 a, androidx.xr.runtime.math.Vector3 b);
+ method public androidx.xr.runtime.math.Vector3 projectOnPlane(androidx.xr.runtime.math.Vector3 vector, androidx.xr.runtime.math.Vector3 planeNormal);
+ property public final androidx.xr.runtime.math.Vector3 Backward;
+ property public final androidx.xr.runtime.math.Vector3 Down;
+ property public final androidx.xr.runtime.math.Vector3 Forward;
+ property public final androidx.xr.runtime.math.Vector3 Left;
+ property public final androidx.xr.runtime.math.Vector3 One;
+ property public final androidx.xr.runtime.math.Vector3 Right;
+ property public final androidx.xr.runtime.math.Vector3 Up;
+ property public final androidx.xr.runtime.math.Vector3 Zero;
+ }
+
+}
+
diff --git a/camera/camera-effects-still-portrait/api/res-current.txt b/xr/runtime/runtime/api/res-current.txt
similarity index 100%
copy from camera/camera-effects-still-portrait/api/res-current.txt
copy to xr/runtime/runtime/api/res-current.txt
diff --git a/xr/runtime/runtime/api/restricted_current.txt b/xr/runtime/runtime/api/restricted_current.txt
new file mode 100644
index 0000000..333830a
--- /dev/null
+++ b/xr/runtime/runtime/api/restricted_current.txt
@@ -0,0 +1,537 @@
+// Signature format: 4.0
+package androidx.xr.runtime {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class CoreState {
+ ctor public CoreState(kotlin.time.ComparableTimeMark timeMark);
+ method public kotlin.time.ComparableTimeMark component1();
+ method public androidx.xr.runtime.CoreState copy(kotlin.time.ComparableTimeMark timeMark);
+ method public kotlin.time.ComparableTimeMark getTimeMark();
+ property public final kotlin.time.ComparableTimeMark timeMark;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class Session {
+ method public androidx.xr.runtime.SessionConfigureResult configure();
+ method public static androidx.xr.runtime.SessionCreateResult create(android.app.Activity activity);
+ method public static androidx.xr.runtime.SessionCreateResult create(android.app.Activity activity, optional kotlinx.coroutines.CoroutineDispatcher coroutineDispatcher);
+ method public void destroy();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public kotlinx.coroutines.CoroutineScope getCoroutineScope();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.runtime.internal.Runtime getRuntime();
+ method public kotlinx.coroutines.flow.StateFlow<androidx.xr.runtime.CoreState> getState();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public java.util.List<androidx.xr.runtime.StateExtender> getStateExtenders();
+ method public void pause();
+ method public androidx.xr.runtime.SessionResumeResult resume();
+ property @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final kotlinx.coroutines.CoroutineScope coroutineScope;
+ property @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final androidx.xr.runtime.internal.Runtime runtime;
+ property public final kotlinx.coroutines.flow.StateFlow<androidx.xr.runtime.CoreState> state;
+ property @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final java.util.List<androidx.xr.runtime.StateExtender> stateExtenders;
+ field public static final androidx.xr.runtime.Session.Companion Companion;
+ }
+
+ public static final class Session.Companion {
+ method public androidx.xr.runtime.SessionCreateResult create(android.app.Activity activity);
+ method public androidx.xr.runtime.SessionCreateResult create(android.app.Activity activity, optional kotlinx.coroutines.CoroutineDispatcher coroutineDispatcher);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SessionConfigureConfigurationNotSupported extends androidx.xr.runtime.SessionConfigureResult {
+ ctor public SessionConfigureConfigurationNotSupported();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract sealed class SessionConfigureResult {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SessionConfigureSuccess extends androidx.xr.runtime.SessionConfigureResult {
+ ctor public SessionConfigureSuccess();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SessionCreatePermissionsNotGranted extends androidx.xr.runtime.SessionCreateResult {
+ ctor public SessionCreatePermissionsNotGranted(java.util.List<java.lang.String> permissions);
+ method public java.util.List<java.lang.String> getPermissions();
+ property public final java.util.List<java.lang.String> permissions;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract sealed class SessionCreateResult {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SessionCreateSuccess extends androidx.xr.runtime.SessionCreateResult {
+ ctor public SessionCreateSuccess(androidx.xr.runtime.Session session);
+ method public androidx.xr.runtime.Session getSession();
+ property public final androidx.xr.runtime.Session session;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SessionResumePermissionsNotGranted extends androidx.xr.runtime.SessionResumeResult {
+ ctor public SessionResumePermissionsNotGranted(java.util.List<java.lang.String> permissions);
+ method public java.util.List<java.lang.String> getPermissions();
+ property public final java.util.List<java.lang.String> permissions;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract sealed class SessionResumeResult {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SessionResumeSuccess extends androidx.xr.runtime.SessionResumeResult {
+ ctor public SessionResumeSuccess();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface StateExtender {
+ method public suspend Object? extend(androidx.xr.runtime.CoreState coreState, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public void initialize(androidx.xr.runtime.internal.Runtime runtime);
+ }
+
+}
+
+package androidx.xr.runtime.internal {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Anchor {
+ method public void detach();
+ method public androidx.xr.runtime.internal.Anchor.PersistenceState getPersistenceState();
+ method public androidx.xr.runtime.math.Pose getPose();
+ method public androidx.xr.runtime.internal.TrackingState getTrackingState();
+ method public java.util.UUID? getUuid();
+ method public void persist();
+ property public abstract androidx.xr.runtime.internal.Anchor.PersistenceState persistenceState;
+ property public abstract androidx.xr.runtime.math.Pose pose;
+ property public abstract androidx.xr.runtime.internal.TrackingState trackingState;
+ property public abstract java.util.UUID? uuid;
+ }
+
+ public static final class Anchor.PersistenceState {
+ field public static final androidx.xr.runtime.internal.Anchor.PersistenceState.Companion Companion;
+ field public static final androidx.xr.runtime.internal.Anchor.PersistenceState NotPersisted;
+ field public static final androidx.xr.runtime.internal.Anchor.PersistenceState Pending;
+ field public static final androidx.xr.runtime.internal.Anchor.PersistenceState Persisted;
+ }
+
+ public static final class Anchor.PersistenceState.Companion {
+ property public final androidx.xr.runtime.internal.Anchor.PersistenceState NotPersisted;
+ property public final androidx.xr.runtime.internal.Anchor.PersistenceState Pending;
+ property public final androidx.xr.runtime.internal.Anchor.PersistenceState Persisted;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class HitResult {
+ ctor public HitResult(float distance, androidx.xr.runtime.math.Pose hitPose, androidx.xr.runtime.internal.Trackable trackable);
+ method public float getDistance();
+ method public androidx.xr.runtime.math.Pose getHitPose();
+ method public androidx.xr.runtime.internal.Trackable getTrackable();
+ property public final float distance;
+ property public final androidx.xr.runtime.math.Pose hitPose;
+ property public final androidx.xr.runtime.internal.Trackable trackable;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface LifecycleManager {
+ method public void configure();
+ method public void create();
+ method public void pause();
+ method public void resume();
+ method public void stop();
+ method public suspend Object? update(kotlin.coroutines.Continuation<? super kotlin.time.ComparableTimeMark>);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface PerceptionManager {
+ method public androidx.xr.runtime.internal.Anchor createAnchor(androidx.xr.runtime.math.Pose pose);
+ method public java.util.List<java.util.UUID> getPersistedAnchorUuids();
+ method public java.util.Collection<androidx.xr.runtime.internal.Trackable> getTrackables();
+ method public java.util.List<androidx.xr.runtime.internal.HitResult> hitTest(androidx.xr.runtime.math.Ray ray);
+ method public androidx.xr.runtime.internal.Anchor loadAnchor(java.util.UUID uuid);
+ method public androidx.xr.runtime.internal.Anchor loadAnchorFromNativePointer(long nativePointer);
+ method public void unpersistAnchor(java.util.UUID uuid);
+ property public abstract java.util.Collection<androidx.xr.runtime.internal.Trackable> trackables;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Plane extends androidx.xr.runtime.internal.Trackable {
+ method public androidx.xr.runtime.math.Pose getCenterPose();
+ method public androidx.xr.runtime.math.Vector2 getExtents();
+ method public androidx.xr.runtime.internal.Plane.Label getLabel();
+ method public androidx.xr.runtime.internal.Plane? getSubsumedBy();
+ method public androidx.xr.runtime.internal.Plane.Type getType();
+ method public java.util.List<androidx.xr.runtime.math.Vector2> getVertices();
+ property public abstract androidx.xr.runtime.math.Pose centerPose;
+ property public abstract androidx.xr.runtime.math.Vector2 extents;
+ property public abstract androidx.xr.runtime.internal.Plane.Label label;
+ property public abstract androidx.xr.runtime.internal.Plane? subsumedBy;
+ property public abstract androidx.xr.runtime.internal.Plane.Type type;
+ property public abstract java.util.List<androidx.xr.runtime.math.Vector2> vertices;
+ }
+
+ public static final class Plane.Label {
+ field public static final androidx.xr.runtime.internal.Plane.Label Ceiling;
+ field public static final androidx.xr.runtime.internal.Plane.Label.Companion Companion;
+ field public static final androidx.xr.runtime.internal.Plane.Label Floor;
+ field public static final androidx.xr.runtime.internal.Plane.Label Table;
+ field public static final androidx.xr.runtime.internal.Plane.Label Unknown;
+ field public static final androidx.xr.runtime.internal.Plane.Label Wall;
+ }
+
+ public static final class Plane.Label.Companion {
+ property public final androidx.xr.runtime.internal.Plane.Label Ceiling;
+ property public final androidx.xr.runtime.internal.Plane.Label Floor;
+ property public final androidx.xr.runtime.internal.Plane.Label Table;
+ property public final androidx.xr.runtime.internal.Plane.Label Unknown;
+ property public final androidx.xr.runtime.internal.Plane.Label Wall;
+ }
+
+ public static final class Plane.Type {
+ field public static final androidx.xr.runtime.internal.Plane.Type.Companion Companion;
+ field public static final androidx.xr.runtime.internal.Plane.Type HorizontalDownwardFacing;
+ field public static final androidx.xr.runtime.internal.Plane.Type HorizontalUpwardFacing;
+ field public static final androidx.xr.runtime.internal.Plane.Type Vertical;
+ }
+
+ public static final class Plane.Type.Companion {
+ property public final androidx.xr.runtime.internal.Plane.Type HorizontalDownwardFacing;
+ property public final androidx.xr.runtime.internal.Plane.Type HorizontalUpwardFacing;
+ property public final androidx.xr.runtime.internal.Plane.Type Vertical;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Runtime {
+ method public androidx.xr.runtime.internal.LifecycleManager getLifecycleManager();
+ method public androidx.xr.runtime.internal.PerceptionManager getPerceptionManager();
+ property public abstract androidx.xr.runtime.internal.LifecycleManager lifecycleManager;
+ property public abstract androidx.xr.runtime.internal.PerceptionManager perceptionManager;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface RuntimeFactory {
+ method public androidx.xr.runtime.internal.Runtime createRuntime(android.app.Activity activity);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Space extends androidx.xr.runtime.internal.Trackable {
+ }
+
+ public static final class Space.Type {
+ field public static final androidx.xr.runtime.internal.Space.Type.Companion Companion;
+ field public static final androidx.xr.runtime.internal.Space.Type Local;
+ field public static final androidx.xr.runtime.internal.Space.Type LocalFloor;
+ }
+
+ public static final class Space.Type.Companion {
+ property public final androidx.xr.runtime.internal.Space.Type Local;
+ property public final androidx.xr.runtime.internal.Space.Type LocalFloor;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Trackable {
+ method public androidx.xr.runtime.internal.Anchor createAnchor(androidx.xr.runtime.math.Pose pose);
+ method public androidx.xr.runtime.internal.TrackingState getTrackingState();
+ property public abstract androidx.xr.runtime.internal.TrackingState trackingState;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class TrackingState {
+ field public static final androidx.xr.runtime.internal.TrackingState.Companion Companion;
+ field public static final androidx.xr.runtime.internal.TrackingState Paused;
+ field public static final androidx.xr.runtime.internal.TrackingState Stopped;
+ field public static final androidx.xr.runtime.internal.TrackingState Tracking;
+ }
+
+ public static final class TrackingState.Companion {
+ property public final androidx.xr.runtime.internal.TrackingState Paused;
+ property public final androidx.xr.runtime.internal.TrackingState Stopped;
+ property public final androidx.xr.runtime.internal.TrackingState Tracking;
+ }
+
+}
+
+package androidx.xr.runtime.java {
+
+ public final class Coroutines {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> com.google.common.util.concurrent.ListenableFuture<T> toFuture(androidx.xr.runtime.Session session, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,? extends java.lang.Object?> coroutine);
+ }
+
+ public final class Flows {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.rxjava3.core.Observable<T> toObservable(androidx.xr.runtime.Session session, kotlinx.coroutines.flow.Flow<? extends T> flow);
+ }
+
+}
+
+package androidx.xr.runtime.math {
+
+ public final class MathHelper {
+ method public static float clamp(float x, float min, float max);
+ method public static float lerp(float a, float b, float t);
+ method public static float toDegrees(float angleInRadians);
+ method public static float toRadians(float angleInDegrees);
+ }
+
+ public final class Matrix4 {
+ ctor public Matrix4(androidx.xr.runtime.math.Matrix4 other);
+ ctor public Matrix4(float[] dataToCopy);
+ method public androidx.xr.runtime.math.Matrix4 copy(optional float[] data);
+ method public static androidx.xr.runtime.math.Matrix4 fromPose(androidx.xr.runtime.math.Pose pose);
+ method public static androidx.xr.runtime.math.Matrix4 fromQuaternion(androidx.xr.runtime.math.Quaternion quaternion);
+ method public static androidx.xr.runtime.math.Matrix4 fromScale(androidx.xr.runtime.math.Vector3 scale);
+ method public static androidx.xr.runtime.math.Matrix4 fromScale(float scale);
+ method public static androidx.xr.runtime.math.Matrix4 fromTranslation(androidx.xr.runtime.math.Vector3 translation);
+ method public static androidx.xr.runtime.math.Matrix4 fromTrs(androidx.xr.runtime.math.Vector3 translation, androidx.xr.runtime.math.Quaternion rotation, androidx.xr.runtime.math.Vector3 scale);
+ method public float[] getData();
+ method public androidx.xr.runtime.math.Matrix4 getInverse();
+ method public androidx.xr.runtime.math.Pose getPose();
+ method public androidx.xr.runtime.math.Quaternion getRotation();
+ method public androidx.xr.runtime.math.Vector3 getScale();
+ method public androidx.xr.runtime.math.Vector3 getTranslation();
+ method public androidx.xr.runtime.math.Matrix4 getTranspose();
+ method public boolean isTrs();
+ method public operator androidx.xr.runtime.math.Matrix4 times(androidx.xr.runtime.math.Matrix4 other);
+ property public final float[] data;
+ property public final androidx.xr.runtime.math.Matrix4 inverse;
+ property public final boolean isTrs;
+ property public final androidx.xr.runtime.math.Pose pose;
+ property public final androidx.xr.runtime.math.Quaternion rotation;
+ property public final androidx.xr.runtime.math.Vector3 scale;
+ property public final androidx.xr.runtime.math.Vector3 translation;
+ property public final androidx.xr.runtime.math.Matrix4 transpose;
+ field public static final androidx.xr.runtime.math.Matrix4.Companion Companion;
+ field public static final androidx.xr.runtime.math.Matrix4 Identity;
+ field public static final androidx.xr.runtime.math.Matrix4 Zero;
+ }
+
+ public static final class Matrix4.Companion {
+ method public androidx.xr.runtime.math.Matrix4 fromPose(androidx.xr.runtime.math.Pose pose);
+ method public androidx.xr.runtime.math.Matrix4 fromQuaternion(androidx.xr.runtime.math.Quaternion quaternion);
+ method public androidx.xr.runtime.math.Matrix4 fromScale(androidx.xr.runtime.math.Vector3 scale);
+ method public androidx.xr.runtime.math.Matrix4 fromScale(float scale);
+ method public androidx.xr.runtime.math.Matrix4 fromTranslation(androidx.xr.runtime.math.Vector3 translation);
+ method public androidx.xr.runtime.math.Matrix4 fromTrs(androidx.xr.runtime.math.Vector3 translation, androidx.xr.runtime.math.Quaternion rotation, androidx.xr.runtime.math.Vector3 scale);
+ property public final androidx.xr.runtime.math.Matrix4 Identity;
+ property public final androidx.xr.runtime.math.Matrix4 Zero;
+ }
+
+ public final class Pose {
+ ctor public Pose();
+ ctor public Pose(androidx.xr.runtime.math.Pose other);
+ ctor public Pose(optional androidx.xr.runtime.math.Vector3 translation);
+ ctor public Pose(optional androidx.xr.runtime.math.Vector3 translation, optional androidx.xr.runtime.math.Quaternion rotation);
+ method public infix androidx.xr.runtime.math.Pose compose(androidx.xr.runtime.math.Pose other);
+ method public androidx.xr.runtime.math.Pose copy();
+ method public androidx.xr.runtime.math.Pose copy(optional androidx.xr.runtime.math.Vector3 translation);
+ method public androidx.xr.runtime.math.Pose copy(optional androidx.xr.runtime.math.Vector3 translation, optional androidx.xr.runtime.math.Quaternion rotation);
+ method public static float distance(androidx.xr.runtime.math.Pose lhs, androidx.xr.runtime.math.Pose rhs);
+ method public static androidx.xr.runtime.math.Pose fromLookAt(androidx.xr.runtime.math.Vector3 eye, androidx.xr.runtime.math.Vector3 target);
+ method public static androidx.xr.runtime.math.Pose fromLookAt(androidx.xr.runtime.math.Vector3 eye, androidx.xr.runtime.math.Vector3 target, optional androidx.xr.runtime.math.Vector3 up);
+ method public inline androidx.xr.runtime.math.Vector3 getBackward();
+ method public inline androidx.xr.runtime.math.Vector3 getDown();
+ method public inline androidx.xr.runtime.math.Vector3 getForward();
+ method public androidx.xr.runtime.math.Pose getInverse();
+ method public inline androidx.xr.runtime.math.Vector3 getLeft();
+ method public inline androidx.xr.runtime.math.Vector3 getRight();
+ method public androidx.xr.runtime.math.Quaternion getRotation();
+ method public androidx.xr.runtime.math.Vector3 getTranslation();
+ method public inline androidx.xr.runtime.math.Vector3 getUp();
+ method public static androidx.xr.runtime.math.Pose lerp(androidx.xr.runtime.math.Pose start, androidx.xr.runtime.math.Pose end, float ratio);
+ method public androidx.xr.runtime.math.Pose rotate(androidx.xr.runtime.math.Quaternion rotation);
+ method public infix androidx.xr.runtime.math.Vector3 transformPoint(androidx.xr.runtime.math.Vector3 point);
+ method public infix androidx.xr.runtime.math.Vector3 transformVector(androidx.xr.runtime.math.Vector3 vector);
+ method public androidx.xr.runtime.math.Pose translate(androidx.xr.runtime.math.Vector3 translation);
+ property public final inline androidx.xr.runtime.math.Vector3 backward;
+ property public final inline androidx.xr.runtime.math.Vector3 down;
+ property public final inline androidx.xr.runtime.math.Vector3 forward;
+ property public final androidx.xr.runtime.math.Pose inverse;
+ property public final inline androidx.xr.runtime.math.Vector3 left;
+ property public final inline androidx.xr.runtime.math.Vector3 right;
+ property public final androidx.xr.runtime.math.Quaternion rotation;
+ property public final androidx.xr.runtime.math.Vector3 translation;
+ property public final inline androidx.xr.runtime.math.Vector3 up;
+ field public static final androidx.xr.runtime.math.Pose.Companion Companion;
+ field public static final androidx.xr.runtime.math.Pose Identity;
+ }
+
+ public static final class Pose.Companion {
+ method public float distance(androidx.xr.runtime.math.Pose lhs, androidx.xr.runtime.math.Pose rhs);
+ method public androidx.xr.runtime.math.Pose fromLookAt(androidx.xr.runtime.math.Vector3 eye, androidx.xr.runtime.math.Vector3 target);
+ method public androidx.xr.runtime.math.Pose fromLookAt(androidx.xr.runtime.math.Vector3 eye, androidx.xr.runtime.math.Vector3 target, optional androidx.xr.runtime.math.Vector3 up);
+ method public androidx.xr.runtime.math.Pose lerp(androidx.xr.runtime.math.Pose start, androidx.xr.runtime.math.Pose end, float ratio);
+ property public final androidx.xr.runtime.math.Pose Identity;
+ }
+
+ public final class Quaternion {
+ ctor public Quaternion();
+ ctor public Quaternion(androidx.xr.runtime.math.Quaternion other);
+ ctor public Quaternion(optional float x);
+ ctor public Quaternion(optional float x, optional float y);
+ ctor public Quaternion(optional float x, optional float y, optional float z);
+ ctor public Quaternion(optional float x, optional float y, optional float z, optional float w);
+ method public static float angle(androidx.xr.runtime.math.Quaternion start, androidx.xr.runtime.math.Quaternion end);
+ method public androidx.xr.runtime.math.Quaternion copy();
+ method public androidx.xr.runtime.math.Quaternion copy(optional float x);
+ method public androidx.xr.runtime.math.Quaternion copy(optional float x, optional float y);
+ method public androidx.xr.runtime.math.Quaternion copy(optional float x, optional float y, optional float z);
+ method public androidx.xr.runtime.math.Quaternion copy(optional float x, optional float y, optional float z, optional float w);
+ method public operator androidx.xr.runtime.math.Quaternion div(float c);
+ method public inline infix float dot(androidx.xr.runtime.math.Quaternion other);
+ method public static float dot(androidx.xr.runtime.math.Quaternion lhs, androidx.xr.runtime.math.Quaternion rhs);
+ method public static androidx.xr.runtime.math.Quaternion fromAxisAngle(androidx.xr.runtime.math.Vector3 axis, float degrees);
+ method public static androidx.xr.runtime.math.Quaternion fromEulerAngles(androidx.xr.runtime.math.Vector3 eulerAngles);
+ method public static androidx.xr.runtime.math.Quaternion fromEulerAngles(float pitch, float yaw, float roll);
+ method public static androidx.xr.runtime.math.Quaternion fromLookTowards(androidx.xr.runtime.math.Vector3 forward, androidx.xr.runtime.math.Vector3 up);
+ method public static androidx.xr.runtime.math.Quaternion fromRotation(androidx.xr.runtime.math.Quaternion start, androidx.xr.runtime.math.Quaternion end);
+ method public static androidx.xr.runtime.math.Quaternion fromRotation(androidx.xr.runtime.math.Vector3 start, androidx.xr.runtime.math.Vector3 end);
+ method public kotlin.Pair<androidx.xr.runtime.math.Vector3,java.lang.Float> getAxisAngle();
+ method public androidx.xr.runtime.math.Vector3 getEulerAngles();
+ method public inline androidx.xr.runtime.math.Quaternion getInverse();
+ method public float getW();
+ method public float getX();
+ method public float getY();
+ method public float getZ();
+ method public static androidx.xr.runtime.math.Quaternion lerp(androidx.xr.runtime.math.Quaternion start, androidx.xr.runtime.math.Quaternion end, float ratio);
+ method public inline operator androidx.xr.runtime.math.Quaternion minus(androidx.xr.runtime.math.Quaternion other);
+ method public inline operator androidx.xr.runtime.math.Quaternion plus(androidx.xr.runtime.math.Quaternion other);
+ method public static androidx.xr.runtime.math.Quaternion slerp(androidx.xr.runtime.math.Quaternion start, androidx.xr.runtime.math.Quaternion end, float ratio);
+ method public inline operator androidx.xr.runtime.math.Quaternion times(androidx.xr.runtime.math.Quaternion other);
+ method public inline operator androidx.xr.runtime.math.Vector3 times(androidx.xr.runtime.math.Vector3 src);
+ method public operator androidx.xr.runtime.math.Quaternion times(float c);
+ method public androidx.xr.runtime.math.Quaternion toNormalized();
+ method public inline operator androidx.xr.runtime.math.Quaternion unaryMinus();
+ property public final kotlin.Pair<androidx.xr.runtime.math.Vector3,java.lang.Float> axisAngle;
+ property public final androidx.xr.runtime.math.Vector3 eulerAngles;
+ property public final inline androidx.xr.runtime.math.Quaternion inverse;
+ property public final float w;
+ property public final float x;
+ property public final float y;
+ property public final float z;
+ field public static final androidx.xr.runtime.math.Quaternion.Companion Companion;
+ field public static final androidx.xr.runtime.math.Quaternion Identity;
+ }
+
+ public static final class Quaternion.Companion {
+ method public float angle(androidx.xr.runtime.math.Quaternion start, androidx.xr.runtime.math.Quaternion end);
+ method public float dot(androidx.xr.runtime.math.Quaternion lhs, androidx.xr.runtime.math.Quaternion rhs);
+ method public androidx.xr.runtime.math.Quaternion fromAxisAngle(androidx.xr.runtime.math.Vector3 axis, float degrees);
+ method public androidx.xr.runtime.math.Quaternion fromEulerAngles(androidx.xr.runtime.math.Vector3 eulerAngles);
+ method public androidx.xr.runtime.math.Quaternion fromEulerAngles(float pitch, float yaw, float roll);
+ method public androidx.xr.runtime.math.Quaternion fromLookTowards(androidx.xr.runtime.math.Vector3 forward, androidx.xr.runtime.math.Vector3 up);
+ method public androidx.xr.runtime.math.Quaternion fromRotation(androidx.xr.runtime.math.Quaternion start, androidx.xr.runtime.math.Quaternion end);
+ method public androidx.xr.runtime.math.Quaternion fromRotation(androidx.xr.runtime.math.Vector3 start, androidx.xr.runtime.math.Vector3 end);
+ method public androidx.xr.runtime.math.Quaternion lerp(androidx.xr.runtime.math.Quaternion start, androidx.xr.runtime.math.Quaternion end, float ratio);
+ method public androidx.xr.runtime.math.Quaternion slerp(androidx.xr.runtime.math.Quaternion start, androidx.xr.runtime.math.Quaternion end, float ratio);
+ property public final androidx.xr.runtime.math.Quaternion Identity;
+ }
+
+ public final class Ray {
+ ctor public Ray();
+ ctor public Ray(androidx.xr.runtime.math.Ray other);
+ ctor public Ray(optional androidx.xr.runtime.math.Vector3 origin, optional androidx.xr.runtime.math.Vector3 direction);
+ method public androidx.xr.runtime.math.Vector3 getDirection();
+ method public androidx.xr.runtime.math.Vector3 getOrigin();
+ property public final androidx.xr.runtime.math.Vector3 direction;
+ property public final androidx.xr.runtime.math.Vector3 origin;
+ }
+
+ public final class Vector2 {
+ ctor public Vector2();
+ ctor public Vector2(androidx.xr.runtime.math.Vector2 other);
+ ctor public Vector2(optional float x);
+ ctor public Vector2(optional float x, optional float y);
+ method public static androidx.xr.runtime.math.Vector2 abs(androidx.xr.runtime.math.Vector2 vector);
+ method public static float angularDistance(androidx.xr.runtime.math.Vector2 vector1, androidx.xr.runtime.math.Vector2 vector2);
+ method public androidx.xr.runtime.math.Vector2 clamp(androidx.xr.runtime.math.Vector2 min, androidx.xr.runtime.math.Vector2 max);
+ method public inline androidx.xr.runtime.math.Vector2 copy();
+ method public inline androidx.xr.runtime.math.Vector2 copy(optional float x);
+ method public inline androidx.xr.runtime.math.Vector2 copy(optional float x, optional float y);
+ method public inline infix float cross(androidx.xr.runtime.math.Vector2 other);
+ method public static float distance(androidx.xr.runtime.math.Vector2 vector1, androidx.xr.runtime.math.Vector2 vector2);
+ method public inline operator androidx.xr.runtime.math.Vector2 div(androidx.xr.runtime.math.Vector2 other);
+ method public inline operator androidx.xr.runtime.math.Vector2 div(float c);
+ method public inline infix float dot(androidx.xr.runtime.math.Vector2 other);
+ method public inline float getLength();
+ method public inline float getLengthSquared();
+ method public float getX();
+ method public float getY();
+ method public static androidx.xr.runtime.math.Vector2 lerp(androidx.xr.runtime.math.Vector2 start, androidx.xr.runtime.math.Vector2 end, float ratio);
+ method public inline operator androidx.xr.runtime.math.Vector2 minus(androidx.xr.runtime.math.Vector2 other);
+ method public operator androidx.xr.runtime.math.Vector2 plus(androidx.xr.runtime.math.Vector2 other);
+ method public inline operator androidx.xr.runtime.math.Vector2 times(androidx.xr.runtime.math.Vector2 other);
+ method public inline operator androidx.xr.runtime.math.Vector2 times(float c);
+ method public androidx.xr.runtime.math.Vector2 toNormalized();
+ method public inline operator androidx.xr.runtime.math.Vector2 unaryMinus();
+ property public final inline float length;
+ property public final inline float lengthSquared;
+ property public final float x;
+ property public final float y;
+ field public static final androidx.xr.runtime.math.Vector2.Companion Companion;
+ field public static final androidx.xr.runtime.math.Vector2 Down;
+ field public static final androidx.xr.runtime.math.Vector2 Left;
+ field public static final androidx.xr.runtime.math.Vector2 One;
+ field public static final androidx.xr.runtime.math.Vector2 Right;
+ field public static final androidx.xr.runtime.math.Vector2 Up;
+ field public static final androidx.xr.runtime.math.Vector2 Zero;
+ }
+
+ public static final class Vector2.Companion {
+ method public androidx.xr.runtime.math.Vector2 abs(androidx.xr.runtime.math.Vector2 vector);
+ method public float angularDistance(androidx.xr.runtime.math.Vector2 vector1, androidx.xr.runtime.math.Vector2 vector2);
+ method public float distance(androidx.xr.runtime.math.Vector2 vector1, androidx.xr.runtime.math.Vector2 vector2);
+ method public androidx.xr.runtime.math.Vector2 lerp(androidx.xr.runtime.math.Vector2 start, androidx.xr.runtime.math.Vector2 end, float ratio);
+ property public final androidx.xr.runtime.math.Vector2 Down;
+ property public final androidx.xr.runtime.math.Vector2 Left;
+ property public final androidx.xr.runtime.math.Vector2 One;
+ property public final androidx.xr.runtime.math.Vector2 Right;
+ property public final androidx.xr.runtime.math.Vector2 Up;
+ property public final androidx.xr.runtime.math.Vector2 Zero;
+ }
+
+ public final class Vector3 {
+ ctor public Vector3();
+ ctor public Vector3(androidx.xr.runtime.math.Vector3 other);
+ ctor public Vector3(optional float x);
+ ctor public Vector3(optional float x, optional float y);
+ ctor public Vector3(optional float x, optional float y, optional float z);
+ method public static androidx.xr.runtime.math.Vector3 abs(androidx.xr.runtime.math.Vector3 vector);
+ method public static float angleBetween(androidx.xr.runtime.math.Vector3 vector1, androidx.xr.runtime.math.Vector3 vector2);
+ method public androidx.xr.runtime.math.Vector3 clamp(androidx.xr.runtime.math.Vector3 min, androidx.xr.runtime.math.Vector3 max);
+ method public androidx.xr.runtime.math.Vector3 copy();
+ method public androidx.xr.runtime.math.Vector3 copy(optional float x);
+ method public androidx.xr.runtime.math.Vector3 copy(optional float x, optional float y);
+ method public androidx.xr.runtime.math.Vector3 copy(optional float x, optional float y, optional float z);
+ method public infix androidx.xr.runtime.math.Vector3 cross(androidx.xr.runtime.math.Vector3 other);
+ method public static float distance(androidx.xr.runtime.math.Vector3 vector1, androidx.xr.runtime.math.Vector3 vector2);
+ method public operator androidx.xr.runtime.math.Vector3 div(androidx.xr.runtime.math.Vector3 other);
+ method public operator androidx.xr.runtime.math.Vector3 div(float c);
+ method public infix float dot(androidx.xr.runtime.math.Vector3 other);
+ method public static androidx.xr.runtime.math.Vector3 fromValue(float value);
+ method public inline float getLength();
+ method public inline float getLengthSquared();
+ method public float getX();
+ method public float getY();
+ method public float getZ();
+ method public static androidx.xr.runtime.math.Vector3 lerp(androidx.xr.runtime.math.Vector3 start, androidx.xr.runtime.math.Vector3 end, float ratio);
+ method public static androidx.xr.runtime.math.Vector3 max(androidx.xr.runtime.math.Vector3 a, androidx.xr.runtime.math.Vector3 b);
+ method public static androidx.xr.runtime.math.Vector3 min(androidx.xr.runtime.math.Vector3 a, androidx.xr.runtime.math.Vector3 b);
+ method public operator androidx.xr.runtime.math.Vector3 minus(androidx.xr.runtime.math.Vector3 other);
+ method public operator androidx.xr.runtime.math.Vector3 plus(androidx.xr.runtime.math.Vector3 other);
+ method public static androidx.xr.runtime.math.Vector3 projectOnPlane(androidx.xr.runtime.math.Vector3 vector, androidx.xr.runtime.math.Vector3 planeNormal);
+ method public operator androidx.xr.runtime.math.Vector3 times(androidx.xr.runtime.math.Vector3 other);
+ method public operator androidx.xr.runtime.math.Vector3 times(float c);
+ method public androidx.xr.runtime.math.Vector3 toNormalized();
+ method public operator androidx.xr.runtime.math.Vector3 unaryMinus();
+ property public final inline float length;
+ property public final inline float lengthSquared;
+ property public final float x;
+ property public final float y;
+ property public final float z;
+ field public static final androidx.xr.runtime.math.Vector3 Backward;
+ field public static final androidx.xr.runtime.math.Vector3.Companion Companion;
+ field public static final androidx.xr.runtime.math.Vector3 Down;
+ field public static final androidx.xr.runtime.math.Vector3 Forward;
+ field public static final androidx.xr.runtime.math.Vector3 Left;
+ field public static final androidx.xr.runtime.math.Vector3 One;
+ field public static final androidx.xr.runtime.math.Vector3 Right;
+ field public static final androidx.xr.runtime.math.Vector3 Up;
+ field public static final androidx.xr.runtime.math.Vector3 Zero;
+ }
+
+ public static final class Vector3.Companion {
+ method public androidx.xr.runtime.math.Vector3 abs(androidx.xr.runtime.math.Vector3 vector);
+ method public float angleBetween(androidx.xr.runtime.math.Vector3 vector1, androidx.xr.runtime.math.Vector3 vector2);
+ method public float distance(androidx.xr.runtime.math.Vector3 vector1, androidx.xr.runtime.math.Vector3 vector2);
+ method public androidx.xr.runtime.math.Vector3 fromValue(float value);
+ method public androidx.xr.runtime.math.Vector3 lerp(androidx.xr.runtime.math.Vector3 start, androidx.xr.runtime.math.Vector3 end, float ratio);
+ method public androidx.xr.runtime.math.Vector3 max(androidx.xr.runtime.math.Vector3 a, androidx.xr.runtime.math.Vector3 b);
+ method public androidx.xr.runtime.math.Vector3 min(androidx.xr.runtime.math.Vector3 a, androidx.xr.runtime.math.Vector3 b);
+ method public androidx.xr.runtime.math.Vector3 projectOnPlane(androidx.xr.runtime.math.Vector3 vector, androidx.xr.runtime.math.Vector3 planeNormal);
+ property public final androidx.xr.runtime.math.Vector3 Backward;
+ property public final androidx.xr.runtime.math.Vector3 Down;
+ property public final androidx.xr.runtime.math.Vector3 Forward;
+ property public final androidx.xr.runtime.math.Vector3 Left;
+ property public final androidx.xr.runtime.math.Vector3 One;
+ property public final androidx.xr.runtime.math.Vector3 Right;
+ property public final androidx.xr.runtime.math.Vector3 Up;
+ property public final androidx.xr.runtime.math.Vector3 Zero;
+ }
+
+}
+
diff --git a/xr/runtime/runtime/build.gradle b/xr/runtime/runtime/build.gradle
new file mode 100644
index 0000000..f670fa8
--- /dev/null
+++ b/xr/runtime/runtime/build.gradle
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+
+import androidx.build.KotlinTarget
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("kotlin-android")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ api(libs.kotlinCoroutinesCore)
+ api("androidx.annotation:annotation:1.8.1")
+
+ implementation(libs.guavaListenableFuture)
+ implementation(libs.kotlinCoroutinesRx3)
+ implementation(libs.kotlinCoroutinesGuava)
+ implementation("androidx.core:core:1.1.0")
+
+ testImplementation(libs.kotlinCoroutinesTest)
+ testImplementation(libs.junit)
+ testImplementation(libs.robolectric)
+ testImplementation(libs.testExtJunit)
+ testImplementation(libs.testRunner)
+ testImplementation(libs.truth)
+ testImplementation(project(":kruth:kruth"))
+ testImplementation(project(":xr:runtime:runtime-testing"))
+ testImplementation("androidx.appcompat:appcompat:1.2.0")
+ testImplementation(libs.testRules)
+}
+
+android {
+ namespace "androidx.xr.runtime"
+ testOptions.unitTests.includeAndroidResources = true
+ defaultConfig {
+ consumerProguardFiles 'proguard-rules.pro'
+ }
+}
+
+androidx {
+ name = "XR Runtime"
+ type = LibraryType.PUBLISHED_LIBRARY
+ inceptionYear = "2024"
+ description = "Runtime interfaces and helper libraries for the androidx.xr namespace."
+ kotlinTarget = KotlinTarget.KOTLIN_1_9
+ // TODO: b/326456246
+ optOutJSpecify = true
+}
diff --git a/xr/runtime/runtime/proguard-rules.pro b/xr/runtime/runtime/proguard-rules.pro
new file mode 100644
index 0000000..af2c756
--- /dev/null
+++ b/xr/runtime/runtime/proguard-rules.pro
@@ -0,0 +1,9 @@
+# Prevent the Internal and Math classes from being obfuscated as they are created from native code.
+-keep class androidx.xr.runtime.internal.** { *; }
+-keep class androidx.xr.runtime.internal.**$* { *; }
+-keep class androidx.xr.runtime.math.** { *; }
+-keep class androidx.xr.runtime.math.**$* { *; }
+-keep class * extends androidx.xr.runtime.internal.** { *; }
+-keep class * extends androidx.xr.runtime.internal.**$* { *; }
+-keep class * extends androidx.xr.runtime.math.** { *; }
+-keep class * extends androidx.xr.runtime.math.**$* { *; }
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/CoreState.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/CoreState.kt
new file mode 100644
index 0000000..e34fb42
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/CoreState.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime
+
+import androidx.annotation.RestrictTo
+import kotlin.time.ComparableTimeMark
+
+/**
+ * Represents the state of the XR system at a specific point in time.
+ *
+ * @property timeMark at which the state was computed.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public data class CoreState(val timeMark: ComparableTimeMark) {}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/CoroutineDispatchers.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/CoroutineDispatchers.kt
new file mode 100644
index 0000000..f5c419e
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/CoroutineDispatchers.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime
+
+import java.util.concurrent.Executors
+import kotlinx.coroutines.asCoroutineDispatcher
+
+/** Provides the [CoroutineDispatcher] objects for all coroutines in Jetpack XR Runtime. */
+internal object CoroutineDispatchers {
+
+ /** A [CoroutineDispatcher] for lightweight tasks that are small and non-blocking. */
+ val Lightweight = Executors.newCachedThreadPool().asCoroutineDispatcher()
+}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/Session.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/Session.kt
new file mode 100644
index 0000000..5537e0a
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/Session.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime
+
+import android.app.Activity
+import android.content.pm.PackageManager
+import androidx.annotation.RestrictTo
+import androidx.core.content.ContextCompat
+import androidx.xr.runtime.internal.Runtime
+import androidx.xr.runtime.internal.RuntimeFactory
+import java.util.ServiceLoader
+import kotlin.time.TimeSource
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+/**
+ * A session is the main entrypoint to features provided by ARCore for Jetpack XR. It manages the
+ * system's state and its lifecycle, and contains the state of objects tracked by ARCore for Jetpack
+ * XR.
+ *
+ * This class owns a significant amount of native heap memory. Apps using a `Session` consider its
+ * lifecycle to ensure that native resources are released when the session is no longer needed. If
+ * your activity is a single XR-enabled activity, it is recommended to call the `Session` object's
+ * lifecycle methods from the activity's lifecycle methods using a
+ * [lifecycle-aware component](https://developer.android.com/topic/libraries/architecture/lifecycle).
+ * See [create], [resume], [pause], and [destroy] for more details.
+ */
+@Suppress("NotCloseable")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class Session
+internal constructor(
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public val runtime: Runtime,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public val stateExtenders: List<StateExtender>,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public val coroutineScope: CoroutineScope,
+ private val activity: Activity,
+) {
+ public companion object {
+ /**
+ * Creates a new [Session].
+ *
+ * Creating a session requires the `android.permission.SCENE_UNDERSTANDING` permission to be
+ * granted.
+ *
+ * @param activity the [Activity] that owns the session.
+ * @param coroutineDispatcher the [CoroutineDispatcher] that will be used to handle the
+ * session's coroutines.
+ * @return the result of the operation. Can be [SessionCreateSuccess], which contains the
+ * newly created session, or [SessionCreatePermissionsNotGranted] if the required
+ * permissions haven't been granted.
+ */
+ @JvmOverloads
+ @JvmStatic
+ public fun create(
+ activity: Activity,
+ coroutineDispatcher: CoroutineDispatcher = CoroutineDispatchers.Lightweight,
+ ): SessionCreateResult {
+ val missingPermissions = activity.selectMissing(SESSION_PERMISSIONS)
+ if (missingPermissions.isNotEmpty()) {
+ return SessionCreatePermissionsNotGranted(missingPermissions)
+ }
+
+ val runtimeFactory: RuntimeFactory =
+ ServiceLoader.load(RuntimeFactory::class.java).iterator().next()
+ val runtime = runtimeFactory.createRuntime(activity)
+ runtime.lifecycleManager.create()
+
+ val stateExtenders = ServiceLoader.load(StateExtender::class.java).toList()
+ for (stateExtender in stateExtenders) {
+ stateExtender.initialize(runtime)
+ }
+ val session =
+ Session(runtime, stateExtenders, CoroutineScope(coroutineDispatcher), activity)
+ return SessionCreateSuccess(session)
+ }
+
+ internal val SESSION_PERMISSIONS: List<String> =
+ listOf("android.permission.SCENE_UNDERSTANDING")
+ }
+
+ /** The state of the runtime. */
+ private enum class RuntimeState {
+ INITIALIZED,
+ RESUMED,
+ PAUSED,
+ STOPPED,
+ }
+
+ private val _state = MutableStateFlow<CoreState>(CoreState(TimeSource.Monotonic.markNow()))
+
+ /** A [StateFlow] of the current state. */
+ public val state: StateFlow<CoreState> = _state.asStateFlow()
+
+ private var runtimeState: RuntimeState = RuntimeState.INITIALIZED
+ private var updateJob: Job? = null
+
+ /**
+ * Sets or changes the configuration to use.
+ *
+ * @return the result of the operation. This will be a [SessionConfigureSuccess] if the
+ * configuration was successful, or a [SessionConfigureConfigurationNotSupported] if the
+ * [SessionConfiguration] is not supported.
+ * @throws IllegalStateException if the session has been destroyed.
+ */
+ public fun configure(): SessionConfigureResult {
+ check(runtimeState != RuntimeState.STOPPED) { "Session has been destroyed." }
+ runtime.lifecycleManager.configure()
+ return SessionConfigureSuccess()
+ }
+
+ /**
+ * Starts or resumes the session.
+ *
+ * Resuming a session requires the `android.permission.SCENE_UNDERSTANDING` to be granted.
+ *
+ * @return the result of the operation. Can be [SessionResumeSuccess] if the session was
+ * successfully resumed, or [SessionResumePermissionsNotGranted] if the required permissions
+ * haven't been granted.
+ * @throws IllegalStateException if the session has been destroyed.
+ */
+ public fun resume(): SessionResumeResult {
+ check(runtimeState != RuntimeState.STOPPED) { "Session has been destroyed." }
+
+ if (runtimeState != RuntimeState.RESUMED) {
+ val missingPermissions = activity.selectMissing(SESSION_PERMISSIONS)
+ if (missingPermissions.isNotEmpty()) {
+ return SessionResumePermissionsNotGranted(missingPermissions)
+ }
+
+ runtime.lifecycleManager.resume()
+ runtimeState = RuntimeState.RESUMED
+ updateJob = coroutineScope.launch { updateLoop() }
+ }
+
+ return SessionResumeSuccess()
+ }
+
+ /**
+ * Pauses execution of the session. Objects tracked by the session will not receive updates. The
+ * system state will be retained in memory.
+ *
+ * Calling this method on an inactive session is a no-op.
+ *
+ * @throws IllegalStateException if the session has been destroyed.
+ */
+ public fun pause() {
+ check(runtimeState != RuntimeState.STOPPED) { "Session has been destroyed." }
+
+ if (runtimeState == RuntimeState.RESUMED) {
+ runtime.lifecycleManager.pause()
+ runtimeState = RuntimeState.PAUSED
+ updateJob?.cancel()
+ updateJob = null
+ }
+ }
+
+ /**
+ * Destroys the session, releasing any resources acquired by the session. Objects tracked by the
+ * system will not receive updates.
+ *
+ * Calling this method on a destroyed session is a no-op. Additionally, calling this method on
+ * an active session will first call [pause].
+ */
+ public fun destroy() {
+ if (runtimeState == RuntimeState.STOPPED) {
+ return
+ } else if (runtimeState == RuntimeState.RESUMED) {
+ pause()
+ }
+
+ runtime.lifecycleManager.stop()
+ coroutineScope.cancel()
+ runtimeState = RuntimeState.STOPPED
+ }
+
+ private suspend fun updateLoop() {
+ while (runtimeState == RuntimeState.RESUMED) {
+ update()
+ }
+ }
+
+ /** Produces the latest [CoreState] so it can be emitted downstream. */
+ private suspend fun update() {
+ val timeMark = runtime.lifecycleManager.update()
+ val state = CoreState(timeMark)
+
+ for (stateExtender in stateExtenders) {
+ stateExtender.extend(state)
+ }
+
+ _state.emit(state)
+ }
+}
+
+private fun Activity.selectMissing(permissions: List<String>): List<String> =
+ permissions.filter { permission -> !hasPermission(permission) }
+
+private fun Activity.hasPermission(permission: String): Boolean =
+ ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/SessionResults.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/SessionResults.kt
new file mode 100644
index 0000000..c01afc71
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/SessionResults.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime
+
+import androidx.annotation.RestrictTo
+
+/** Result of a [Session.create] call. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public sealed class SessionCreateResult
+
+/**
+ * Result of a successful [Session.create] call.
+ *
+ * @property session the [Session] that was created.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SessionCreateSuccess(public val session: Session) : SessionCreateResult()
+
+/**
+ * Result of an unsuccessful [Session.create] call. The session was not created due to the required
+ * [permissions] not being granted.
+ *
+ * @property permissions the permissions that were not granted.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SessionCreatePermissionsNotGranted(public val permissions: List<String>) :
+ SessionCreateResult()
+
+/** Result of a [Session.configure] call. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public sealed class SessionConfigureResult
+
+/** Result of a successful [Session.configure] call. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SessionConfigureSuccess() : SessionConfigureResult()
+
+/**
+ * Result of an unsuccessful [Session.configure] call. The session was not configured due to the
+ * given [SessionConfig] not being supported.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SessionConfigureConfigurationNotSupported() : SessionConfigureResult()
+
+/** Result of a [Session.resume] call. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public sealed class SessionResumeResult
+
+/** Result of a successful [Session.resume] call. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SessionResumeSuccess() : SessionResumeResult()
+
+/**
+ * Result of an unsuccessful [Session.resume] call. The session was not resumed due to the required
+ * [permissions] not being granted.
+ *
+ * @property permissions the permissions that were not granted.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SessionResumePermissionsNotGranted(public val permissions: List<String>) :
+ SessionResumeResult()
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/StateExtender.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/StateExtender.kt
new file mode 100644
index 0000000..f36fe20
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/StateExtender.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.internal.Runtime
+
+/** Class in charge of extending [CoreState] with a sub-state by using the [Runtime]. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface StateExtender {
+ /** Initializes the [StateExtender]. */
+ public fun initialize(runtime: Runtime)
+
+ /** Extends [CoreState] with a package-specific sub-state. */
+ public suspend fun extend(coreState: CoreState)
+}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/Anchor.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/Anchor.kt
new file mode 100644
index 0000000..7528bd0
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/Anchor.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.internal
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.math.Pose
+import java.util.UUID
+
+/** Describes a fixed location and orientation in the real world. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface Anchor {
+
+ /** Describes the state of persistence for an [Anchor]. */
+ public class PersistenceState private constructor(private val value: Int) {
+ public companion object {
+ /** The anchor has not been requested to be persisted. */
+ @JvmField public val NotPersisted: PersistenceState = PersistenceState(0)
+
+ /** The anchor has been requested to be persisted but the operation is still pending. */
+ @JvmField public val Pending: PersistenceState = PersistenceState(1)
+
+ /** The anchor has been persisted. */
+ @JvmField public val Persisted: PersistenceState = PersistenceState(2)
+ }
+ }
+
+ /** The location of the anchor in the world coordinate space. */
+ public val pose: Pose
+
+ /** The current state of the pose of this anchor. */
+ public val trackingState: TrackingState
+
+ /** The [PersistenceState] for this anchor. */
+ public val persistenceState: PersistenceState
+
+ /** The [UUID] that identifies this Anchor if it is persisted. */
+ public val uuid: UUID?
+
+ /**
+ * Detaches this anchor from its [Trackable]. After detaching, the anchor will not be updated
+ * anymore and cannot be reattached to another trackable.
+ */
+ public fun detach()
+
+ /**
+ * Sends a request to persist this anchor. The value of [persistenceState] will be updated based
+ * on the progress of this operation. The value of [uuid] will be set if the operation is
+ * successful.
+ */
+ public fun persist()
+}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/HitResult.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/HitResult.kt
new file mode 100644
index 0000000..2c8d045
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/HitResult.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.internal
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.math.Pose
+
+/**
+ * Defines an intersection between a ray and estimated real-world geometry.
+ *
+ * @property distance from the camera to the hit location, in meters.
+ * @property hitPose of the intersection between a ray and detected real-world geometry.
+ * @property trackable that was hit.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class HitResult(
+ public val distance: Float,
+ public val hitPose: Pose,
+ public val trackable: Trackable,
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is HitResult) return false
+
+ if (distance != other.distance) return false
+ if (hitPose != other.hitPose) return false
+ if (trackable != other.trackable) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = distance.hashCode()
+ result = 31 * result + hitPose.hashCode()
+ result = 31 * result + trackable.hashCode()
+ return result
+ }
+}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/LifecycleManager.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/LifecycleManager.kt
new file mode 100644
index 0000000..5706f02
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/LifecycleManager.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.internal
+
+import androidx.annotation.RestrictTo
+import kotlin.time.ComparableTimeMark
+
+/** Describes the lifecycle a runtime implementation. */
+@Suppress("NotCloseable")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface LifecycleManager {
+ /**
+ * Executes the [Runtime] initialization logic. It is necessary to call [resume] after calling
+ * this method to start the runtime's execution logic.
+ */
+ public fun create()
+
+ /**
+ * Sets or changes the configuration to use, which will affect the availability of properties or
+ * features in other managers. It is necessary to have called [create] before calling this
+ * method.
+ */
+ public fun configure()
+
+ /**
+ * Resumes execution from a paused or init state. It is necessary to have called [create] before
+ * calling this method.
+ */
+ public fun resume()
+
+ /**
+ * Updates the state of the system. The call is blocking and will return once the underlying
+ * implementation has been updated or a platform-specific timeout has been reached. This method
+ * can only be called when the runtime is resumed.
+ *
+ * @return the timemark of the latest state. This value is to be used for comparison with other
+ * timemarks and not to be used for absolute time calculations.
+ */
+ public suspend fun update(): ComparableTimeMark
+
+ /** Pauses execution while retaining the state in memory. */
+ public fun pause()
+
+ /**
+ * Stops the execution and releases all resources. It is not valid to call any other method
+ * after calling [stop]. The runtime must not be resumed when this method is called.
+ */
+ public fun stop()
+}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/PerceptionManager.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/PerceptionManager.kt
new file mode 100644
index 0000000..a3a746d
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/PerceptionManager.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.internal
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Ray
+import java.util.UUID
+import kotlin.collections.Collection
+import kotlin.collections.List
+
+/**
+ * Describes the perception functionality that is required from a [Runtime] implementation. It is
+ * expected that these functions are only valid while the [Runtime] is in a resumed state.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface PerceptionManager {
+ /** Defines a tracked location in the physical world. */
+ public fun createAnchor(pose: Pose): Anchor
+
+ /** Performs a ray cast in the direction of the given [ray] in the latest camera view. */
+ public fun hitTest(ray: Ray): List<HitResult>
+
+ /** Retrieves all the [UUID] instances from [Anchor] objects that have been persisted. */
+ public fun getPersistedAnchorUuids(): List<UUID>
+
+ /** Loads an [Anchor] from local storage. */
+ public fun loadAnchor(uuid: UUID): Anchor
+
+ /** Loads an [Anchor] from a native pointer. */
+ // TODO(b/373711152) : Remove this method once the Jetpack XR Runtime API migration is done.
+ public fun loadAnchorFromNativePointer(nativePointer: Long): Anchor
+
+ /** Deletes a persisted [Anchor] from local storage. */
+ public fun unpersistAnchor(uuid: UUID)
+
+ /** Returns the list of all known trackables. */
+ public val trackables: Collection<Trackable>
+}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/Plane.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/Plane.kt
new file mode 100644
index 0000000..5ca34aa
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/Plane.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.internal
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Vector2
+
+/** Describes the current best knowledge of a real-world planar surface. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface Plane : Trackable {
+ /** The [Type] of the plane */
+ public val type: Type
+
+ /* The [Label] of the plane */
+ public val label: Label
+
+ /** The center of the detected plane. */
+ public val centerPose: Pose
+
+ /** The dimensions of the detected plane. */
+ public val extents: Vector2
+
+ /** If this plane has been subsumed, returns the plane this plane was merged into. */
+ public val subsumedBy: Plane?
+
+ /**
+ * Returns the 2D vertices (three or more) of a convex polygon approximating the detected plane.
+ */
+ public val vertices: List<Vector2>
+
+ /** Simple summary of the normal vector of a plane, for filtering purposes. */
+ public class Type private constructor(private val name: Int) {
+ public companion object {
+ /** A horizontal plane facing upward (e.g. floor or tabletop). */
+ @JvmField public val HorizontalUpwardFacing: Type = Type(0)
+
+ /** A horizontal plane facing downward (e.g. a ceiling). */
+ @JvmField public val HorizontalDownwardFacing: Type = Type(1)
+
+ /** A vertical plane (e.g. a wall). */
+ @JvmField public val Vertical: Type = Type(2)
+ }
+ }
+
+ /** A semantic description of a [Plane]. */
+ public class Label private constructor(private val name: Int) {
+ public companion object {
+ /** A plane of unknown type. */
+ @JvmField public val Unknown: Label = Label(0)
+
+ /** A plane that represents a wall. */
+ @JvmField public val Wall: Label = Label(1)
+
+ /** A plane that represents a floor. */
+ @JvmField public val Floor: Label = Label(2)
+
+ /** A plane that represents a ceiling. */
+ @JvmField public val Ceiling: Label = Label(3)
+
+ /** A plane that represents a table. */
+ @JvmField public val Table: Label = Label(4)
+ }
+ }
+}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/Runtime.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/Runtime.kt
new file mode 100644
index 0000000..ee82ece
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/Runtime.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.internal
+
+import androidx.annotation.RestrictTo
+
+/** Set of behaviors that collectively define a runtime. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface Runtime {
+ /** Mandatory lifecycle runtime behavior. */
+ public val lifecycleManager: LifecycleManager
+
+ /** Mandatory perception runtime behavior. */
+ public val perceptionManager: PerceptionManager
+}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/RuntimeFactory.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/RuntimeFactory.kt
new file mode 100644
index 0000000..9f8ff48
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/RuntimeFactory.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.internal
+
+import android.app.Activity
+import androidx.annotation.RestrictTo
+
+/** Factory for creating instances of Runtime. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface RuntimeFactory {
+ /** Creates a [Runtime] instance */
+ public fun createRuntime(activity: Activity): Runtime
+}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/Space.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/Space.kt
new file mode 100644
index 0000000..a0af994
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/Space.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.internal
+
+import androidx.annotation.RestrictTo
+
+/**
+ * Describes a well-known coordinate system that is available for [anchors][Anchor] to be attached
+ * to.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface Space : Trackable {
+ /** Describes the origin and extents of the well-known coordinate system. */
+ public class Type private constructor(private val name: Int) {
+ public companion object {
+ /**
+ * A world-locked origin useful when an application needs to render seated-scale content
+ * that is not positioned relative to the physical floor.
+ */
+ @JvmField public val Local: Type = Type(0)
+
+ /**
+ * Similar to [LOCAL] but with a different height/Y coordinate. Matches [STAGE] but with
+ * potentially reduced bounds.
+ */
+ @JvmField public val LocalFloor: Type = Type(1)
+ }
+ }
+}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/Trackable.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/Trackable.kt
new file mode 100644
index 0000000..1a6c5f1
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/Trackable.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.internal
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.math.Pose
+
+/** A trackable is something can be tracked in space and that an [Anchor] can be attached to. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface Trackable {
+ /**
+ * Creates an [Anchor] that is attached to this trackable, using the given initial [pose] in the
+ * world coordinate space.
+ */
+ public fun createAnchor(pose: Pose): Anchor
+
+ /** The [TrackingState] of this trackable. */
+ public val trackingState: TrackingState
+}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/TrackingState.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/TrackingState.kt
new file mode 100644
index 0000000..760d8c3
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/TrackingState.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.internal
+
+import androidx.annotation.RestrictTo
+
+/** Describes the state of the tracking performed. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class TrackingState private constructor(private val value: Int) {
+ public companion object {
+ /** The trackable is currently tracked and its pose is current. */
+ @JvmField public val Tracking: TrackingState = TrackingState(0)
+
+ /** Tracking has been paused for this instance but may be resumed in the future. */
+ @JvmField public val Paused: TrackingState = TrackingState(1)
+
+ /** Tracking has stopped for this instance and will never be resumed in the future. */
+ @JvmField public val Stopped: TrackingState = TrackingState(2)
+ }
+
+ public override fun toString(): String =
+ when (this) {
+ Tracking -> "Tracking"
+ Paused -> "Paused"
+ Stopped -> "Stopped"
+ else -> "Unknown"
+ }
+}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/java/Coroutines.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/java/Coroutines.kt
new file mode 100644
index 0000000..a13981a
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/java/Coroutines.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+@file:JvmName("Coroutines")
+
+package androidx.xr.runtime.java
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.Session
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.guava.future
+
+/**
+ * Converts a coroutine created within the [session] to a [ListenableFuture].
+ *
+ * The returned [ListenableFuture] will be automatically cancelled when the [session] is destroyed.
+ *
+ * @param session the [Session] that originated the [coroutine].
+ * @param coroutine the coroutine to convert to a [ListenableFuture].
+ */
+@Suppress("AsyncSuffixFuture")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun <T> toFuture(
+ session: Session,
+ coroutine: suspend CoroutineScope.() -> T,
+): ListenableFuture<T> =
+ session.coroutineScope.future(
+ session.coroutineScope.coroutineContext,
+ CoroutineStart.DEFAULT,
+ coroutine,
+ )
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/java/Flows.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/java/Flows.kt
new file mode 100644
index 0000000..d8990c4
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/java/Flows.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+@file:JvmName("Flows")
+
+package androidx.xr.runtime.java
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.Session
+import io.reactivex.rxjava3.core.Observable
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.rx3.asObservable
+
+/**
+ * Converts a [flow] created within the [session] to an [Observable].
+ *
+ * The returned [Observable] will be given the session's [CoroutineContext].
+ *
+ * @param session the [Session] that originated the [flow].
+ * @param flow the [Flow] to convert to an [Observable].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun <T : Any> toObservable(session: Session, flow: Flow<T>): Observable<T> =
+ flow.asObservable(session.coroutineScope.coroutineContext)
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/FloatExt.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/FloatExt.kt
new file mode 100644
index 0000000..0a238bd
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/FloatExt.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+@file:JvmName("MathHelper")
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.xr.runtime.math
+
+import kotlin.math.PI
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.sqrt
+
+private const val DEGREES_PER_RADIAN = 180.0f / PI.toFloat()
+private const val RADIANS_PER_DEGREE = PI.toFloat() / 180.0f
+
+/** Calculates the reciprocal square root 1/sqrt(x) with a maximum error up to threshold. */
+internal inline fun rsqrt(x: Float): Float {
+ return 1 / sqrt(x)
+}
+
+/**
+ * Clamps a value.
+ *
+ * @param x the value to clamp.
+ * @param min the minimum value.
+ * @param max the maximum value.
+ */
+public fun clamp(x: Float, min: Float, max: Float): Float {
+ val result = min(max, max(min, x))
+ return result
+}
+
+/**
+ * Linearly interpolates between two values.
+ *
+ * @param a the start value.
+ * @param b the end value.
+ * @param t the ratio between the two floats.
+ * @return the interpolated value between [a] and [b].
+ */
+public fun lerp(a: Float, b: Float, t: Float): Float {
+ return a * (1.0f - t) + b * t
+}
+
+/** Converts [angleInRadians] from radians to degrees. */
+public fun toDegrees(angleInRadians: Float): Float = angleInRadians * DEGREES_PER_RADIAN
+
+/** Converts [angleInDegrees] from degrees to radians. */
+public fun toRadians(angleInDegrees: Float): Float = angleInDegrees * RADIANS_PER_DEGREE
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Matrix4.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Matrix4.kt
new file mode 100644
index 0000000..635bd04
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Matrix4.kt
@@ -0,0 +1,384 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.math
+
+import kotlin.math.sign
+import kotlin.math.sqrt
+
+/**
+ * An immutable 4x4 matrix that represents translation, scale, and rotation. The matrix is column
+ * major and right handed. The indexes of [dataToCopy] represent the following matrix layout:
+ * ```
+ * [0, 4, 8, 12]
+ * [1, 5, 9, 13]
+ * [2, 6, 10, 14]
+ * [3, 7, 11, 15]
+ * ```
+ *
+ * @param dataToCopy the array with 16 elements that will be copied over.
+ */
+public class Matrix4(dataToCopy: FloatArray) {
+ init {
+ // TODO: Consider using contracts to avoid the exception being inlined.
+ require(dataToCopy.size == 16) {
+ "Input array must have exactly 16 elements for a 4x4 matrix"
+ }
+ }
+
+ /** Returns an array of the components of this matrix. */
+ public val data: FloatArray = dataToCopy.copyOf()
+
+ /** Returns a matrix that performs the opposite transformation. */
+ public val inverse: Matrix4 by lazy(LazyThreadSafetyMode.NONE) { inverse() }
+
+ /** Returns a matrix that is the transpose of this matrix. */
+ public val transpose: Matrix4 by lazy(LazyThreadSafetyMode.NONE) { transpose() }
+
+ /** Returns the translation component of this matrix. */
+ public val translation: Vector3 by
+ lazy(LazyThreadSafetyMode.NONE) { Vector3(data[12], data[13], data[14]) }
+
+ /** Returns the scale component of this matrix. */
+ public val scale: Vector3 by lazy(LazyThreadSafetyMode.NONE) { scale() }
+
+ /** Returns the rotation component of this matrix. */
+ public val rotation: Quaternion by lazy(LazyThreadSafetyMode.NONE) { rotation() }
+
+ /** Returns the pose (i.e. rotation and translation) of this matrix. */
+ public val pose: Pose by lazy(LazyThreadSafetyMode.NONE) { Pose(translation, rotation) }
+
+ /**
+ * Returns true if this matrix is a valid transformation matrix that can be decomposed into
+ * translation, rotation and scale using determinant properties.
+ */
+ public val isTrs: Boolean by lazy(LazyThreadSafetyMode.NONE) { determinant() != 0.0f }
+
+ /** Creates a new matrix with a deep copy of the data from the [other] [Matrix4]. */
+ public constructor(other: Matrix4) : this(other.data.copyOf())
+
+ /**
+ * Returns a new matrix with the matrix multiplication product of this matrix and the [other]
+ * matrix.
+ */
+ public operator fun times(other: Matrix4): Matrix4 {
+ val result = Matrix4.Zero
+ android.opengl.Matrix.multiplyMM(
+ /* result= */ result.data,
+ /* resultOffset= */ 0,
+ /* lhs= */ this.data,
+ /* lhsOffset= */ 0,
+ /* rhs= */ other.data,
+ /* rhsOffset= */ 0,
+ )
+
+ return Matrix4(result.data)
+ }
+
+ private fun inverse(): Matrix4 {
+ val result = Matrix4.Zero
+ android.opengl.Matrix.invertM(
+ /* mInv= */ result.data,
+ /* mInvOffset= */ 0,
+ /* m= */ this.data,
+ /* mOffset= */ 0,
+ )
+
+ return Matrix4(result.data)
+ }
+
+ private fun transpose(): Matrix4 {
+ val result = Matrix4.Zero
+ android.opengl.Matrix.transposeM(
+ /* mTrans= */ result.data,
+ /* mTransOffset= */ 0,
+ /* m= */ this.data,
+ /* mOffset= */ 0,
+ )
+
+ return Matrix4(result.data)
+ }
+
+ private fun rotation(): Quaternion {
+ val m00 = data[0]
+ val m01 = data[4]
+ val m02 = data[8]
+ val m10 = data[1]
+ val m11 = data[5]
+ val m12 = data[9]
+ val m20 = data[2]
+ val m21 = data[6]
+ val m22 = data[10]
+
+ val trace = m00 + m11 + m22 + 1.0f
+
+ return if (trace > 0) {
+ val s = 0.5f / sqrt(trace)
+ Quaternion((m21 - m12) * s, (m02 - m20) * s, (m10 - m01) * s, 0.25f / s)
+ } else if ((m00 > m11) && (m00 > m22)) {
+ val s = 2.0f * sqrt(1.0f + m00 - m11 - m22)
+ Quaternion(0.25f * s, (m01 + m10) / s, (m02 + m20) / s, (m21 - m12) / s)
+ } else if (m11 > m22) {
+ val s = 2.0f * sqrt(1.0f + m11 - m00 - m22)
+ Quaternion((m01 + m10) / s, 0.25f * s, (m12 + m21) / s, (m02 - m20) / s)
+ } else {
+ val s = 2.0f * sqrt(1.0f + m22 - m00 - m11)
+ Quaternion((m02 + m20) / s, (m12 + m21) / s, 0.25f * s, (m10 - m01) / s)
+ }
+ }
+
+ private fun scale(): Vector3 {
+ // TODO: b/367780918 - Investigate why scale can have negative values when inputs were
+ // positive.
+ // We shouldn't use sign() directly because we don't want it to ever return 0
+ val signX = if (data[0] == 0.0f) 1.0f else sign(data[0])
+ val signY = if (data[5] == 0.0f) 1.0f else sign(data[5])
+ val signZ = if (data[10] == 0.0f) 1.0f else sign(data[10])
+ return Vector3(
+ signX * sqrt(data[0] * data[0] + data[1] * data[1] + data[2] * data[2]),
+ signY * sqrt(data[4] * data[4] + data[5] * data[5] + data[6] * data[6]),
+ signZ * sqrt(data[8] * data[8] + data[9] * data[9] + data[10] * data[10]),
+ )
+ }
+
+ /** Computes the determinant of a 4x4 matrix. */
+ private fun determinant(): Float =
+ data[0] *
+ (data[5] * (data[10] * data[15] - data[14] * data[11]) -
+ data[9] * (data[6] * data[15] - data[14] * data[7]) +
+ data[13] * (data[6] * data[11] - data[10] * data[7])) -
+ data[4] *
+ (data[1] * (data[10] * data[15] - data[14] * data[11]) -
+ data[9] * (data[2] * data[15] - data[14] * data[3]) +
+ data[13] * (data[2] * data[11] - data[10] * data[3])) +
+ data[8] *
+ (data[1] * (data[6] * data[15] - data[14] * data[7]) -
+ data[5] * (data[2] * data[15] - data[14] * data[3]) +
+ data[13] * (data[2] * data[7] - data[6] * data[3])) -
+ data[12] *
+ (data[1] * (data[6] * data[11] - data[10] * data[7]) -
+ data[5] * (data[2] * data[11] - data[10] * data[3]) +
+ data[9] * (data[2] * data[7] - data[6] * data[3]))
+
+ /** Returns true if this pose is equal to [other]. */
+ public override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Matrix4) return false
+
+ return this.data.contentEquals(other.data)
+ }
+
+ /** Standard hash code calculation using constructor values */
+ public override fun hashCode(): Int =
+ 31 * data[0].hashCode() +
+ data[1].hashCode() +
+ data[2].hashCode() +
+ data[3].hashCode() +
+ data[4].hashCode() +
+ data[5].hashCode() +
+ data[6].hashCode() +
+ data[7].hashCode() +
+ data[8].hashCode() +
+ data[9].hashCode() +
+ data[10].hashCode() +
+ data[11].hashCode() +
+ data[12].hashCode() +
+ data[13].hashCode() +
+ data[14].hashCode() +
+ data[15].hashCode()
+
+ /** Standard toString() implementation */
+ public override fun toString(): String =
+ "\n[ " +
+ data[0] +
+ "\t" +
+ data[4] +
+ "\t" +
+ data[8] +
+ "\t" +
+ data[12] +
+ "\n " +
+ data[1] +
+ "\t" +
+ data[5] +
+ "\t" +
+ data[9] +
+ "\t" +
+ data[13] +
+ "\n " +
+ data[2] +
+ "\t" +
+ data[6] +
+ "\t" +
+ data[10] +
+ "\t" +
+ data[14] +
+ "\n " +
+ data[3] +
+ "\t" +
+ data[7] +
+ "\t" +
+ data[11] +
+ "\t" +
+ data[15] +
+ " ]"
+
+ /** Returns a copy of the matrix. */
+ public fun copy(data: FloatArray = this.data): Matrix4 = Matrix4(data)
+
+ public companion object {
+ /** Returns an identity matrix. */
+ @JvmField
+ public val Identity: Matrix4 =
+ Matrix4(floatArrayOf(1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f))
+
+ /** Returns a zero matrix. */
+ @JvmField
+ public val Zero: Matrix4 =
+ Matrix4(floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f))
+
+ /**
+ * Returns a new transformation matrix. The returned matrix is such that it first scales
+ * objects, then rotates them, and finally translates them.
+ */
+ @JvmStatic
+ public fun fromTrs(translation: Vector3, rotation: Quaternion, scale: Vector3): Matrix4 {
+ // implementationd details: https://www.songho.ca/opengl/gl_quaternion.html
+ val q = rotation.toNormalized()
+
+ // double var1 var2
+ val dqyx = 2 * q.y * q.x
+ val dqxz = 2 * q.x * q.z
+ val dqxw = 2 * q.x * q.w
+ val dqyw = 2 * q.y * q.w
+ val dqzw = 2 * q.z * q.w
+ val dqzy = 2 * q.z * q.y
+
+ // double var squared
+ val dsqz = 2 * q.z * q.z
+ val dsqy = 2 * q.y * q.y
+
+ val oneMinusDSQX = 1 - 2 * q.x * q.x
+
+ return Matrix4(
+ floatArrayOf(
+ (1 - dsqy - dsqz) * scale.x,
+ (dqyx + dqzw) * scale.x,
+ (dqxz - dqyw) * scale.x,
+ 0.0f,
+ (dqyx - dqzw) * scale.y,
+ (oneMinusDSQX - dsqz) * scale.y,
+ (dqzy + dqxw) * scale.y,
+ 0.0f,
+ (dqxz + dqyw) * scale.z,
+ (dqzy - dqxw) * scale.z,
+ (oneMinusDSQX - dsqy) * scale.z,
+ 0.0f,
+ translation.x,
+ translation.y,
+ translation.z,
+ 1.0f,
+ )
+ )
+ }
+
+ /** Returns a new translation matrix. */
+ @JvmStatic
+ public fun fromTranslation(translation: Vector3): Matrix4 =
+ Matrix4(
+ floatArrayOf(
+ 1.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 1.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 1.0f,
+ 0.0f,
+ translation.x,
+ translation.y,
+ translation.z,
+ 1.0f,
+ )
+ )
+
+ /** Returns a new uniform scale matrix. */
+ @JvmStatic
+ public fun fromScale(scale: Vector3): Matrix4 =
+ Matrix4(
+ floatArrayOf(
+ scale.x,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ scale.y,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ scale.z,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 1.0f,
+ )
+ )
+
+ /** Returns a new scale matrix. */
+ @JvmStatic
+ public fun fromScale(scale: Float): Matrix4 =
+ Matrix4(
+ floatArrayOf(
+ scale,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ scale,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ scale,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 0.0f,
+ 1.0f,
+ )
+ )
+
+ /** Returns a new rotation matrix. */
+ @JvmStatic
+ public fun fromQuaternion(quaternion: Quaternion): Matrix4 =
+ fromTrs(Vector3.Zero, quaternion, Vector3.One)
+
+ /**
+ * Returns a new rigid transformation matrix. The returned matrix is such that it first
+ * rotates objects, and then translates them.
+ */
+ @JvmStatic
+ public fun fromPose(pose: Pose): Matrix4 {
+ return fromTrs(pose.translation, pose.rotation, Vector3.One)
+ }
+ }
+}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Pose.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Pose.kt
new file mode 100644
index 0000000..6bd40d7
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Pose.kt
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.math
+
+/**
+ * Represents an immutable rigid transformation from one coordinate space to another.
+ *
+ * @property translation the translation component of this pose.
+ * @property rotation the rotation component of this pose.
+ */
+public class Pose
+@JvmOverloads
+constructor(
+ public val translation: Vector3 = Vector3(),
+ public val rotation: Quaternion = Quaternion(),
+) {
+
+ /** Returns a pose that performs the opposite translation. */
+ public val inverse: Pose
+ get() = invert()
+
+ /** The up vector in the local coordinate system. */
+ public inline val up: Vector3
+ get() = rotation * Vector3.Up
+
+ /** The down vector in the local coordinate system. */
+ public inline val down: Vector3
+ get() = rotation * Vector3.Down
+
+ /** The left vector in the local coordinate system. */
+ public inline val left: Vector3
+ get() = rotation * Vector3.Left
+
+ /** The right vector in the local coordinate system. */
+ public inline val right: Vector3
+ get() = rotation * Vector3.Right
+
+ /** The forward vector in the local coordinate system. */
+ public inline val forward: Vector3
+ get() = rotation * Vector3.Forward
+
+ /** The backward vector in the local coordinate system. */
+ public inline val backward: Vector3
+ get() = rotation * Vector3.Backward
+
+ /** Creates a new pose with the same values as the [other] pose. */
+ public constructor(other: Pose) : this(other.translation, other.rotation)
+
+ /** Returns the result of composing [this] with [other]. */
+ public infix fun compose(other: Pose): Pose =
+ Pose(rotation * other.translation + this.translation, rotation * other.rotation)
+
+ /** Returns a pose that performs the opposite transformation. */
+ private fun invert(): Pose {
+ val outRotation = rotation.inverse
+ val outTranslation = -(outRotation * translation)
+
+ return Pose(outTranslation, outRotation)
+ }
+
+ /** Translates this pose by the given [translation]. */
+ public fun translate(translation: Vector3): Pose =
+ Pose(this.translation + translation, this.rotation)
+
+ /** Rotates this pose by the given [rotation]. */
+ public fun rotate(rotation: Quaternion): Pose = Pose(this.translation, this.rotation * rotation)
+
+ /**
+ * Transforms the provided point by the pose by applying both the rotation and the translation
+ * components of the pose. This is because a point represents a specific location in space. It
+ * needs to account for the position, scale and orientation of the space it is in.
+ */
+ public infix fun transformPoint(point: Vector3): Vector3 = rotation * point + translation
+
+ /**
+ * Transforms the provided vector by the pose by only applying the rotation component of the
+ * pose. This is because a vector represents a direction and magnitude, not a specific location.
+ * It only needs to account for the scale and orientation of the space it is in since it has no
+ * position.
+ */
+ public infix fun transformVector(vector: Vector3): Vector3 = rotation * vector
+
+ /** Returns a copy of the pose. */
+ @JvmOverloads
+ public fun copy(
+ translation: Vector3 = this.translation,
+ rotation: Quaternion = this.rotation,
+ ): Pose = Pose(translation, rotation)
+
+ /** Returns true if this pose is equal to the [other]. */
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Pose) return false
+
+ return this.translation == other.translation && this.rotation == other.rotation
+ }
+
+ override fun hashCode(): Int = 31 * translation.hashCode() + rotation.hashCode()
+
+ override fun toString(): String = "Pose{\n\tTranslation=$translation\n\tRotation=$rotation\n}"
+
+ public companion object {
+ /** Returns a new pose using the identity rotation. */
+ @JvmField public val Identity: Pose = Pose()
+
+ /**
+ * Returns a new pose oriented to look at [target] from [eye] position with [up] as the up
+ * vector.
+ *
+ * @param eye the position from which to look at [target].
+ * @param target the target position to look at.
+ * @param up a vector indicating the general "up" direction.
+ * @return the pose oriented to look at [target] from [eye] position with [up] as the up
+ * vector.
+ */
+ @JvmStatic
+ @JvmOverloads
+ public fun fromLookAt(eye: Vector3, target: Vector3, up: Vector3 = Vector3.Up): Pose {
+ val forward = (target - eye).toNormalized()
+ val rotation = Quaternion.fromLookTowards(forward, up)
+
+ return Pose(eye, rotation)
+ }
+
+ /** Returns the distance between the two poses. */
+ @JvmStatic
+ public fun distance(lhs: Pose, rhs: Pose): Float =
+ Vector3.Companion.distance(lhs.translation, rhs.translation)
+
+ /**
+ * Returns a new pose that is linearly interpolated between [start] and [end] using the
+ * interpolation amount [ratio]. The position is [lerped][Vector3.lerp], but the rotation
+ * will be [slerped][Quaternion.slerp] if the angles are far apart.
+ *
+ * If [ratio] is outside of the range `[0, 1]`, the returned pose will be extrapolated.
+ */
+ @JvmStatic
+ public fun lerp(start: Pose, end: Pose, ratio: Float): Pose {
+ val interpolatedPosition =
+ Vector3.Companion.lerp(start.translation, end.translation, ratio)
+
+ val interpolatedRotation =
+ if (start.rotation.dot(end.rotation) < 0.9995f) { // Check if angle is large
+ Quaternion.Companion.slerp(start.rotation, end.rotation, ratio)
+ } else {
+ // If the angle is small, lerp can be used for efficiency.
+ // Note: This assumes both quaternions are normalized.
+ Quaternion.Companion.lerp(start.rotation, end.rotation, ratio).toNormalized()
+ }
+
+ return Pose(interpolatedPosition, interpolatedRotation)
+ }
+ }
+}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Quaternion.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Quaternion.kt
new file mode 100644
index 0000000..9b19e12
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Quaternion.kt
@@ -0,0 +1,389 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.xr.runtime.math
+
+import kotlin.math.abs
+import kotlin.math.acos
+import kotlin.math.asin
+import kotlin.math.atan2
+import kotlin.math.cos
+import kotlin.math.sin
+import kotlin.math.sqrt
+
+/**
+ * Represents a rotation component in three-dimensional space. Any vector can be provided and the
+ * resulting quaternion will be normalized at construction time.
+ *
+ * @param x the x value of the quaternion.
+ * @param y the y value of the quaternion.
+ * @param z the z value of the quaternion.
+ * @param w the rotation of the unit vector, in radians.
+ */
+public class Quaternion
+@JvmOverloads
+constructor(x: Float = 0F, y: Float = 0F, z: Float = 0F, w: Float = 1F) {
+ /** The normalized x component of the quaternion. */
+ public val x: Float
+ /** The normalized y component of the quaternion. */
+ public val y: Float
+ /** The normalized z component of the quaternion. */
+ public val z: Float
+ /** The normalized w component of the quaternion. */
+ public val w: Float
+
+ init {
+ val length = sqrt(x * x + y * y + z * z + w * w)
+ this.x = x / length
+ this.y = y / length
+ this.z = z / length
+ this.w = w / length
+ }
+
+ /** Returns a new quaternion with the inverse rotation. Assumes unit length. */
+ public inline val inverse: Quaternion
+ get() = Quaternion(-x, -y, -z, w)
+
+ /**
+ * Returns this quaternion as Euler angles (in degrees) applied in YXZ (yaw, pitch, roll) order.
+ */
+ public val eulerAngles: Vector3
+ get() = toYawPitchRoll()
+
+ /** Returns this quaternion as an axis/angle (in degrees) pair. */
+ public val axisAngle: Pair<Vector3, Float>
+ get() = toAxisAngle()
+
+ /** Creates a new quaternion with the same values as the [other] quaternion. */
+ public constructor(other: Quaternion) : this(other.x, other.y, other.z, other.w)
+
+ /** Creates a new quaternion using the components of a [Vector4]. */
+ internal constructor(vector: Vector4) : this(vector.x, vector.y, vector.z, vector.w)
+
+ /** Flips the sign of the quaternion, but represents the same rotation. */
+ public inline operator fun unaryMinus(): Quaternion = Quaternion(-x, -y, -z, -w)
+
+ /** Returns a new quaternion with the sum of this quaternion and [other] quaternion. */
+ public inline operator fun plus(other: Quaternion): Quaternion =
+ Quaternion(x + other.x, y + other.y, z + other.z, w + other.w)
+
+ /**
+ * Returns a new quaternion with the difference of this quaternion and the [other] quaternion.
+ */
+ public inline operator fun minus(other: Quaternion): Quaternion =
+ Quaternion(x - other.x, y - other.y, z - other.z, w - other.w)
+
+ /** Rotates a [Vector3] by this quaternion. */
+ public inline operator fun times(src: Vector3): Vector3 {
+ val qx = x
+ val qy = y
+ val qz = z
+ val qw = w
+ val vx = src.x
+ val vy = src.y
+ val vz = src.z
+
+ val rx = qy * vz - qz * vy + qw * vx
+ val ry = qz * vx - qx * vz + qw * vy
+ val rz = qx * vy - qy * vx + qw * vz
+ val sx = qy * rz - qz * ry
+ val sy = qz * rx - qx * rz
+ val sz = qx * ry - qy * rx
+ return Vector3(2 * sx + vx, 2 * sy + vy, 2 * sz + vz)
+ }
+
+ /**
+ * Returns a new quaternion with the product of this quaternion and the [other] quaternion. The
+ * order of the multiplication is `[this] * [other]`.
+ */
+ public inline operator fun times(other: Quaternion): Quaternion {
+ val lx = this.x
+ val ly = this.y
+ val lz = this.z
+ val lw = this.w
+ val rx = other.x
+ val ry = other.y
+ val rz = other.z
+ val rw = other.w
+
+ return Quaternion(
+ lw * rx + lx * rw + ly * rz - lz * ry,
+ lw * ry - lx * rz + ly * rw + lz * rx,
+ lw * rz + lx * ry - ly * rx + lz * rw,
+ lw * rw - lx * rx - ly * ry - lz * rz,
+ )
+ }
+
+ /** Returns a new quaternion with the product of this quaternion and a scalar amount. */
+ public operator fun times(c: Float): Quaternion = Quaternion(x * c, y * c, z * c, w * c)
+
+ /** Returns a new quaternion with this quaternion divided by a scalar amount. */
+ public operator fun div(c: Float): Quaternion = Quaternion(x / c, y / c, z / c, w / c)
+
+ /** Returns a new quaternion with a normalized rotation. */
+ public fun toNormalized(): Quaternion {
+ val norm = rsqrt(x * x + y * y + z * z + w * w)
+ return this * norm
+ }
+
+ /** Returns the dot product of this quaternion and the [other] quaternion. */
+ public inline infix fun dot(other: Quaternion): Float =
+ x * other.x + y * other.y + z * other.z + w * other.w
+
+ /**
+ * Get a [Vector3] containing the pitch, yaw and roll in degrees, extracted in YXZ (yaw, pitch,
+ * roll) order.
+ */
+ private fun toYawPitchRoll(): Vector3 {
+ val test = w * x - y * z
+ if (test > EULER_THRESHOLD) {
+ // There is a singularity when the pitch is directly up, so calculate the
+ // angles another way.
+ return Vector3(90f, toDegrees(-2 * atan2(z, w)), 0f)
+ }
+ if (test < -EULER_THRESHOLD) {
+ // There is a singularity when the pitch is directly down, so calculate the
+ // angles another way.
+ return Vector3(-90f, toDegrees(2 * atan2(z, w)), 0f)
+ }
+
+ val pitch = asin(2 * test)
+ val yaw = atan2(2 * (w * y + x * z).toDouble(), 1.0 - 2 * (x * x + y * y)).toFloat()
+ val roll = atan2(2 * (w * z + x * y).toDouble(), 1.0 - 2 * (x * x + z * z)).toFloat()
+
+ return Vector3(toDegrees(pitch), toDegrees(yaw), toDegrees(roll))
+ }
+
+ /** Returns a Pair containing the axis of rotation and the angle of rotation in degrees. */
+ private fun toAxisAngle(): Pair<Vector3, Float> {
+ val normalized = this.toNormalized()
+ val angleRadians = 2 * acos(normalized.w)
+ val sinHalfAngle = sin(angleRadians / 2)
+ val axis =
+ if (sinHalfAngle < 0.0001f) {
+ Vector3.Right // Default axis when angle is 0
+ } else {
+ Vector3(
+ normalized.x / sinHalfAngle,
+ normalized.y / sinHalfAngle,
+ normalized.z / sinHalfAngle,
+ )
+ }
+
+ return Pair(axis, toDegrees(angleRadians))
+ }
+
+ /** Returns a copy of the quaternion. */
+ @JvmOverloads
+ public fun copy(
+ x: Float = this.x,
+ y: Float = this.y,
+ z: Float = this.z,
+ w: Float = this.w,
+ ): Quaternion = Quaternion(x, y, z, w)
+
+ /** Returns true if this quaternion is equal to the [other]. */
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Quaternion) return false
+
+ return this.x == other.x && this.y == other.y && this.z == other.z && this.w == other.w
+ }
+
+ override fun hashCode(): Int = 31 * x.hashCode() + y.hashCode() + z.hashCode() + w.hashCode()
+
+ override fun toString(): String = "[x=$x, y=$y, z=$z, w=$w]"
+
+ /** Returns a new quaternion with the identity rotation. */
+ public companion object {
+ private const val EULER_THRESHOLD: Float = 0.49999994f
+ private const val COS_THRESHOLD: Float = 0.9995f
+
+ @JvmField public val Identity: Quaternion = Quaternion()
+
+ /** Returns a new quaternion representing the rotation from one vector to another. */
+ @JvmStatic
+ public fun fromRotation(start: Vector3, end: Vector3): Quaternion {
+ val startNorm = start.toNormalized()
+ val endNorm = end.toNormalized()
+
+ val cosTheta = startNorm.dot(endNorm)
+ if (cosTheta < -COS_THRESHOLD) {
+ // Special case when vectors in opposite directions: no "ideal" rotation axis
+ // Guess one; any work as long as perpendicular to start
+ var rotationAxis = Vector3.Backward.cross(startNorm)
+ if (rotationAxis.lengthSquared < 0.01f) {
+ rotationAxis =
+ Vector3.Right.cross(
+ startNorm
+ ) // pick new rotation axis as the original was parallel
+ }
+ return Quaternion.Companion.fromAxisAngle(rotationAxis, 180f)
+ }
+
+ val rotationAxis = startNorm.cross(endNorm)
+
+ return Quaternion(rotationAxis.x, rotationAxis.y, rotationAxis.z, 1 + cosTheta)
+ .toNormalized()
+ }
+
+ /** Returns a new quaternion representing the rotation from one quaternion to another. */
+ @JvmStatic
+ public fun fromRotation(start: Quaternion, end: Quaternion): Quaternion =
+ Quaternion(end * start.inverse).toNormalized()
+
+ /** Returns a new quaternion with the specified forward and upward directions. */
+ @JvmStatic
+ public fun fromLookTowards(forward: Vector3, up: Vector3): Quaternion {
+ val forwardNormalized = forward.toNormalized()
+ val right = (up cross forwardNormalized).toNormalized()
+ val upNormalized = (forwardNormalized cross right).toNormalized()
+
+ val m00 = right.x
+ val m01 = right.y
+ val m02 = right.z
+ val m10 = upNormalized.x
+ val m11 = upNormalized.y
+ val m12 = upNormalized.z
+ val m20 = forwardNormalized.x
+ val m21 = forwardNormalized.y
+ val m22 = forwardNormalized.z
+
+ val trace = m00 + m11 + m22
+ return if (trace > 0) {
+ val s = 0.5f / sqrt(trace + 1.0f)
+ Quaternion((m12 - m21) * s, (m20 - m02) * s, (m01 - m10) * s, 0.25f / s)
+ } else {
+ if (m00 > m11 && m00 > m22) {
+ val s = sqrt(1.0f + m00 - m11 - m22) * 2.0f
+ Quaternion(0.25f * s, (m01 + m10) / s, (m02 + m20) / s, (m12 - m21) / s)
+ } else if (m11 > m22) {
+ val s = sqrt(1.0f + m11 - m00 - m22) * 2.0f
+ Quaternion((m01 + m10) / s, 0.25f * s, (m12 + m21) / s, (m20 - m02) / s)
+ } else {
+ val s = sqrt(1.0f + m22 - m00 - m11) * 2.0f
+ Quaternion((m02 + m20) / s, (m12 + m21) / s, 0.25f * s, (m01 - m10) / s)
+ }
+ }
+ }
+
+ /** Creates a new quaternion using an axis/angle to define the rotation. */
+ @JvmStatic
+ public fun fromAxisAngle(axis: Vector3, degrees: Float): Quaternion =
+ Quaternion(
+ sin(0.5f * toRadians(degrees)) * axis.toNormalized().x,
+ sin(0.5f * toRadians(degrees)) * axis.toNormalized().y,
+ sin(0.5f * toRadians(degrees)) * axis.toNormalized().z,
+ cos(0.5f * toRadians(degrees)),
+ )
+
+ /**
+ * Returns a new quaternion using Euler angles (in degrees) to define the rotation in YXZ
+ * (yaw, pitch, roll) order.
+ */
+ @JvmStatic
+ public fun fromEulerAngles(eulerAngles: Vector3): Quaternion =
+ Quaternion(fromAxisAngle(Vector3.Up, eulerAngles.y)) *
+ Quaternion(fromAxisAngle(Vector3.Right, eulerAngles.x)) *
+ Quaternion(fromAxisAngle(Vector3.Backward, eulerAngles.z))
+
+ /**
+ * Returns a new quaternion using Euler angles (in degrees) to define the rotation in YXZ
+ * (yaw, pitch, roll) order.
+ */
+ @JvmStatic
+ public fun fromEulerAngles(pitch: Float, yaw: Float, roll: Float): Quaternion =
+ Quaternion(fromAxisAngle(Vector3.Up, yaw)) *
+ Quaternion(fromAxisAngle(Vector3.Right, pitch)) *
+ Quaternion(fromAxisAngle(Vector3.Backward, roll))
+
+ /**
+ * Returns a new quaternion that is linearly interpolated between [start] and [end] using
+ * the interpolation amount [ratio].
+ *
+ * If [ratio] is outside of the range `[0, 1]`, the returned quaternion will be
+ * extrapolated.
+ */
+ @JvmStatic
+ public fun lerp(start: Quaternion, end: Quaternion, ratio: Float): Quaternion =
+ Quaternion(
+ lerp(start.x, end.x, ratio),
+ lerp(start.y, end.y, ratio),
+ lerp(start.z, end.z, ratio),
+ lerp(start.w, end.w, ratio),
+ )
+
+ /**
+ * Returns a new quaternion that is spherically interpolated between [start] and [end] using
+ * the interpolation amount [ratio]. If [ratio] is 0, this returns [start]. As [ratio]
+ * approaches 1, the result of this function may approach either `+end` or `-end` (whichever
+ * is closest to [start]).
+ *
+ * If [ratio] is outside of the range `[0, 1]`, the returned quaternion will be
+ * extrapolated.
+ */
+ @JvmStatic
+ public fun slerp(start: Quaternion, end: Quaternion, ratio: Float): Quaternion {
+ val orientationStart = start
+ var orientationEnd = end
+
+ // cosTheta0 provides the angle between the rotations at t = 0
+ var cosTheta0 = orientationStart.dot(orientationEnd)
+
+ // Flip end rotation to get shortest path between the two rotations
+ if (cosTheta0 < 0.0f) {
+ orientationEnd = -orientationEnd
+ cosTheta0 = -cosTheta0
+ }
+
+ // Small rotations can use linear interpolation
+ if (cosTheta0 > COS_THRESHOLD) {
+ return lerp(orientationStart, orientationEnd, ratio)
+ }
+
+ val sinTheta0 = sqrt(1.0 - cosTheta0 * cosTheta0)
+ val theta0 = acos(cosTheta0)
+ val thetaT = theta0 * ratio
+ val sinThetaT = sin(thetaT)
+ val costThetaT = cos(thetaT)
+
+ val s1 = sinThetaT / sinTheta0
+ val s0 = costThetaT - cosTheta0 * s1
+
+ // Do component-wise multiplication since s0 and s1 could be near-zero which would cause
+ // precision issues when (quat * 0.0f) is normalized due to division by near-zero
+ // length.
+ return Quaternion(
+ orientationStart.x * s0.toFloat() + orientationEnd.x * s1.toFloat(),
+ orientationStart.y * s0.toFloat() + orientationEnd.y * s1.toFloat(),
+ orientationStart.z * s0.toFloat() + orientationEnd.z * s1.toFloat(),
+ orientationStart.w * s0.toFloat() + orientationEnd.w * s1.toFloat(),
+ )
+ }
+
+ /** Returns the angle between [start] and [end] quaternion in degrees. */
+ @JvmStatic
+ public fun angle(start: Quaternion, end: Quaternion): Float =
+ toDegrees(2.0f * acos(abs(clamp(dot(start, end), -1.0f, 1.0f))))
+
+ /** Returns the dot product of two quaternions. */
+ @JvmStatic
+ public fun dot(lhs: Quaternion, rhs: Quaternion): Float =
+ lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z + lhs.w * rhs.w
+ }
+}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Ray.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Ray.kt
new file mode 100644
index 0000000..f4d9113
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Ray.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.math
+
+/**
+ * Represents a ray in 3D space. A ray is defined by an origin point and a direction vector.
+ *
+ * @property origin the origin of the ray.
+ * @property direction the direction of the ray.
+ */
+public class Ray(
+ public val origin: Vector3 = Vector3(),
+ public val direction: Vector3 = Vector3(),
+) {
+ /** Creates a new Ray with the same values as the [other] ray. */
+ public constructor(other: Ray) : this(other.origin, other.direction)
+
+ /** Returns true if this ray is equal to the [other] ray. */
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Ray) return false
+
+ return this.origin == other.origin && this.direction == other.direction
+ }
+
+ override fun hashCode(): Int = 31 * origin.hashCode() + direction.hashCode()
+
+ override fun toString(): String = "[origin=$origin, direction=$direction]"
+}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Vector2.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Vector2.kt
new file mode 100644
index 0000000..9f35620
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Vector2.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.xr.runtime.math
+
+import kotlin.math.abs
+import kotlin.math.acos
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.sqrt
+
+/**
+ * Represents a position in the 2D plane.
+ *
+ * @property x X component of the vector.
+ * @property y Y component of the vector.
+ */
+public class Vector2 @JvmOverloads constructor(public val x: Float = 0F, public val y: Float = 0F) {
+ /** The squared length of the vector. */
+ public inline val lengthSquared: Float
+ get() = x * x + y * y
+
+ /** The length of the vector. */
+ public inline val length: Float
+ get() = sqrt(lengthSquared)
+
+ /** Creates a new vector with the same values as the [other] vector. */
+ public constructor(other: Vector2) : this(other.x, other.y)
+
+ /** Negates the values of this vector. */
+ public inline operator fun unaryMinus(): Vector2 = Vector2(-x, -y)
+
+ /** Returns a new vector with the sum of this vector and the [other] vector. */
+ public operator fun plus(other: Vector2): Vector2 = Vector2(this.x + other.x, this.y + other.y)
+
+ /** Returns a new vector with the difference of this vector and the [other] vector. */
+ public inline operator fun minus(other: Vector2): Vector2 =
+ Vector2(this.x - other.x, this.y - other.y)
+
+ /** Returns a new vector multiplied by a scalar amount */
+ public inline operator fun times(c: Float): Vector2 = Vector2(x * c, y * c)
+
+ /** Returns a new vector with the product of this vector and the [other] vector. */
+ public inline operator fun times(other: Vector2): Vector2 =
+ Vector2(this.x * other.x, this.y * other.y)
+
+ /** Returns a new vector with this vector divided by a scalar amount. */
+ public inline operator fun div(c: Float): Vector2 = Vector2(x / c, y / c)
+
+ /** Returns a new vector with this vector divided by the [other] vector. */
+ public inline operator fun div(other: Vector2): Vector2 = Vector2(x / other.x, y / other.y)
+
+ /** Returns a normalized version of this vector. */
+ public fun toNormalized(): Vector2 {
+ val norm = rsqrt(lengthSquared)
+
+ return Vector2(x * norm, y * norm)
+ }
+
+ /** Returns the cross product of this vector and the [other] vector. */
+ public inline infix fun cross(other: Vector2): Float = this.x * other.y - this.y * other.x
+
+ /** Returns the dot product of this vector and the [other] vector. */
+ public inline infix fun dot(other: Vector2): Float = x * other.x + y * other.y
+
+ /** Returns a new vector with the values clamped between [min] and [max] vectors. */
+ public fun clamp(min: Vector2, max: Vector2): Vector2 {
+ var clampedX = max(x, min.x)
+ var clampedY = max(y, min.y)
+
+ clampedX = min(clampedX, max.x)
+ clampedY = min(clampedY, max.y)
+
+ return Vector2(clampedX, clampedY)
+ }
+
+ /** Returns a copy of the vector. */
+ @JvmOverloads
+ public inline fun copy(x: Float = this.x, y: Float = this.y): Vector2 = Vector2(x, y)
+
+ /** Returns true if this vector is equal to the [other]. */
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Vector2) return false
+
+ return this.x == other.x && this.y == other.y
+ }
+
+ override fun hashCode(): Int = 31 * x.hashCode() + y.hashCode()
+
+ override fun toString(): String = "[x=$x, y=$y]"
+
+ public companion object {
+ /** Vector with all components set to zero. */
+ @JvmField public val Zero: Vector2 = Vector2(x = 0f, y = 0f)
+
+ /** Vector with all components set to one. */
+ @JvmField public val One: Vector2 = Vector2(x = 1f, y = 1f)
+
+ /** Vector with y set to one and all other components set to zero. */
+ @JvmField public val Up: Vector2 = Vector2(x = 0f, y = 1f)
+
+ /** Vector with y set to negative one and all other components set to zero. */
+ @JvmField public val Down: Vector2 = Vector2(x = 0f, y = -1f)
+
+ /** Vector with x set to negative one and all other components set to zero. */
+ @JvmField public val Left: Vector2 = Vector2(x = -1f, y = 0f)
+
+ /** Vector with x set to one and all other components set to zero. */
+ @JvmField public val Right: Vector2 = Vector2(x = 1f, y = 0f)
+
+ /** Returns the distance between this vector and the [other] vector. */
+ @JvmStatic
+ public fun distance(vector1: Vector2, vector2: Vector2): Float = (vector1 - vector2).length
+
+ /** Returns the angle between this vector and the [other] vector. */
+ @JvmStatic
+ public fun angularDistance(vector1: Vector2, vector2: Vector2): Float {
+ val dot = vector1 dot vector2
+ val magnitude = vector1.length * vector2.length
+
+ if (magnitude < 1e-10f) {
+ return 0.0f
+ }
+
+ // Clamp due to floating point precision errors that could cause dot to be > mag.
+ // Would cause acos to return NaN.
+ val cos = clamp(dot / magnitude, -1.0f, 1.0f)
+ val angleRadians = acos(cos)
+
+ return toDegrees(angleRadians)
+ }
+
+ /**
+ * Returns a new vector that is linearly interpolated between [start] and [end] using the
+ * interpolation amount [ratio].
+ *
+ * If [ratio] is outside of the range `[0, 1]`, the returned vector will be extrapolated.
+ */
+ @JvmStatic
+ public fun lerp(start: Vector2, end: Vector2, ratio: Float): Vector2 =
+ Vector2(lerp(start.x, end.x, ratio), lerp(start.y, end.y, ratio))
+
+ /** Returns the absolute values of each component of the vector. */
+ @JvmStatic public fun abs(vector: Vector2): Vector2 = Vector2(abs(vector.x), abs(vector.y))
+ }
+}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Vector3.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Vector3.kt
new file mode 100644
index 0000000..59dac4c
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Vector3.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.math
+
+import kotlin.math.abs
+import kotlin.math.acos
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.sqrt
+
+/**
+ * Represents a three-dimensional position in space.
+ *
+ * The coordinate system is right-handed. The [x]-axis points to the right, the [y]-axis up and the
+ * [z]-axis back.
+ *
+ * @property x the value of the horizontal component.
+ * @property y the value of the vertical component.
+ * @property z the value of the forward component.
+ */
+public class Vector3
+@JvmOverloads
+constructor(public val x: Float = 0F, public val y: Float = 0F, public val z: Float = 0F) {
+ /** The squared length of the vector. */
+ public inline val lengthSquared: Float
+ get() = x * x + y * y + z * z
+
+ /** The length of the vector. */
+ public inline val length: Float
+ get() = sqrt(lengthSquared)
+
+ /** Creates a new vector with the same values as the [other] vector. */
+ public constructor(other: Vector3) : this(other.x, other.y, other.z)
+
+ /** Negates this vector. */
+ public operator fun unaryMinus(): Vector3 = Vector3(-x, -y, -z)
+
+ /** Returns a new vector with the sum of this vector and the [other] vector. */
+ public operator fun plus(other: Vector3): Vector3 =
+ Vector3(x + other.x, y + other.y, z + other.z)
+
+ /** Returns a new vector with the difference of this vector and the [other] vector. */
+ public operator fun minus(other: Vector3): Vector3 =
+ Vector3(x - other.x, y - other.y, z - other.z)
+
+ /** Get a new vector multiplied by a scalar amount. */
+ public operator fun times(c: Float): Vector3 = Vector3(x * c, y * c, z * c)
+
+ /** Returns a new vector with the product of this vector and the [other] vector. */
+ public operator fun times(other: Vector3): Vector3 =
+ Vector3(x * other.x, y * other.y, z * other.z)
+
+ /** Returns a new vector with this vector divided by a scalar amount. */
+ public operator fun div(c: Float): Vector3 = Vector3(x / c, y / c, z / c)
+
+ /** Returns a new vector with this vector divided by the [other] vector. */
+ public operator fun div(other: Vector3): Vector3 =
+ Vector3(x / other.x, y / other.y, z / other.z)
+
+ /** Returns the dot product of this vector and the [other] vector. */
+ public infix fun dot(other: Vector3): Float = x * other.x + y * other.y + z * other.z
+
+ /** Returns the cross product of this vector and the [other] vector. */
+ public infix fun cross(other: Vector3): Vector3 =
+ Vector3(y * other.z - z * other.y, z * other.x - x * other.z, x * other.y - y * other.x)
+
+ /** Returns the normalized version of this vector. */
+ public fun toNormalized(): Vector3 {
+ val norm = rsqrt(lengthSquared)
+
+ return Vector3(x * norm, y * norm, z * norm)
+ }
+
+ /** Returns a new vector with its values clamped between [min] and [max] vectors. */
+ public fun clamp(min: Vector3, max: Vector3): Vector3 {
+ var clampedX = clamp(x, min.x, max.x)
+ var clampedY = clamp(y, min.y, max.y)
+ var clampedZ = clamp(z, min.z, max.z)
+
+ return Vector3(clampedX, clampedY, clampedZ)
+ }
+
+ /** Returns a copy of the vector. */
+ @JvmOverloads
+ public fun copy(x: Float = this.x, y: Float = this.y, z: Float = this.z): Vector3 =
+ Vector3(x, y, z)
+
+ /** Returns true if this vector is equal to [other]. */
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Vector3) return false
+
+ return this.x == other.x && this.y == other.y && this.z == other.z
+ }
+
+ override fun hashCode(): Int = 31 * x.hashCode() + y.hashCode() + z.hashCode()
+
+ override fun toString(): String = "[x=$x, y=$y, z=$z]"
+
+ public companion object {
+ /** Vector with all components set to zero. */
+ @JvmField public val Zero: Vector3 = Vector3(x = 0f, y = 0f, z = 0f)
+
+ /** Vector with all components set to one. */
+ @JvmField public val One: Vector3 = Vector3(x = 1f, y = 1f, z = 1f)
+
+ /** Vector with y set to one and all other components set to zero. */
+ @JvmField public val Up: Vector3 = Vector3(x = 0f, y = 1f, z = 0f)
+
+ /** Vector with y set to negative one and all other components set to zero. */
+ @JvmField public val Down: Vector3 = Vector3(x = 0f, y = -1f, z = 0f)
+
+ /** Vector with x set to negative one and all other components set to zero. */
+ @JvmField public val Left: Vector3 = Vector3(x = -1f, y = 0f, z = 0f)
+
+ /** Vector with x set to one and all other components set to zero. */
+ @JvmField public val Right: Vector3 = Vector3(x = 1f, y = 0f, z = 0f)
+
+ /** Vector with z set to one and all other components set to zero. */
+ @JvmField public val Backward: Vector3 = Vector3(x = 0f, y = 0f, z = 1f)
+
+ /** Vector with z set to negative one and all other components set to zero. */
+ @JvmField public val Forward: Vector3 = Vector3(x = 0f, y = 0f, z = -1f)
+
+ /** Creates a new vector with all components set to [value]. */
+ @JvmStatic public fun fromValue(value: Float): Vector3 = Vector3(value, value, value)
+
+ /**
+ * Returns the angle between this vector and the [other] vector in degrees. The result is
+ * never greater than 180 degrees.
+ */
+ @JvmStatic
+ public fun angleBetween(vector1: Vector3, vector2: Vector3): Float {
+ val dot = vector1 dot vector2
+ val magnitude = vector1.length * vector2.length
+
+ if (magnitude < 1e-10f) {
+ return 0.0f
+ }
+
+ // Clamp due to floating point precision errors that could cause dot to be > mag.
+ // Would cause acos to return NaN.
+ val cos = clamp(dot / magnitude, -1.0f, 1.0f)
+
+ return acos(cos)
+ }
+
+ /** Returns the distance between this vector and the [other] vector. */
+ @JvmStatic
+ public fun distance(vector1: Vector3, vector2: Vector3): Float = (vector1 - vector2).length
+
+ /**
+ * Returns a new vector that is linearly interpolated between [start] and [end] using the
+ * interpolated amount [ratio].
+ *
+ * If [ratio] is outside of the range `[0, 1]`, the returned vector will be extrapolated.
+ */
+ @JvmStatic
+ public fun lerp(start: Vector3, end: Vector3, ratio: Float): Vector3 =
+ Vector3(
+ lerp(start.x, end.x, ratio),
+ lerp(start.y, end.y, ratio),
+ lerp(start.z, end.z, ratio)
+ )
+
+ /** Returns the minimum of each component of the two vectors. */
+ @JvmStatic
+ public fun min(a: Vector3, b: Vector3): Vector3 =
+ Vector3(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z))
+
+ /** Returns the maximum of each component of the two vectors. */
+ @JvmStatic
+ public fun max(a: Vector3, b: Vector3): Vector3 =
+ Vector3(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z))
+
+ /** Computes the vector projected from [vector] onto [planeNormal]. */
+ @JvmStatic
+ public fun projectOnPlane(vector: Vector3, planeNormal: Vector3): Vector3 =
+ vector - planeNormal * (vector dot planeNormal) / (planeNormal dot planeNormal)
+
+ /** Returns the absolute values of each component of the vector. */
+ @JvmStatic
+ public fun abs(vector: Vector3): Vector3 =
+ Vector3(abs(vector.x), abs(vector.y), abs(vector.z))
+ }
+}
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Vector4.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Vector4.kt
new file mode 100644
index 0000000..29d137c
--- /dev/null
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/math/Vector4.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.math
+
+import kotlin.math.abs
+import kotlin.math.acos
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.sqrt
+
+/**
+ * Represents a four-dimensional position in space.
+ *
+ * @param x X component of the vector.
+ * @param y Y component of the vector.
+ * @param z Z component of the vector.
+ * @param w W component of the vector.
+ */
+internal class Vector4
+@JvmOverloads
+constructor(
+ public val x: Float = 0F,
+ public val y: Float = 0F,
+ public val z: Float = 0F,
+ public val w: Float = 0F,
+) {
+ /** The squared length of the vector. */
+ public inline val lengthSquared: Float
+ get() = x * x + y * y + z * z + w * w
+
+ /** The length of the vector. */
+ public inline val length: Float
+ get() = sqrt(lengthSquared)
+
+ /** Creates a new vector with the same values as the [other] vector. */
+ public constructor(other: Vector4) : this(other.x, other.y, other.z, other.w)
+
+ /** Negates this vector. */
+ public operator fun unaryMinus(): Vector4 = Vector4(-x, -y, -z, -w)
+
+ /** Returns a new vector with the sum of this vector and the [other] vector. */
+ public operator fun plus(other: Vector4): Vector4 =
+ Vector4(x + other.x, y + other.y, z + other.z, w + other.w)
+
+ /** Returns a new vector with the difference of this vector and the [other] vector. */
+ public operator fun minus(other: Vector4): Vector4 =
+ Vector4(x - other.x, y - other.y, z - other.z, w - other.w)
+
+ /** Get a new vector multiplied by a scalar amount. */
+ public operator fun times(c: Float): Vector4 = Vector4(x * c, y * c, z * c, w * c)
+
+ /** Returns a new vector with the product of this vector and the [other] vector. */
+ public operator fun times(other: Vector4): Vector4 =
+ Vector4(x * other.x, y * other.y, z * other.z, w * other.w)
+
+ /** Returns a new vector with this vector divided by a scalar amount. */
+ public operator fun div(c: Float): Vector4 = Vector4(x / c, y / c, z / c, w / c)
+
+ /** Returns a new vector with this vector divided by the [other] vector. */
+ public operator fun div(other: Vector4): Vector4 =
+ Vector4(x / other.x, y / other.y, z / other.z, w / other.w)
+
+ /** Returns the dot product of this vector and the [other] vector. */
+ public infix fun dot(other: Vector4): Float =
+ x * other.x + y * other.y + z * other.z + w * other.w
+
+ /** Returns the normalized version of this vector. */
+ public fun toNormalized(): Vector4 {
+ val norm = rsqrt(lengthSquared)
+
+ return Vector4(x * norm, y * norm, z * norm, w * norm)
+ }
+
+ /** Returns a new vector with the values clamped between [min] and [max] vectors. */
+ public fun clamp(min: Vector4, max: Vector4): Vector4 {
+ val clampedX = clamp(x, min.x, max.x)
+ val clampedY = clamp(y, min.y, max.y)
+ val clampedZ = clamp(z, min.z, max.z)
+ val clampedW = clamp(w, min.w, max.w)
+
+ return Vector4(clampedX, clampedY, clampedZ, clampedW)
+ }
+
+ /** Returns a copy of the vector. */
+ @JvmOverloads
+ public fun copy(
+ x: Float = this.x,
+ y: Float = this.y,
+ z: Float = this.z,
+ w: Float = this.w,
+ ): Vector4 = Vector4(x, y, z, w)
+
+ /** Returns true if this vector is equal to the [other]. */
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Vector4) return false
+
+ return this.x == other.x && this.y == other.y && this.z == other.z && this.w == other.w
+ }
+
+ override fun hashCode(): Int = 31 * x.hashCode() + y.hashCode() + z.hashCode() + w.hashCode()
+
+ override fun toString(): String = "[x=$x, y=$y, z=$z, w=$w]"
+
+ public companion object {
+ /** Vector with all components set to zero. */
+ @JvmField public val Zero: Vector4 = Vector4(x = 0f, y = 0f, z = 0f, w = 0f)
+
+ /** Vector with all components set to one. */
+ @JvmField public val One: Vector4 = Vector4(x = 1f, y = 1f, z = 1f, w = 1f)
+
+ /** Creates a new vector with all components set to [value]. */
+ @JvmStatic public fun fromValue(value: Float): Vector4 = Vector4(value, value, value, value)
+
+ /**
+ * Returns the angle between this vector and [other] vector in degrees. The result is never
+ * greater than 180 degrees.
+ */
+ @JvmStatic
+ public fun angleBetween(vector1: Vector4, vector2: Vector4): Float {
+ val dot = vector1 dot vector2
+ val magnitude = vector1.length * vector2.length
+
+ if (magnitude < 1e-10f) {
+ return 0.0f
+ }
+
+ // Clamp due to floating point precision errors that could cause dot to be > mag.
+ // Would cause acos to return NaN.
+ val cos = clamp(dot / magnitude, -1.0f, 1.0f)
+
+ return acos(cos)
+ }
+
+ /** Returns the distance between this vector and the [other] vector. */
+ @JvmStatic
+ public fun distance(vector1: Vector4, vector2: Vector4): Float = (vector1 - vector2).length
+
+ /**
+ * Returns a new vector that is linearly interpolated between [start] and [end] using the
+ * interpolation amount [ratio].
+ *
+ * If [ratio] is outside of the range `[0, 1]`, the returned vector will be extrapolated.
+ */
+ @JvmStatic
+ public fun lerp(start: Vector4, end: Vector4, ratio: Float): Vector4 =
+ Vector4(
+ lerp(start.x, end.x, ratio),
+ lerp(start.y, end.y, ratio),
+ lerp(start.z, end.z, ratio),
+ lerp(start.w, end.w, ratio),
+ )
+
+ /** Returns the minimum of each component of the two vectors. */
+ @JvmStatic
+ public fun min(a: Vector4, b: Vector4): Vector4 =
+ Vector4(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z), min(a.w, b.w))
+
+ /** Returns the maximum of each component of the two vectors. */
+ @JvmStatic
+ public fun max(a: Vector4, b: Vector4): Vector4 =
+ Vector4(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z), max(a.w, b.w))
+
+ /** Returns the absolute values of each component of the vector. */
+ @JvmStatic
+ public fun abs(vector: Vector4): Vector4 =
+ Vector4(abs(vector.x), abs(vector.y), abs(vector.z), abs(vector.w))
+ }
+}
diff --git a/xr/runtime/runtime/src/test/AndroidManifest.xml b/xr/runtime/runtime/src/test/AndroidManifest.xml
new file mode 100644
index 0000000..666fbed
--- /dev/null
+++ b/xr/runtime/runtime/src/test/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2024 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
+
+ http://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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <application>
+ <activity android:name="android.app.Activity" />
+ </application>
+</manifest>
diff --git a/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/SessionTest.kt b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/SessionTest.kt
new file mode 100644
index 0000000..3110bae
--- /dev/null
+++ b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/SessionTest.kt
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime
+
+import android.app.Activity
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.runtime.testing.FakeLifecycleManager
+import androidx.xr.runtime.testing.FakeStateExtender
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import kotlin.time.Duration.Companion.hours
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class SessionTest {
+ private lateinit var activity: Activity
+ private lateinit var testDispatcher: TestDispatcher
+ private lateinit var testScope: TestScope
+
+ @get:Rule val activityScenarioRule = ActivityScenarioRule<Activity>(Activity::class.java)
+
+ @Before
+ fun setUp() {
+ activityScenarioRule.scenario.onActivity { this.activity = it }
+ shadowOf(activity).grantPermissions(*Session.SESSION_PERMISSIONS.toTypedArray())
+
+ testDispatcher = StandardTestDispatcher()
+ testScope = TestScope(testDispatcher)
+ }
+
+ @Test
+ fun create_returnsSuccessResultWithNonNullSession() {
+ val result = Session.create(activity) as SessionCreateSuccess
+
+ assertThat(result.session).isNotNull()
+ }
+
+ @Test
+ fun create_setsLifecycleToInitialized() {
+ val underTest = (Session.create(activity) as SessionCreateSuccess).session
+
+ val lifecycleManager = underTest.runtime.lifecycleManager as FakeLifecycleManager
+ assertThat(lifecycleManager.state).isEqualTo(FakeLifecycleManager.State.INITIALIZED)
+ }
+
+ @Test
+ fun create_initializesStateExtender() {
+ val underTest = (Session.create(activity) as SessionCreateSuccess).session
+
+ // The FakeStateExtender is being loaded in Session here because it is defined as a class in
+ // the
+ // "//third_party/arcore/androidx/java/androidx/xr/testing" dependency.
+ val stateExtender = underTest.stateExtenders.first() as FakeStateExtender
+ assertThat(stateExtender.isInitialized).isTrue()
+ }
+
+ @Test
+ fun create_permissionNotGranted_returnsPermissionsNotGranted() {
+ val permission = "android.permission.SCENE_UNDERSTANDING"
+ shadowOf(activity).denyPermissions(permission)
+
+ val result = Session.create(activity) as SessionCreatePermissionsNotGranted
+
+ assertThat(result.permissions).containsExactly(permission)
+ }
+
+ @Test
+ fun configure_destroyed_throwsIllegalStateException() {
+ val underTest = (Session.create(activity) as SessionCreateSuccess).session
+ underTest.destroy()
+
+ assertFailsWith<IllegalStateException> { underTest.configure() }
+ }
+
+ // TODO(b/349855733): Add a test to verify configure() calls the corresponding LifecycleManager
+ // method once FakeRuntime supports it.
+
+ @Test
+ fun resume_returnsSuccessAndSetsLifecycleToResumed() {
+ val underTest = (Session.create(activity) as SessionCreateSuccess).session
+
+ val result = underTest.resume()
+
+ assertThat(result).isInstanceOf(SessionResumeSuccess::class.java)
+ val lifecycleManager = underTest.runtime.lifecycleManager as FakeLifecycleManager
+ assertThat(lifecycleManager.state).isEqualTo(FakeLifecycleManager.State.RESUMED)
+ }
+
+ @Test
+ fun resume_destroyed_throwsIllegalStateException() {
+ val underTest = (Session.create(activity) as SessionCreateSuccess).session
+ underTest.destroy()
+
+ assertFailsWith<IllegalStateException> { underTest.resume() }
+ }
+
+ @Test
+ fun resume_permissionNotGranted_returnsPermissionsNotGranted() {
+ val underTest = (Session.create(activity) as SessionCreateSuccess).session
+ val permission = "android.permission.SCENE_UNDERSTANDING"
+ shadowOf(activity).denyPermissions(permission)
+
+ val result = underTest.resume() as SessionResumePermissionsNotGranted
+
+ assertThat(result.permissions).containsExactly(permission)
+ }
+
+ // TODO(b/349859981): Add a test to verify update() calls the corresponding LifecycleManager
+ // method once FakeRuntime supports it.
+ @Test
+ fun update_emitsUpdatedState() =
+ runTest(testDispatcher) {
+ val underTest =
+ (Session.create(activity, testDispatcher) as SessionCreateSuccess).session
+ val lifecycleManager = underTest.runtime.lifecycleManager as FakeLifecycleManager
+ val timeSource = lifecycleManager.timeSource
+ val expectedDuration = 100.milliseconds
+
+ awaitNewCoreState(underTest, this)
+ val beforeTimeMark = underTest.state.value.timeMark
+ timeSource += expectedDuration
+ // By default FakeLifecycleManager will only allow one call to update() to go through.
+ // Since
+ // we are calling update() twice, we need to allow one more call to go through.
+ lifecycleManager.allowOneMoreCallToUpdate()
+ awaitNewCoreState(underTest, this)
+ val afterTimeMark = underTest.state.value.timeMark
+
+ val actualDuration = afterTimeMark - beforeTimeMark
+ assertThat(actualDuration).isEqualTo(expectedDuration)
+ }
+
+ @Test
+ fun update_extendsState() =
+ runTest(testDispatcher) {
+ val underTest =
+ (Session.create(activity, testDispatcher) as SessionCreateSuccess).session
+ val stateExtender = underTest.stateExtenders.first() as FakeStateExtender
+ check(stateExtender.extended.isEmpty())
+
+ awaitNewCoreState(underTest, this)
+
+ assertThat(stateExtender.extended).isNotEmpty()
+ }
+
+ @Test
+ fun pause_setsLifecycleToPaused() {
+ val underTest = (Session.create(activity) as SessionCreateSuccess).session
+ underTest.resume()
+
+ underTest.pause()
+
+ val lifecycleManager = underTest.runtime.lifecycleManager as FakeLifecycleManager
+ assertThat(lifecycleManager.state).isEqualTo(FakeLifecycleManager.State.PAUSED)
+ }
+
+ @Test
+ fun pause_destroyed_throwsIllegalStateException() {
+ val underTest = (Session.create(activity) as SessionCreateSuccess).session
+ underTest.destroy()
+
+ assertFailsWith<IllegalStateException> { underTest.pause() }
+ }
+
+ @Test
+ fun destroy_initialized_setsLifecycleToStopped() {
+ val underTest = (Session.create(activity) as SessionCreateSuccess).session
+
+ underTest.destroy()
+
+ val lifecycleManager = underTest.runtime.lifecycleManager as FakeLifecycleManager
+ assertThat(lifecycleManager.state).isEqualTo(FakeLifecycleManager.State.STOPPED)
+ }
+
+ @Test
+ fun destroy_resumed_setsLifecycleToStopped() {
+ val underTest = (Session.create(activity) as SessionCreateSuccess).session
+ underTest.resume()
+
+ underTest.destroy()
+
+ val lifecycleManager = underTest.runtime.lifecycleManager as FakeLifecycleManager
+ assertThat(lifecycleManager.state).isEqualTo(FakeLifecycleManager.State.STOPPED)
+ }
+
+ @Test
+ fun destroy_cancelsCoroutineScope() {
+ val underTest = (Session.create(activity) as SessionCreateSuccess).session
+ // Creating a job that will not finish by the time destroy is called.
+ val job = underTest.coroutineScope.launch { delay(12.hours) }
+
+ underTest.destroy()
+
+ // The job should be cancelled iff destroy was called and the coroutine scope was cancelled.
+ assertThat(job.isCancelled).isTrue()
+ }
+
+ /** Resumes and pauses the session just enough to emit a new CoreState. */
+ @OptIn(ExperimentalCoroutinesApi::class)
+ private suspend fun awaitNewCoreState(session: Session, testScope: TestScope) {
+ session.resume()
+ testScope.advanceUntilIdle()
+ session.pause()
+ }
+}
diff --git a/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/internal/HitResultTest.kt b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/internal/HitResultTest.kt
new file mode 100644
index 0000000..81b9187
--- /dev/null
+++ b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/internal/HitResultTest.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.internal
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.runtime.math.Pose
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class HitResultTest {
+
+ class TestTrackable : Trackable {
+ override fun createAnchor(pose: Pose): Anchor {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override val trackingState: TrackingState = TrackingState.Stopped
+ }
+
+ @Test
+ fun equals_sameObject_returnsTrue() {
+ val underTest = HitResult(1.0f, Pose(), TestTrackable())
+
+ assertThat(underTest.equals(underTest)).isTrue()
+ }
+
+ @Test
+ fun equals_differentObjectsSameValues_returnsTrue() {
+ val distance = 1.0f
+ val pose = Pose()
+ val trackable = TestTrackable()
+ val underTest1 = HitResult(distance, pose, trackable)
+ val underTest2 = HitResult(distance, pose, trackable)
+
+ assertThat(underTest1.equals(underTest2)).isTrue()
+ }
+
+ @Test
+ fun equals_differentObjectsDifferentValues_returnsFalse() {
+ val underTest1 = HitResult(1.0f, Pose(), TestTrackable())
+ val underTest2 = HitResult(2.0f, Pose(), TestTrackable())
+
+ assertThat(underTest1.equals(underTest2)).isFalse()
+ }
+
+ @Test
+ fun hashCode_differentObjectsSameValues_returnsSameHashCode() {
+ val distance = 1.0f
+ val pose = Pose()
+ val trackable = TestTrackable()
+ val underTest1 = HitResult(distance, pose, trackable)
+ val underTest2 = HitResult(distance, pose, trackable)
+
+ assertThat(underTest1.hashCode()).isEqualTo(underTest2.hashCode())
+ }
+
+ @Test
+ fun hashCode_differentObjectsDifferentValues_returnsDifferentHashCodes() {
+ val underTest1 = HitResult(1.0f, Pose(), TestTrackable())
+ val underTest2 = HitResult(2.0f, Pose(), TestTrackable())
+
+ assertThat(underTest1.hashCode()).isNotEqualTo(underTest2.hashCode())
+ }
+}
diff --git a/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/java/CoroutinesTest.kt b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/java/CoroutinesTest.kt
new file mode 100644
index 0000000..f97d743
--- /dev/null
+++ b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/java/CoroutinesTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.java
+
+import android.app.Activity
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.runtime.Session
+import androidx.xr.runtime.SessionCreateSuccess
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.hours
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.guava.future
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class CoroutinesTest {
+ private lateinit var activity: Activity
+ private lateinit var testDispatcher: TestDispatcher
+ private lateinit var testScope: TestScope
+
+ @get:Rule val activityScenarioRule = ActivityScenarioRule<Activity>(Activity::class.java)
+
+ @Before
+ fun setUp() {
+ activityScenarioRule.scenario.onActivity { this.activity = it }
+ shadowOf(activity).grantPermissions("android.permission.SCENE_UNDERSTANDING")
+
+ testDispatcher = StandardTestDispatcher()
+ testScope = TestScope(testDispatcher)
+ }
+
+ @Test
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun toFuture_cancelsFutureWhenSessionIsDestroyed() =
+ runTest(testDispatcher) {
+ val session = (Session.create(activity, testDispatcher) as SessionCreateSuccess).session
+ var isCoroutineComplete = false
+
+ val future =
+ toFuture(session) {
+ delay(1.hours)
+ isCoroutineComplete = true
+ }
+ session.destroy()
+ testScope.advanceUntilIdle()
+
+ assertThat(future.isCancelled).isTrue()
+ assertThat(future.isDone).isTrue()
+ // Verify that the coroutine was cancelled and did not actually complete.
+ assertThat(isCoroutineComplete).isFalse()
+ }
+}
diff --git a/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/java/FlowsTest.kt b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/java/FlowsTest.kt
new file mode 100644
index 0000000..8fbdb5f
--- /dev/null
+++ b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/java/FlowsTest.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.java
+
+import android.app.Activity
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.runtime.Session
+import androidx.xr.runtime.SessionCreateSuccess
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.hours
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class FlowsTest {
+ private lateinit var activity: Activity
+ private lateinit var testDispatcher: TestDispatcher
+ private lateinit var testScope: TestScope
+
+ @get:Rule val activityScenarioRule = ActivityScenarioRule<Activity>(Activity::class.java)
+
+ @Before
+ fun setUp() {
+ activityScenarioRule.scenario.onActivity { this.activity = it }
+ shadowOf(activity).grantPermissions("android.permission.SCENE_UNDERSTANDING")
+
+ testDispatcher = StandardTestDispatcher()
+ testScope = TestScope(testDispatcher)
+ }
+
+ @Test
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun toObservable_terminatesObservableWhenSessionIsDestroyed() =
+ runTest(testDispatcher) {
+ val session = (Session.create(activity, testDispatcher) as SessionCreateSuccess).session
+ var isTerminated = false
+
+ val observable =
+ toObservable(
+ session,
+ flow {
+ emit(1)
+ delay(1.hours)
+ emit(2)
+ },
+ )
+ .doOnTerminate() { isTerminated = true }
+ observable.subscribe()
+ session.destroy()
+ testScope.advanceUntilIdle()
+
+ assertThat(isTerminated).isTrue()
+ }
+}
diff --git a/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/FloatExtTest.kt b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/FloatExtTest.kt
new file mode 100644
index 0000000..3471cef
--- /dev/null
+++ b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/FloatExtTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.math
+
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.PI
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class FloatExtTest {
+ @Test
+ fun rsqrt_returnsReciprocalSqrt() {
+ assertThat(rsqrt(0.02f)).isWithin(1.0e-5f).of(7.0710683f)
+ assertThat(rsqrt(1f)).isWithin(1.0e-5f).of(1f)
+ assertThat(rsqrt(2f)).isWithin(1.0e-5f).of(0.70710677f)
+ assertThat(rsqrt(3f)).isWithin(1.0e-5f).of(0.57735026f)
+ }
+
+ @Test
+ fun clamp_returnsValueBetweenMinAndMax() {
+ assertThat(clamp(0f, 1f, 2f)).isEqualTo(1f)
+ assertThat(clamp(3f, 1f, 2f)).isEqualTo(2f)
+ assertThat(clamp(2f, 1f, 3f)).isEqualTo(2f)
+ }
+
+ @Test
+ fun lerpFloat_returnsInterpolatedValueBetweenStartAndEnd() {
+ assertThat(lerp(1f, 2f, 0f)).isEqualTo(1f)
+ assertThat(lerp(1f, 2f, 0.5f)).isEqualTo(1.5f)
+ assertThat(lerp(1f, 2f, 1f)).isEqualTo(2f)
+ }
+
+ @Test
+ fun toDegrees_returnsDegreesFromRadians() {
+ assertThat(toDegrees(PI.toFloat() / 1f)).isEqualTo(180f)
+ assertThat(toDegrees(PI.toFloat() / 2f)).isEqualTo(90f)
+ assertThat(toDegrees(PI.toFloat() / 3f)).isEqualTo(60f)
+ assertThat(toDegrees(PI.toFloat() / 4f)).isEqualTo(45f)
+ assertThat(toDegrees(PI.toFloat() / 6f)).isEqualTo(30f)
+ }
+
+ @Test
+ fun toRadians_returnsRadiansFromDegrees() {
+ assertThat(toRadians(180f)).isEqualTo(PI.toFloat() / 1f)
+ assertThat(toRadians(90f)).isEqualTo(PI.toFloat() / 2f)
+ assertThat(toRadians(60f)).isEqualTo(PI.toFloat() / 3f)
+ assertThat(toRadians(45f)).isEqualTo(PI.toFloat() / 4f)
+ assertThat(toRadians(30f)).isEqualTo(PI.toFloat() / 6f)
+ }
+}
diff --git a/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/Matrix4Test.kt b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/Matrix4Test.kt
new file mode 100644
index 0000000..18b9f57
--- /dev/null
+++ b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/Matrix4Test.kt
@@ -0,0 +1,801 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.math
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class Matrix4Test {
+ @Test
+ fun constructorEquals_expectedToString_returnsTrue() {
+ val underTest =
+ Matrix4(
+ floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 11f, 12f, 13f, 14f, 15f, 16f)
+ )
+
+ assertThat(underTest.toString())
+ .isEqualTo(
+ "\n[ " +
+ 1.0 +
+ "\t" +
+ 5.0 +
+ "\t" +
+ 9.0 +
+ "\t" +
+ 13.0 +
+ "\n " +
+ 2.0 +
+ "\t" +
+ 6.0 +
+ "\t" +
+ 10.0 +
+ "\t" +
+ 14.0 +
+ "\n " +
+ 3.0 +
+ "\t" +
+ 7.0 +
+ "\t" +
+ 11.0 +
+ "\t" +
+ 15.0 +
+ "\n " +
+ 4.0 +
+ "\t" +
+ 8.0 +
+ "\t" +
+ 12.0 +
+ "\t" +
+ 16.0 +
+ " ]"
+ )
+ }
+
+ @Test
+ fun constructor_fromMatrix4_returnsSameValues() {
+ val underTest =
+ Matrix4(
+ floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 11f, 12f, 13f, 14f, 15f, 16f)
+ )
+ val underTest2 = underTest
+
+ assertThat(underTest).isEqualTo(underTest2)
+ }
+
+ @Test
+ fun equals_sameValues_returnsTrue() {
+ val underTest =
+ Matrix4(
+ floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 11f, 12f, 13f, 14f, 15f, 16f)
+ )
+ val underTest2 =
+ Matrix4(
+ floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 11f, 12f, 13f, 14f, 15f, 16f)
+ )
+
+ assertThat(underTest).isEqualTo(underTest2)
+ }
+
+ @Test
+ fun equals_differentValues_returnsFalse() {
+ val underTest =
+ Matrix4(
+ floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 11f, 12f, 13f, 14f, 15f, 16f)
+ )
+ val underTest2 =
+ Matrix4(
+ floatArrayOf(
+ 9f,
+ 10f,
+ 11f,
+ 12f,
+ 13f,
+ 14f,
+ 15f,
+ 16f,
+ 17f,
+ 18f,
+ 19f,
+ 20f,
+ 21f,
+ 22f,
+ 23f,
+ 24f
+ )
+ )
+
+ assertThat(underTest).isNotEqualTo(underTest2)
+ }
+
+ @Test
+ fun equals_differentObjects_returnsFalse() {
+ val underTest =
+ Matrix4(
+ floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 11f, 12f, 13f, 14f, 15f, 16f)
+ )
+ val underTest2 = Vector3()
+
+ assertThat(underTest).isNotEqualTo(underTest2)
+ }
+
+ @Test
+ fun hashCodeEquals_sameValues_returnsTrue() {
+ val underTest =
+ Matrix4(
+ floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 11f, 12f, 13f, 14f, 15f, 16f)
+ )
+ val underTest2 =
+ Matrix4(
+ floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 11f, 12f, 13f, 14f, 15f, 16f)
+ )
+
+ assertThat(underTest.hashCode()).isEqualTo(underTest2.hashCode())
+ }
+
+ @Test
+ fun hashCodeEquals_differentValues_returnsFalse() {
+ val underTest =
+ Matrix4(
+ floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 11f, 12f, 13f, 14f, 15f, 16f)
+ )
+ val underTest2 =
+ Matrix4(
+ floatArrayOf(
+ 9f,
+ 10f,
+ 11f,
+ 12f,
+ 13f,
+ 14f,
+ 15f,
+ 16f,
+ 17f,
+ 18f,
+ 19f,
+ 20f,
+ 21f,
+ 22f,
+ 23f,
+ 24f
+ )
+ )
+
+ assertThat(underTest.hashCode()).isNotEqualTo(underTest2.hashCode())
+ }
+
+ @Test
+ fun hashCodeEquals_differentObjects_returnsFalse() {
+ val underTest =
+ Matrix4(
+ floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 11f, 12f, 13f, 14f, 15f, 16f)
+ )
+ val underTest2 = Vector3()
+
+ assertThat(underTest.hashCode()).isNotEqualTo(underTest2.hashCode())
+ }
+
+ @Test
+ fun inverse_returnsInverseMatrix1() {
+ val underTest =
+ Matrix4(floatArrayOf(1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f))
+ val expected =
+ Matrix4(floatArrayOf(1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f))
+ val underTestInverse = underTest.inverse
+
+ assertThat(underTestInverse.data).isEqualTo(expected.data)
+ }
+
+ @Test
+ fun inverse_returnsInverseMatrix2() {
+ val underTest =
+ Matrix4(floatArrayOf(1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 5f, -4f, 3f, 1f))
+ val expected =
+ Matrix4(floatArrayOf(1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, -5f, 4f, -3f, 1f))
+ val underTestInverse = underTest.inverse
+
+ assertThat(underTestInverse.data).isEqualTo(expected.data)
+ }
+
+ @Test
+ fun inverse_returnsInverseMatrix3() {
+ val underTest =
+ Matrix4(floatArrayOf(1f, 0f, 0f, 0f, 0f, 2f, 0f, 0f, 0f, 0f, 4f, 0f, 0f, 0f, 0f, 1f))
+ val expected =
+ Matrix4(
+ floatArrayOf(1f, 0f, 0f, 0f, 0f, 0.5f, 0f, 0f, 0f, 0f, 0.25f, 0f, 0f, 0f, 0f, 1f)
+ )
+ val underTestInverse = underTest.inverse
+
+ assertThat(underTestInverse.data).isEqualTo(expected.data)
+ }
+
+ @Test
+ fun transpose_returnsTransposeMatrix1() {
+ val underTest =
+ Matrix4(floatArrayOf(1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f))
+ val expected =
+ Matrix4(floatArrayOf(1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f))
+ val underTestTranspose = underTest.transpose
+
+ assertThat(underTestTranspose.data).isEqualTo(expected.data)
+ }
+
+ @Test
+ fun transpose_returnsTransposeMatrix2() {
+ val underTest =
+ Matrix4(
+ floatArrayOf(1f, 5f, 9f, 13f, 2f, 6f, 10f, 14f, 3f, 7f, 11f, 15f, 4f, 8f, 12f, 16f)
+ )
+ val expected =
+ Matrix4(
+ floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 11f, 12f, 13f, 14f, 15f, 16f)
+ )
+ val underTestTranspose = underTest.transpose
+
+ assertThat(underTestTranspose.data).isEqualTo(expected.data)
+ }
+
+ @Test
+ fun translation_returnsTranslationVector1() {
+ val underTest =
+ Matrix4(floatArrayOf(1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f))
+ val underTestTranslation = underTest.translation
+
+ assertThat(underTestTranslation).isEqualTo(Vector3(0f, 0f, 0f))
+ }
+
+ @Test
+ fun translation_returnsTranslationVector2() {
+ val underTest =
+ Matrix4(floatArrayOf(1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 5f, -4f, 3f, 1f))
+ val underTestTranslation = underTest.translation
+
+ assertThat(underTestTranslation).isEqualTo(Vector3(5f, -4f, 3f))
+ }
+
+ @Test
+ fun translation_returnsTranslationVector3() {
+ val underTest =
+ Matrix4(floatArrayOf(1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 2f, 0f, 1f))
+ val underTestTranslation = underTest.translation
+
+ assertThat(underTestTranslation).isEqualTo(Vector3(0f, 2f, 0f))
+ }
+
+ @Test
+ fun scale_returnsScaleVector1() {
+ val underTest =
+ Matrix4(floatArrayOf(1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f))
+ val underTestScale = underTest.scale
+
+ assertThat(underTestScale).isEqualTo(Vector3(1f, 1f, 1f))
+ }
+
+ @Test
+ fun scale_returnsScaleVector2() {
+ val underTest =
+ Matrix4(floatArrayOf(5f, 0f, 0f, 0f, 0f, -4f, 0f, 0f, 0f, 0f, 3f, 0f, 0f, 0f, 0f, 1f))
+ val underTestScale = underTest.scale
+
+ assertThat(underTestScale).isEqualTo(Vector3(5f, -4f, 3f))
+ }
+
+ @Test
+ fun scale_returnsScaleVector3() {
+ val underTest =
+ Matrix4(floatArrayOf(1f, 0f, 0f, 0f, 0f, 2f, 0f, 0f, 0f, 0f, 4f, 0f, 0f, 0f, 0f, 1f))
+ val underTestScale = underTest.scale
+
+ assertThat(underTestScale).isEqualTo(Vector3(1f, 2f, 4f))
+ }
+
+ @Test
+ fun scale_returnsScaleVector4() {
+ val underTest =
+ Matrix4(
+ floatArrayOf(
+ 0.707f,
+ 0.707f,
+ 0f,
+ 0f,
+ -0.707f,
+ 0.707f,
+ 0f,
+ 0f,
+ 0f,
+ 0f,
+ 1f,
+ 2f,
+ 5f,
+ 3f,
+ 1f,
+ 1f,
+ )
+ )
+ val underTestScale = underTest.scale
+
+ assertThat(underTestScale.x).isWithin(1e-3f).of(1f)
+ assertThat(underTestScale.y).isWithin(1e-3f).of(1f)
+ assertThat(underTestScale.z).isWithin(1e-3f).of(1f)
+ }
+
+ @Test
+ fun scale_returnsScaleVector5() {
+ val underTest =
+ Matrix4(
+ floatArrayOf(
+ 0.707f,
+ -0.5f,
+ 0.5f,
+ 0f,
+ 0.612f,
+ 0.707f,
+ 0.354f,
+ 0f,
+ -0.354f,
+ 0.5f,
+ -0.793f,
+ 0f,
+ 2.5f,
+ 1.8f,
+ 3.1f,
+ 1f,
+ )
+ )
+
+ val underTestScale = underTest.scale
+
+ assertThat(underTestScale.x).isWithin(1e-3f).of(1.0f)
+ assertThat(underTestScale.y).isWithin(1e-3f).of(1.0f)
+ assertThat(underTestScale.z).isWithin(1e-3f).of(-1.002f)
+ }
+
+ @Test
+ fun rotation_returnsRotationQuaternion1() {
+ val underTest =
+ Matrix4(floatArrayOf(0f, 1f, 0f, 0f, -1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f))
+ val underTestRotation = underTest.rotation
+
+ assertThat(underTestRotation.x).isWithin(1e-5f).of(0f)
+ assertThat(underTestRotation.y).isWithin(1e-5f).of(0f)
+ assertThat(underTestRotation.z).isWithin(1e-5f).of(0.70711f)
+ assertThat(underTestRotation.w).isWithin(1e-5f).of(0.70711f)
+ }
+
+ @Test
+ fun rotation_returnsRotationQuaternion2() {
+ val underTest =
+ Matrix4(floatArrayOf(5f, 1f, 3f, 0f, -1f, 6f, 0f, 0f, 0f, 0f, 1f, 3f, 1f, 0f, 0f, 1f))
+ val underTestRotation = underTest.rotation
+
+ assertThat(underTestRotation.x).isWithin(1e-5f).of(0f)
+ assertThat(underTestRotation.y).isWithin(1e-5f).of(-0.22237f)
+ assertThat(underTestRotation.z).isWithin(1e-5f).of(0.14825f)
+ assertThat(underTestRotation.w).isWithin(1e-5f).of(0.96362f)
+ }
+
+ @Test
+ fun rotation_returnsRotationQuaternion3() {
+ val underTest =
+ Matrix4(floatArrayOf(5f, 1f, 3f, 2f, -1f, 6f, 4f, 0f, 2f, 0f, 1f, 3f, 1f, 2f, 3f, 4f))
+ val underTestRotation = underTest.rotation
+
+ assertThat(underTestRotation.x).isWithin(1e-5f).of(0.29019f)
+ assertThat(underTestRotation.y).isWithin(1e-5f).of(-0.07254f)
+ assertThat(underTestRotation.z).isWithin(1e-5f).of(0.14509f)
+ assertThat(underTestRotation.w).isWithin(1e-5f).of(0.94312f)
+ }
+
+ @Test
+ fun pose_returnsTranslationRotationPose1() {
+ val underTest =
+ Matrix4(floatArrayOf(0f, 1f, 0f, 0f, -1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f))
+ val underTestPose = underTest.pose
+
+ assertThat(underTestPose.translation.x).isWithin(1e-5f).of(0f)
+ assertThat(underTestPose.translation.y).isWithin(1e-5f).of(0f)
+ assertThat(underTestPose.translation.z).isWithin(1e-5f).of(0f)
+ assertThat(underTestPose.rotation.x).isWithin(1e-5f).of(0f)
+ assertThat(underTestPose.rotation.y).isWithin(1e-5f).of(0f)
+ assertThat(underTestPose.rotation.z).isWithin(1e-5f).of(0.70711f)
+ assertThat(underTestPose.rotation.w).isWithin(1e-5f).of(0.70711f)
+ }
+
+ @Test
+ fun pose_returnsTranslationRotationPose2() {
+ val underTest =
+ Matrix4(floatArrayOf(5f, 1f, 3f, 0f, -1f, 6f, 0f, 0f, 0f, 0f, 1f, 3f, 1f, 0f, 0f, 1f))
+ val underTestPose = underTest.pose
+
+ assertThat(underTestPose.translation.x).isWithin(1e-5f).of(1f)
+ assertThat(underTestPose.translation.y).isWithin(1e-5f).of(0f)
+ assertThat(underTestPose.translation.z).isWithin(1e-5f).of(0f)
+ assertThat(underTestPose.rotation.x).isWithin(1e-5f).of(0f)
+ assertThat(underTestPose.rotation.y).isWithin(1e-5f).of(-0.22237f)
+ assertThat(underTestPose.rotation.z).isWithin(1e-5f).of(0.14825f)
+ assertThat(underTestPose.rotation.w).isWithin(1e-5f).of(0.96362f)
+ }
+
+ @Test
+ fun pose_returnsTranslationRotationPose3() {
+ val underTest =
+ Matrix4(floatArrayOf(5f, 1f, 3f, 2f, -1f, 6f, 4f, 0f, 2f, 0f, 1f, 3f, 1f, 2f, 3f, 4f))
+ val underTestPose = underTest.pose
+
+ assertThat(underTestPose.translation.x).isWithin(1e-5f).of(1f)
+ assertThat(underTestPose.translation.y).isWithin(1e-5f).of(2f)
+ assertThat(underTestPose.translation.z).isWithin(1e-5f).of(3f)
+ assertThat(underTestPose.rotation.x).isWithin(1e-5f).of(0.29019f)
+ assertThat(underTestPose.rotation.y).isWithin(1e-5f).of(-0.07255f)
+ assertThat(underTestPose.rotation.z).isWithin(1e-5f).of(0.14509f)
+ assertThat(underTestPose.rotation.w).isWithin(1e-5f).of(0.94312f)
+ }
+
+ @Test
+ fun multiply_returnsMultipliedMatrix1() {
+ val underTest =
+ Matrix4(floatArrayOf(2f, 0f, 5f, 0f, 3f, 4f, 5f, 0f, 4f, 6f, 1f, 4f, 0f, 5f, 5f, 0f))
+ val underTest2 =
+ Matrix4(floatArrayOf(1f, 0f, 7f, 5f, 0f, 3f, 2f, 2f, 6f, 5f, 4f, 0f, 2f, 0f, 4f, 0f))
+
+ assertThat(underTest * underTest2)
+ .isEqualTo(
+ Matrix4(
+ floatArrayOf(
+ 30f,
+ 67f,
+ 37f,
+ 28f,
+ 17f,
+ 34f,
+ 27f,
+ 8f,
+ 43f,
+ 44f,
+ 59f,
+ 16f,
+ 20f,
+ 24f,
+ 14f,
+ 16f,
+ )
+ )
+ )
+ }
+
+ @Test
+ fun multiply_returnsMultipliedMatrix2() {
+ val underTest =
+ Matrix4(floatArrayOf(1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f))
+ val underTest2 =
+ Matrix4(
+ floatArrayOf(1f, 5f, 9f, 13f, 2f, 6f, 10f, 14f, 3f, 7f, 11f, 15f, 4f, 8f, 12f, 16f)
+ )
+
+ assertThat(underTest * underTest2)
+ .isEqualTo(
+ Matrix4(
+ floatArrayOf(
+ 1f,
+ 5f,
+ 9f,
+ 13f,
+ 2f,
+ 6f,
+ 10f,
+ 14f,
+ 3f,
+ 7f,
+ 11f,
+ 15f,
+ 4f,
+ 8f,
+ 12f,
+ 16f
+ )
+ )
+ )
+ }
+
+ @Test
+ fun multiply_returnsMultipliedMatrix3() {
+ val underTest =
+ Matrix4(floatArrayOf(1f, 3f, 1f, 3f, 2f, 3f, 2f, 3f, 3f, 3f, 3f, 3f, 4f, 3f, 4f, 3f))
+ val underTest2 =
+ Matrix4(floatArrayOf(1f, 2f, 1f, 2f, 2f, 2f, 2f, 2f, 3f, 2f, 3f, 2f, 4f, 2f, 4f, 2f))
+
+ assertThat(underTest * underTest2)
+ .isEqualTo(
+ Matrix4(
+ floatArrayOf(
+ 16f,
+ 18f,
+ 16f,
+ 18f,
+ 20f,
+ 24f,
+ 20f,
+ 24f,
+ 24f,
+ 30f,
+ 24f,
+ 30f,
+ 28f,
+ 36f,
+ 28f,
+ 36f,
+ )
+ )
+ )
+ }
+
+ @Test
+ fun copy_returnsCopyOfMatrix() {
+ val underTest =
+ Matrix4(
+ floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 11f, 12f, 13f, 14f, 15f, 16f)
+ )
+ val underTest2 = underTest.copy()
+
+ assertThat(underTest2).isEqualTo(underTest)
+ }
+
+ @Test
+ fun isTrs_returnsTrueIfTransformationMatrixIsValid() {
+ assertThat(
+ Matrix4(
+ floatArrayOf(1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f)
+ )
+ .isTrs
+ )
+ .isTrue()
+ }
+
+ @Test
+ fun isTrs_returnsFalseIfTransformationMatrixIsNotValid() {
+ assertThat(
+ Matrix4(
+ floatArrayOf(0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f)
+ )
+ .isTrs
+ )
+ .isFalse()
+ }
+
+ @Test
+ fun fromTrs_returnsNewTransformationMatrix1() {
+ val underTest =
+ Matrix4.fromTrs(Vector3(0f, 0f, 0f), Quaternion(0f, 0f, 0f, 1f), Vector3(1f, 1f, 1f))
+ val expected =
+ Matrix4(floatArrayOf(1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f))
+
+ assertMatrix(underTest, expected)
+ }
+
+ @Test
+ fun fromTrs_returnsNewTransformationMatrix2() {
+ val underTest =
+ Matrix4.fromTrs(
+ Vector3(0f, 0f, 0f),
+ Quaternion(0f, 0f, 0.70710678f, 0.70710678f),
+ Vector3(1f, 1f, 1f),
+ )
+ val expected =
+ Matrix4(floatArrayOf(0f, 1f, 0f, 0f, -1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f))
+
+ assertMatrix(underTest, expected)
+ }
+
+ @Test
+ fun fromTrs_returnsNewTransformationMatrix3() {
+ val underTest =
+ Matrix4.fromTrs(Vector3(0f, 0f, 0f), Quaternion(0f, 0f, 0f, 1f), Vector3(2f, 0.5f, 3f))
+ val expected =
+ Matrix4(floatArrayOf(2f, 0f, 0f, 0f, 0f, 0.5f, 0f, 0f, 0f, 0f, 3f, 0f, 0f, 0f, 0f, 1f))
+
+ assertMatrix(underTest, expected)
+ }
+
+ @Test
+ fun fromTranslation_returnsNewTranslationMatrix1() {
+ val underTest = Matrix4.fromTranslation(Vector3(2f, 3f, 4f))
+
+ assertThat(underTest.data)
+ .isEqualTo(floatArrayOf(1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 2f, 3f, 4f, 1f))
+ }
+
+ @Test
+ fun fromTranslation_returnsNewTranslationMatrix2() {
+ val underTest = Matrix4.fromTranslation(Vector3(-2f, -3f, -4f))
+
+ assertThat(underTest.data)
+ .isEqualTo(
+ floatArrayOf(1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, -2f, -3f, -4f, 1f)
+ )
+ }
+
+ @Test
+ fun fromScale_returnsNewScaleMatrix1() {
+ val underTest = Matrix4.fromScale(Vector3(2f, 3f, 4f))
+
+ assertThat(underTest.data)
+ .isEqualTo(floatArrayOf(2f, 0f, 0f, 0f, 0f, 3f, 0f, 0f, 0f, 0f, 4f, 0f, 0f, 0f, 0f, 1f))
+ }
+
+ @Test
+ fun fromScale_returnsNewScaleMatrix2() {
+ val underTest = Matrix4.fromScale(Vector3(-2f, -3f, -4f))
+
+ assertThat(underTest.data)
+ .isEqualTo(
+ floatArrayOf(-2f, 0f, 0f, 0f, 0f, -3f, 0f, 0f, 0f, 0f, -4f, 0f, 0f, 0f, 0f, 1f)
+ )
+ }
+
+ @Test
+ fun fromScaleFloat_returnsNewScaleMatrix1() {
+ val underTest = Matrix4.fromScale(2f)
+
+ assertThat(underTest.data)
+ .isEqualTo(floatArrayOf(2f, 0f, 0f, 0f, 0f, 2f, 0f, 0f, 0f, 0f, 2f, 0f, 0f, 0f, 0f, 1f))
+ }
+
+ @Test
+ fun fromScaleFloat_returnsNewScaleMatrix2() {
+ val underTest = Matrix4.fromScale(-2f)
+
+ assertThat(underTest.data)
+ .isEqualTo(
+ floatArrayOf(-2f, 0f, 0f, 0f, 0f, -2f, 0f, 0f, 0f, 0f, -2f, 0f, 0f, 0f, 0f, 1f)
+ )
+ }
+
+ @Test
+ fun fromQuaternion_returnsNewRotationMatrix1() {
+ val underTest = Matrix4.fromQuaternion(Quaternion(0f, 0.7071f, 0f, 0.7071f))
+ val expected =
+ Matrix4(floatArrayOf(0f, 0f, -1f, 0f, 0f, 1f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 0f, 1f))
+
+ assertMatrix(underTest, expected)
+ }
+
+ @Test
+ fun fromQuaternion_returnsNewRotationMatrix2() {
+ val underTest = Matrix4.fromQuaternion(Quaternion(0.7071f, 0f, 0.7071f, 0f))
+ val expected =
+ Matrix4(floatArrayOf(0f, 0f, 1f, 0f, 0f, -1f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 0f, 1f))
+
+ assertMatrix(underTest, expected)
+ }
+
+ @Test
+ fun fromQuaternion_returnsNewRotationMatrix3() {
+ val underTest = Matrix4.fromQuaternion(Quaternion(0f, -0.38f, 0.92f, 0f))
+ val expected =
+ Matrix4(
+ floatArrayOf(
+ -1f,
+ 0f,
+ 0f,
+ 0f,
+ 0f,
+ -0.70852f,
+ -0.70569f,
+ 0f,
+ 0f,
+ -0.70569f,
+ 0.70852f,
+ 0f,
+ 0f,
+ 0f,
+ 0f,
+ 1f,
+ )
+ )
+
+ assertMatrix(underTest, expected)
+ }
+
+ @Test
+ fun fromPose_returnsNewTransformationMatrix1() {
+ val underTest =
+ Matrix4.fromPose(Pose(Vector3(0f, 0f, 0f), Quaternion(0f, -0.38f, 0.92f, 0f)))
+ val expected =
+ Matrix4(
+ floatArrayOf(
+ -1f,
+ 0f,
+ 0f,
+ 0f,
+ 0f,
+ -0.70852f,
+ -0.70569f,
+ 0f,
+ 0f,
+ -0.70569f,
+ 0.70852f,
+ 0f,
+ 0f,
+ 0f,
+ 0f,
+ 1f,
+ )
+ )
+
+ assertMatrix(underTest, expected)
+ }
+
+ @Test
+ fun fromPose_returnsNewTransformationMatrix2() {
+ val underTest = Matrix4.fromPose(Pose(Vector3(2f, 3f, 4f), Quaternion(0f, 0f, 0f, 1f)))
+ val expected =
+ Matrix4(floatArrayOf(1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 2f, 3f, 4f, 1f))
+
+ assertMatrix(underTest, expected)
+ }
+
+ @Test
+ fun fromPose_returnsNewTransformationMatrix3() {
+ val underTest =
+ Matrix4.fromPose(Pose(Vector3(5f, 6f, 7f), Quaternion(0f, 0.7071f, 0f, 0.7071f)))
+ val expected =
+ Matrix4(floatArrayOf(0f, 0f, -1f, 0f, 0f, 1f, 0f, 0f, 1f, 0f, 0f, 0f, 5f, 6f, 7f, 1f))
+
+ assertMatrix(underTest, expected)
+ }
+
+ private fun assertMatrix(matrix: Matrix4, expected: Matrix4) {
+ assertThat(matrix.data.size).isEqualTo(expected.data.size)
+ for (i in matrix.data.indices) {
+ assertThat(matrix.data[i]).isWithin(1e-5f).of(expected.data[i])
+ }
+ }
+
+ @Test
+ fun data_returnsFloatArrayOfMatrixComponents() {
+ val underTest =
+ Matrix4(
+ floatArrayOf(
+ 1f,
+ 2f,
+ 3f,
+ 4f,
+ 5f,
+ 6f,
+ 7f,
+ 8f,
+ 9f,
+ 10f,
+ 11f,
+ 12f,
+ 13f,
+ 14f,
+ 15f,
+ 16f
+ )
+ )
+ .data
+
+ assertThat(underTest)
+ .isEqualTo(
+ floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 11f, 12f, 13f, 14f, 15f, 16f)
+ )
+ }
+}
diff --git a/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/PoseTest.kt b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/PoseTest.kt
new file mode 100644
index 0000000..bd9f755
--- /dev/null
+++ b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/PoseTest.kt
@@ -0,0 +1,386 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.math
+
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.sqrt
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class PoseTest {
+
+ @Test
+ fun constructor_noArguments_returnsZeroVectorAndIdentityQuaternion() {
+ val underTest = Pose()
+
+ assertThat(underTest.translation).isEqualTo(Vector3(0f, 0f, 0f))
+ assertThat(underTest.rotation).isEqualTo(Quaternion(0f, 0f, 0f, 1f))
+ }
+
+ @Test
+ fun equals_sameValues_returnsTrue() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(4f, 5f, 6f, 7f))
+ val underTest2 =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(4f, 5f, 6f, 7f))
+
+ assertThat(underTest).isEqualTo(underTest2)
+ }
+
+ @Test
+ fun equals_differentValues_returnsFalse() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(4f, 5f, 6f, 7f))
+ val underTest2 =
+ Pose(translation = Vector3(9f, 10f, 11f), rotation = Quaternion(4f, 5f, 6f, 7f))
+ val underTest3 = Vector3()
+
+ assertThat(underTest).isNotEqualTo(underTest2)
+ assertThat(underTest).isNotEqualTo(underTest3)
+ }
+
+ @Test
+ fun hashCodeEquals_sameValues_returnsTrue() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(4f, 5f, 6f, 7f))
+ val underTest2 =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(4f, 5f, 6f, 7f))
+
+ assertThat(underTest.hashCode()).isEqualTo(underTest2.hashCode())
+ }
+
+ @Test
+ fun hashCodeEquals_differentValues_returnsFalse() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(4f, 5f, 6f, 7f))
+ val underTest2 =
+ Pose(translation = Vector3(9f, 10f, 11f), rotation = Quaternion(4f, 5f, 6f, 7f))
+ val underTest3 = Vector3()
+
+ assertThat(underTest.hashCode()).isNotEqualTo(underTest2.hashCode())
+ assertThat(underTest.hashCode()).isNotEqualTo(underTest3.hashCode())
+ }
+
+ @Test
+ fun constructorEquals_expectedToString_returnsTrue() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(4f, 5f, 6f, 7f))
+ val underTest2 = Pose()
+
+ assertThat(underTest.toString())
+ .isEqualTo(
+ "Pose{\n\tTranslation=[x=1.0, y=2.0, z=3.0]\n\tRotation=[x=0.35634834, y=0.4454354, z=0.5345225, w=0.6236096]\n}"
+ )
+ assertThat(underTest2.toString())
+ .isEqualTo(
+ "Pose{\n\tTranslation=[x=0.0, y=0.0, z=0.0]\n\tRotation=[x=0.0, y=0.0, z=0.0, w=1.0]\n}"
+ )
+ }
+
+ @Test
+ fun distance_returnsLengthOfVectorBetweenTranslations() {
+ val underTest = Pose(translation = Vector3(0F, 3f, 4F), rotation = Quaternion())
+
+ // (0, 3, 4) - (0, 0, 0) = (0, 3, 4) -> sqrt(0^2 + 3^2 + 4^2) = sqrt(25)
+ assertThat(Pose.distance(underTest, Pose())).isEqualTo(5F)
+ }
+
+ @Test
+ fun constructor_fromPose_returnsSameValues() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(4f, 5f, 6f, 7f))
+ val underTest2 = Pose(underTest)
+
+ assertThat(underTest).isEqualTo(underTest2)
+ }
+
+ @Test
+ fun compose_returnsPoseWithTranslationAndRotation() {
+ val underTest =
+ Pose(
+ translation = Vector3(3f, 0f, 0f),
+ rotation = Quaternion(0f, sqrt(2f) / 2, 0f, sqrt(2f) / 2),
+ )
+ val underTest2 =
+ Pose(
+ translation = Vector3(0f, 0f, -3f),
+ rotation = Quaternion(0f, sqrt(2f) / 2, 0f, sqrt(2f) / 2),
+ )
+
+ val underTestCompose = underTest.compose(underTest2)
+
+ assertTranslation(underTestCompose.translation, 0f, 0f, 0f)
+ assertRotation(underTestCompose.rotation, 0f, 1f, 0f, 0f)
+ }
+
+ @Test
+ fun translate_withZeroVector3_returnsSamePose() {
+ val underTest = Pose(translation = Vector3(1f, 2f, 3f))
+ val translation = Vector3.Zero
+
+ val translatedPose = underTest.translate(translation)
+
+ assertTranslation(translatedPose.translation, 1f, 2f, 3f)
+ assertThat(translatedPose.rotation).isEqualTo(underTest.rotation)
+ }
+
+ @Test
+ fun translate_withVector3_returnsTranslatedPose() {
+ val underTest = Pose(translation = Vector3(1f, 2f, 3f))
+ val translation = Vector3(1f, 1f, 1f)
+
+ val translatedPose = underTest.translate(translation)
+
+ assertTranslation(translatedPose.translation, 2f, 3f, 4f)
+ assertThat(translatedPose.rotation).isEqualTo(underTest.rotation)
+ }
+
+ @Test
+ fun rotate_withIdentityQuaternion_returnsSamePose() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(1f, 2f, 3f, 4f))
+ val rotation = Quaternion.Identity
+
+ val rotatedPose = underTest.rotate(rotation)
+
+ assertTranslation(rotatedPose.translation, 1f, 2f, 3f)
+ assertRotation(
+ rotatedPose.rotation,
+ underTest.rotation.x,
+ underTest.rotation.y,
+ underTest.rotation.z,
+ underTest.rotation.w,
+ )
+ }
+
+ @Test
+ fun rotate_withQuaternion_returnsRotatedPose() {
+ val underTest = Pose(rotation = Quaternion.Identity)
+ val rotation = Quaternion.fromAxisAngle(Vector3.Forward, 180f)
+
+ val rotatedPose = underTest.rotate(rotation)
+
+ assertThat(rotatedPose.translation).isEqualTo(underTest.translation)
+ assertRotation(rotatedPose.rotation, rotation.x, rotation.y, rotation.z, rotation.w)
+ }
+
+ @Test
+ fun rotate_withNonIdentityQuaternion_returnsRotatedPose() {
+ // A pose with a rotation of 45 degrees around the Y-axis.
+ val underTest = Pose(rotation = Quaternion(0f, sqrt(2f) / 2, 0f, sqrt(2f) / 2))
+ // A Quaternion representing a 45-degree rotation around the Y-axis.
+ val rotation = Quaternion(0f, sqrt(2f) / 2, 0f, sqrt(2f) / 2)
+
+ val rotatedPose = underTest.rotate(rotation)
+
+ assertThat(rotatedPose.translation).isEqualTo(underTest.translation)
+ // The rotation of the rotated Pose is equal to a 90-degree rotation around the Y-axis.
+ assertRotation(rotatedPose.rotation, 0f, 1f, 0f, 0f)
+ }
+
+ @Test
+ fun inverse_returnsPoseWithOppositeTransformation() {
+ val underTest =
+ Pose(
+ translation = Vector3(3f, 0f, 0f),
+ rotation = Quaternion(0f, sqrt(2f) / 2, 0f, sqrt(2f) / 2),
+ )
+
+ val underTestInverted = underTest.inverse
+
+ assertTranslation(underTestInverted.translation, 0f, 0f, -3f)
+ assertRotation(underTestInverted.rotation, 0f, -sqrt(2f) / 2, 0f, sqrt(2f) / 2)
+ }
+
+ @Test
+ fun transform_returnsTransformedPointByPose() {
+ val underTest =
+ Pose(translation = Vector3(0f, 0f, 1f), rotation = Quaternion(0f, 0.7071f, 0f, 0.7071f))
+ val point = Vector3(0f, 0f, 1f)
+
+ val transformedPoint = underTest.transformPoint(point)
+
+ assertTranslation(transformedPoint, 1f, 0f, 1f)
+ }
+
+ @Test
+ fun lerp_returnsInterpolatedPose() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(4f, 5f, 6f, 7f))
+ val underTest2 =
+ Pose(translation = Vector3(4f, 5f, 6f), rotation = Quaternion(8f, 9f, 10f, 11f))
+
+ val interpolatedPose = Pose.lerp(underTest, underTest2, 0.5f)
+
+ assertTranslation(interpolatedPose.translation, 2.5f, 3.5f, 4.5f)
+ assertRotation(interpolatedPose.rotation, 0.38759f, 0.45833f, 0.52907f, 0.59981f)
+ }
+
+ @Test
+ fun up_returnsUpVectorInLocalCoordinateSystem1() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(0f, 0.7071f, 0f, 0.7071f))
+
+ assertTranslation(underTest.up, 0f, 1f, 0f)
+ }
+
+ @Test
+ fun up_returnsUpVectorInLocalCoordinateSystem2() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(0.7071f, 0f, 0f, 0.7071f))
+
+ assertTranslation(underTest.up, 0f, 0f, 1f)
+ }
+
+ @Test
+ fun down_returnsDownVectorInLocalCoordinateSystem1() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(0f, 0.7071f, 0f, 0.7071f))
+
+ assertTranslation(underTest.down, 0f, -1f, 0f)
+ }
+
+ @Test
+ fun down_returnsDownVectorInLocalCoordinateSystem2() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(0.7071f, 0f, 0f, 0.7071f))
+
+ assertTranslation(underTest.down, 0f, 0f, -1f)
+ }
+
+ @Test
+ fun left_returnsLeftVectorInLocalCoordinateSystem1() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(0.7071f, 0f, 0f, 0.7071f))
+
+ assertTranslation(underTest.left, -1f, 0f, 0f)
+ }
+
+ @Test
+ fun left_returnsLeftVectorInLocalCoordinateSystem2() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(0f, 0.7071f, 0f, 0.7071f))
+
+ assertTranslation(underTest.left, 0f, 0f, 1f)
+ }
+
+ @Test
+ fun right_returnsRightVectorInLocalCoordinateSystem1() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(0.7071f, 0f, 0f, 0.7071f))
+
+ assertTranslation(underTest.right, 1f, 0f, 0f)
+ }
+
+ @Test
+ fun right_returnsRightVectorInLocalCoordinateSystem2() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(0f, 0.7071f, 0f, 0.7071f))
+
+ assertTranslation(underTest.right, 0f, 0f, -1f)
+ }
+
+ @Test
+ fun forward_returnsForwardVectorInLocalCoordinateSystem1() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(0f, 0f, 0.7071f, 0.7071f))
+
+ assertTranslation(underTest.forward, 0f, 0f, -1f)
+ }
+
+ @Test
+ fun forward_returnsForwardVectorInLocalCoordinateSystem2() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(0.7071f, 0f, 0f, 0.7071f))
+
+ assertTranslation(underTest.forward, 0f, 1f, 0f)
+ }
+
+ @Test
+ fun backward_returnsBackwardVectorInLocalCoordinateSystem1() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(0f, 0f, 0.7071f, 0.7071f))
+
+ assertTranslation(underTest.backward, 0f, 0f, 1f)
+ }
+
+ @Test
+ fun backward_returnsBackwardVectorInLocalCoordinateSystem2() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(0.7071f, 0f, 0f, 0.7071f))
+
+ assertTranslation(underTest.backward, 0f, -1f, 0f)
+ }
+
+ @Test
+ fun transformVector_returnsVectorTransformedByPose() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(1f, 2f, 3f, 4f))
+ val vector = Vector3(1f, 2f, 3f)
+ val underTestRotated = underTest.transformVector(vector)
+
+ assertTranslation(underTestRotated, 1f, 2f, 3f)
+ }
+
+ @Test
+ fun copy_returnsCopyOfPose() {
+ val underTest =
+ Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(4f, 5f, 6f, 7f))
+ val underTest2 = underTest.copy()
+
+ assertThat(underTest).isEqualTo(underTest2)
+ }
+
+ @Test
+ fun fromLookAt_returnsPoseLookingAtTarget() {
+ val underTest =
+ Pose.fromLookAt(
+ eye = Vector3.Zero,
+ target = Vector3(0f, 0f, 10f),
+ up = Vector3(0f, 1f, 0f)
+ )
+
+ assertTranslation(underTest.translation, 0f, 0f, 0f)
+ assertRotation(underTest.rotation, 0f, 0f, 0f, 1f)
+ }
+
+ private fun assertTranslation(
+ translation: Vector3,
+ expectedX: Float,
+ expectedY: Float,
+ expectedZ: Float,
+ ) {
+ assertThat(translation.x).isWithin(1.0e-4f).of(expectedX)
+ assertThat(translation.y).isWithin(1.0e-4f).of(expectedY)
+ assertThat(translation.z).isWithin(1.0e-4f).of(expectedZ)
+ }
+
+ private fun assertRotation(
+ rotation: Quaternion,
+ expectedX: Float,
+ expectedY: Float,
+ expectedZ: Float,
+ expectedW: Float,
+ ) {
+ assertThat(rotation.x).isWithin(1.0e-4f).of(expectedX)
+ assertThat(rotation.y).isWithin(1.0e-4f).of(expectedY)
+ assertThat(rotation.z).isWithin(1.0e-4f).of(expectedZ)
+ assertThat(rotation.w).isWithin(1.0e-4f).of(expectedW)
+ }
+}
diff --git a/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/QuaternionTest.kt b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/QuaternionTest.kt
new file mode 100644
index 0000000..660cc98
--- /dev/null
+++ b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/QuaternionTest.kt
@@ -0,0 +1,498 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.math
+
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.sqrt
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class QuaternionTest {
+
+ @Test
+ fun constructor_noArguments_returnsIdentityQuaternion() {
+ val underTest = Quaternion()
+ assertRotation(underTest, 0f, 0f, 0f, 1f)
+
+ val underTest2 = Quaternion.Identity
+ assertRotation(underTest2, 0f, 0f, 0f, 1f)
+ }
+
+ @Test
+ fun equals_sameValues_returnsTrue() {
+ val underTest = Quaternion(1f, 2f, 3f, 4f)
+ val underTest2 = Quaternion(1f, 2f, 3f, 4f)
+
+ assertThat(underTest).isEqualTo(underTest2)
+ }
+
+ @Test
+ fun equals_differentValues_returnsFalse() {
+ val underTest = Quaternion(1f, 2f, 3f, 4f)
+ val underTest2 = Quaternion(9f, 10f, 11f, 12f)
+ val underTest3 = Vector2()
+
+ assertThat(underTest).isNotEqualTo(underTest2)
+ assertThat(underTest).isNotEqualTo(underTest3)
+ }
+
+ @Test
+ fun hashCodeEquals_sameValues_returnsTrue() {
+ val underTest = Quaternion(1f, 2f, 3f, 4f)
+ val underTest2 = Quaternion(1f, 2f, 3f, 4f)
+
+ assertThat(underTest.hashCode()).isEqualTo(underTest2.hashCode())
+ }
+
+ @Test
+ fun hashCodeEquals_differentValues_returnsFalse() {
+ val underTest = Quaternion(1f, 2f, 3f, 4f)
+ val underTest2 = Quaternion(9f, 10f, 11f, 12f)
+ val underTest3 = Quaternion()
+
+ assertThat(underTest.hashCode()).isNotEqualTo(underTest2.hashCode())
+ assertThat(underTest.hashCode()).isNotEqualTo(underTest3.hashCode())
+ }
+
+ @Test
+ fun constructorEquals_expectedToString_returnsTrue() {
+ val underTest = Quaternion(0f, 1f, 2f, 0f)
+ val underTest2 = Quaternion()
+
+ assertThat(underTest.toString()).isEqualTo("[x=0.0, y=0.4472136, z=0.8944272, w=0.0]")
+ assertThat(underTest2.toString()).isEqualTo("[x=0.0, y=0.0, z=0.0, w=1.0]")
+ }
+
+ @Test
+ fun constructor_fromQuaternion_returnsSameValues() {
+ val underTest = Quaternion(1f, 2f, 3f, 4f)
+ val underTest2 = Quaternion(underTest)
+
+ assertRotation(underTest, underTest2.x, underTest2.y, underTest2.z, underTest2.w)
+ }
+
+ @Test
+ fun constructor_fromVector4_returnsSameValues() {
+ val underTest = Vector4(1f, 2f, 3f, 4f)
+ val underTest2 = Quaternion(underTest)
+ val length =
+ sqrt(
+ underTest.x * underTest.x +
+ underTest.y * underTest.y +
+ underTest.z * underTest.z +
+ underTest.w * underTest.w
+ )
+
+ assertRotation(
+ underTest2,
+ underTest.x / length,
+ underTest.y / length,
+ underTest.z / length,
+ underTest.w / length,
+ )
+ }
+
+ @Test
+ fun axisAngle_returnsQuaternionFromAxis() {
+ val underTestAxis = Vector3(1f, 0f, 0f)
+ val degrees = 180f
+
+ // angle = 1/2 * 180 * pi / 180 = 1/2 * pi
+ // sin(angle) = sin(1/2 pi) = 1
+ // cos(angle) = cos(1/2 pi) = 0
+ // returns Quaternion(1 * 1, 1 * 0, 1 * 0, 0)
+ val underTestAxisAngle = Quaternion.fromAxisAngle(underTestAxis, degrees)
+
+ assertRotation(underTestAxisAngle, 1f, 0f, 0f, 0f)
+ }
+
+ @Test
+ fun quaternion_fromEulerAngles1() {
+ val eulerAngles = Vector3(180f, 0f, 0f)
+ val underTestYawPitchRoll = Quaternion.fromEulerAngles(eulerAngles)
+
+ assertRotation(underTestYawPitchRoll, 1f, 0f, 0f, 0f)
+ }
+
+ @Test
+ fun quaternion_fromEulerAngles2() {
+ val eulerAngles = Vector3(0f, 0f, 0f)
+ val underTestYawPitchRoll = Quaternion.fromEulerAngles(eulerAngles)
+
+ assertRotation(underTestYawPitchRoll, 0f, 0f, 0f, 1f)
+ }
+
+ @Test
+ fun quaternion_fromEulerAngles3() {
+ val eulerAngles = Vector3(90f, 0f, 0f)
+ val underTestYawPitchRoll = Quaternion.fromEulerAngles(eulerAngles)
+
+ assertRotation(underTestYawPitchRoll, 0.7071f, 0f, 0f, 0.7071f)
+ }
+
+ @Test
+ fun quaternion_fromEulerAngles4() {
+ val eulerAngles = Vector3(0f, 180f, 0f)
+ val underTestYawPitchRoll = Quaternion.fromEulerAngles(eulerAngles)
+
+ assertRotation(underTestYawPitchRoll, 0f, 1f, 0f, 0f)
+ }
+
+ @Test
+ fun quaternion_fromPitchYawRoll1() {
+ val underTestYawPitchRoll = Quaternion.fromEulerAngles(180f, 0f, 0f)
+
+ assertRotation(underTestYawPitchRoll, 1f, 0f, 0f, 0f)
+ }
+
+ @Test
+ fun quaternion_fromPitchYawRoll2() {
+ val underTestYawPitchRoll = Quaternion.fromEulerAngles(0f, 0f, 0f)
+
+ assertRotation(underTestYawPitchRoll, 0f, 0f, 0f, 1f)
+ }
+
+ @Test
+ fun quaternion_fromPitchYawRoll3() {
+ val underTestYawPitchRoll = Quaternion.fromEulerAngles(90f, 0f, 0f)
+
+ assertRotation(underTestYawPitchRoll, 0.7071f, 0f, 0f, 0.7071f)
+ }
+
+ @Test
+ fun quaternion_fromPitchYawRoll4() {
+ val underTestYawPitchRoll = Quaternion.fromEulerAngles(0f, 180f, 0f)
+
+ assertRotation(underTestYawPitchRoll, 0f, 1f, 0f, 0f)
+ }
+
+ @Test
+ fun normalized_returnsQuaternionNormalized() {
+ val underTest = Quaternion(1f, 2f, 3f, 4f)
+ val underTestNormalized = underTest.toNormalized()
+
+ assertRotation(
+ underTest,
+ underTestNormalized.x,
+ underTestNormalized.y,
+ underTestNormalized.z,
+ underTestNormalized.w,
+ )
+ }
+
+ @Test
+ fun inverted_returnsQuaternionInverted() {
+ val underTest = Quaternion(1f, 2f, 3f, 4f)
+ val underTestInverted = underTest.inverse
+
+ assertRotation(
+ underTestInverted,
+ -1 * underTest.x,
+ -1 * underTest.y,
+ -1 * underTest.z,
+ 1 * underTest.w,
+ )
+ }
+
+ @Test
+ fun unaryMinus_returnsQuaternionWithSignsFlipped() {
+ val underTest = Quaternion(1f, -2f, 3f, -4f)
+ val underTestUnary = -underTest
+
+ assertRotation(
+ underTestUnary,
+ -1 * underTest.x,
+ -1 * underTest.y,
+ -1 * underTest.z,
+ -1 * underTest.w,
+ )
+ }
+
+ @Test
+ fun rotateVector_returnsVector3RotatedByQuaternion() {
+ val underTest = Quaternion(1f, 2f, 3f, 4f)
+ val vector = Vector3(1f, 2f, 3f)
+ val underTestRotated = underTest * vector
+
+ assertThat(underTestRotated.x).isWithin(1e-5f).of(1f)
+ assertThat(underTestRotated.y).isWithin(1e-5f).of(2f)
+ assertThat(underTestRotated.z).isWithin(1e-5f).of(3f)
+ }
+
+ @Test
+ fun times_returnsTwoQuaternionsMultiplied() {
+ val underTest = Quaternion(1f, 2f, 3f, 4f)
+ val underTest2 = Quaternion(5f, -6f, 7f, -8f)
+ val underTestTimes = underTest * underTest2
+
+ assertRotation(underTestTimes, 0.6090002f, -0.442909f, -0.16609f, -0.63668f)
+ }
+
+ @Test
+ fun div_returnsQuaternionDividedByScalar() {
+ val underTest = Quaternion(0f, 0f, 0f, 1f)
+ val underTestDiv = underTest / 2f
+
+ assertRotation(underTestDiv, 0f, 0f, 0f, 1f)
+ }
+
+ @Test
+ fun interpolate_returnsInterpolatedQuaternion() {
+ val underTest = Quaternion(1f, 2f, 3f, 4f)
+ val underTest2 = Quaternion(2f, 4f, 6f, 8f)
+ val underTestInterpolate = Quaternion.lerp(underTest, underTest2, 0.5f)
+
+ assertRotation(underTestInterpolate, 0.18257418f, 0.36514837f, 0.5477226f, 0.73029673f)
+ }
+
+ @Test
+ fun sphericalInterpolate_returnsQuaternionInterpolatedBetweenQuaternions() {
+ val axis = Vector3(1f, 2f, 3f)
+ val ratio = 0.5f
+ val sourceAngle = 90f
+ val destinationAngle = 180f
+ val expectedAngle = sourceAngle + (destinationAngle - sourceAngle) * ratio
+ val sourceQuaternion = Quaternion.fromAxisAngle(axis, sourceAngle)
+ val destinationQuaternion = Quaternion.fromAxisAngle(axis, destinationAngle)
+ val expected = Quaternion.fromAxisAngle(axis, expectedAngle)
+ val slerpResult = Quaternion.slerp(sourceQuaternion, destinationQuaternion, ratio)
+
+ assertRotation(slerpResult, expected.x, expected.y, expected.z, expected.w)
+ }
+
+ @Test
+ fun sphericalInterpolate_smallAngle_returnsQuaternionInterpolatedBetweenQuaternions() {
+ val axis = Vector3(1f, 2f, 3f)
+ val ratio = 0.5f
+ val sourceAngle = 90f
+ val destinationAngle = 90.01f
+ val expectedAngle = sourceAngle + (destinationAngle - sourceAngle) * ratio
+ val sourceQuaternion = Quaternion.fromAxisAngle(axis, sourceAngle)
+ val destinationQuaternion = Quaternion.fromAxisAngle(axis, destinationAngle)
+ val expected = Quaternion.fromAxisAngle(axis, expectedAngle)
+ val slerpResult = Quaternion.slerp(sourceQuaternion, destinationQuaternion, ratio)
+
+ assertRotation(
+ slerpResult,
+ expected.x,
+ expected.y,
+ expected.z,
+ expected.w,
+ tolerance = 1.0e-5f
+ )
+ }
+
+ @Test
+ fun sphericalInterpolate_ratioAboveOne_returnsQuaternionExtrapolatedOutsideQuaternions() {
+ val axis = Vector3(1f, 2f, 3f)
+ val ratio = 2.0f
+ val sourceAngle = 90f
+ val destinationAngle = 100f
+ val expectedAngle = sourceAngle + (destinationAngle - sourceAngle) * ratio
+ val sourceQuaternion = Quaternion.fromAxisAngle(axis, sourceAngle)
+ val destinationQuaternion = Quaternion.fromAxisAngle(axis, destinationAngle)
+ val expected = Quaternion.fromAxisAngle(axis, expectedAngle)
+
+ val slerpResult = Quaternion.slerp(sourceQuaternion, destinationQuaternion, ratio)
+
+ assertRotation(slerpResult, expected.x, expected.y, expected.z, expected.w)
+ }
+
+ @Test
+ fun sphericalInterpolate_ratioBelowZero_returnsQuaternionExtrapolatedOutsideQuaternions() {
+ val axis = Vector3(1f, 2f, 3f)
+ val ratio = -2.0f
+ val sourceAngle = 90f
+ val destinationAngle = 100f
+ val expectedAngle = sourceAngle + (destinationAngle - sourceAngle) * ratio
+ val sourceQuaternion = Quaternion.fromAxisAngle(axis, sourceAngle)
+ val destinationQuaternion = Quaternion.fromAxisAngle(axis, destinationAngle)
+ val expected = Quaternion.fromAxisAngle(axis, expectedAngle)
+ val slerpResult = Quaternion.slerp(sourceQuaternion, destinationQuaternion, ratio)
+
+ assertRotation(slerpResult, expected.x, expected.y, expected.z, expected.w)
+ }
+
+ @Test
+ fun sphericalInterpolate_RatioZero_returnsSourceQuaternion() {
+ val sourceQuaternion = Quaternion(1f, 2f, 3f, 4f)
+ val destinationQuaternion = Quaternion(2f, 5f, 8f, 23f)
+ val slerpResult = Quaternion.slerp(sourceQuaternion, destinationQuaternion, 0.0f)
+ val expected = sourceQuaternion // The source quaternion should be returned at ratio 0.
+
+ assertRotation(slerpResult, expected.x, expected.y, expected.z, expected.w)
+ }
+
+ @Test
+ fun sphericalInterpolate_RatioOne_returnsDestinationQuaternion() {
+ val axis = Vector3(1f, 2f, 3f)
+ val sourceQuaternion = Quaternion.fromAxisAngle(axis, 90f)
+ val destinationQuaternion = Quaternion.fromAxisAngle(axis, 180f)
+ val slerpResult = Quaternion.slerp(sourceQuaternion, destinationQuaternion, 1.0f)
+ val expected = destinationQuaternion // The destination quat should be returned at ratio 1.
+
+ assertRotation(slerpResult, expected.x, expected.y, expected.z, expected.w)
+ }
+
+ @Test
+ fun dot_returnsDotProductOfTwoQuaternions() {
+ val underTest = Quaternion(1f, 2f, 3f, 4f)
+ val underTest2 = Quaternion(1f, -2f, 3f, 2f)
+ val underTestDot = underTest.dot(underTest2)
+ val underTestDot2 = Quaternion.dot(underTest, underTest2)
+
+ assertThat(underTestDot).isEqualTo(0.6024641f)
+ assertThat(underTestDot2).isEqualTo(0.6024641f)
+ }
+
+ @Test
+ fun eulerAngles_returnedFromQuaternion() {
+ val underTest = Quaternion(1f, 2f, 3f, 4f)
+ val yawPitchRollVector = underTest.eulerAngles
+
+ assertThat(yawPitchRollVector.x).isWithin(1.0e-5f).of(-7.66226f)
+ assertThat(yawPitchRollVector.y).isWithin(1.0e-5f).of(47.726307f)
+ assertThat(yawPitchRollVector.z).isWithin(1.0e-5f).of(70.34616f)
+ }
+
+ @Test
+ fun rotationBetweenQuaternions_returnsAngleBetweenTwoQuaternions1() {
+ assertThat(Quaternion.angle(Quaternion(1f, 2f, 3f, 0f), Quaternion(1f, 2f, 3f, 0f)))
+ .isEqualTo(0.055952907f)
+ }
+
+ @Test
+ fun rotationBetweenQuaternions_returnsAngleBetweenTwoQuaternions2() {
+ assertThat(Quaternion.angle(Quaternion(1f, 2f, 3f, 5f), Quaternion(-1f, -2f, -3f, 5f)))
+ .isEqualTo(147.23466f)
+ }
+
+ @Test
+ fun fromRotation_returnsQuaternionFromRotationBetweenTwoVectors1() {
+ val underTest = Quaternion.fromRotation(Vector3(1f, 0f, 0f), Vector3(1f, 0f, 0f))
+
+ assertRotation(underTest, 0f, 0f, 0f, 1f)
+ }
+
+ @Test
+ fun fromRotation_returnsQuaternionFromRotationBetweenTwoVectors2() {
+ val underTest = Quaternion.fromRotation(Vector3(1f, 0f, 0f), Vector3(-1f, 0f, 0f))
+
+ assertRotation(underTest, 0f, 1f, 0f, 0f)
+ }
+
+ @Test
+ fun fromRotation_returnsQuaternionFromRotationBetweenTwoVectors3() {
+ val underTest = Quaternion.fromRotation(Vector3(1f, 0f, 0f), Vector3(0f, 1f, 0f))
+
+ assertRotation(underTest, 0f, 0f, 0.7071f, 0.7071f)
+ }
+
+ @Test
+ fun axisAngle_returnsAxisAngleOfQuaternion1() {
+ assertAxisAngle(Quaternion(0f, 1f, 0f, 0f), Vector3(0f, 1f, 0f), 180f)
+ }
+
+ @Test
+ fun axisAngle_returnsAxisAngleOfQuaternion2() {
+ assertAxisAngle(Quaternion(0f, 0f, 0.7071f, 0.7071f), Vector3(0f, 0f, 1f), 90f)
+ }
+
+ @Test
+ fun axisAngle_returnsAxisAngleOfQuaternion3() {
+ assertAxisAngle(Quaternion(0f, 0f, 0f, 1f), Vector3(1f, 0f, 0f), 0f)
+ }
+
+ @Test
+ fun axisAngle_returnsAxisAngleOfQuaternion4() {
+ assertAxisAngle(Quaternion(0f, 0.7071f, 0f, 0.7071f), Vector3(0f, 1f, 0f), 90f)
+ }
+
+ private fun assertAxisAngle(rotation: Quaternion, axis: Vector3, angle: Float) {
+ assertThat(rotation.axisAngle.first.x).isWithin(1.0e-5f).of(axis.x)
+ assertThat(rotation.axisAngle.first.y).isWithin(1.0e-5f).of(axis.y)
+ assertThat(rotation.axisAngle.first.z).isWithin(1.0e-5f).of(axis.z)
+ assertThat(rotation.axisAngle.second).isWithin(1.0e-5f).of(angle)
+ }
+
+ @Test
+ fun copy_returnsCopyOfQuaternion() {
+ val underTest = Quaternion(1f, 2f, 3f, 4f)
+ val underTestCopy = underTest.copy()
+
+ assertRotation(underTestCopy, underTest.x, underTest.y, underTest.z, underTest.w)
+ }
+
+ @Test
+ fun fromRotation_returnsQuaternionFromRotationBetweenTwoQuaternions1() {
+ val resultantQuaternion =
+ Quaternion.fromRotation(Quaternion(1f, 2f, 3f, 0f), Quaternion(1f, 2f, 3f, 0f))
+
+ assertRotation(resultantQuaternion, 0f, 0f, 0f, 1f)
+ }
+
+ @Test
+ fun fromRotation_returnsQuaternionFromRotationBetweenTwoQuaternions2() {
+ val resultantQuaternion =
+ Quaternion.fromRotation(
+ Quaternion(0f, 0f, 0.7071f, 0.7071f),
+ Quaternion(0f, 0f, 0.7071f, -0.7071f),
+ )
+
+ assertRotation(resultantQuaternion, 0f, 0f, 1f, 0f)
+ }
+
+ @Test
+ fun fromRotation_returnsQuaternionFromRotationBetweenTwoQuaternions3() {
+ val resultantQuaternion =
+ Quaternion.fromRotation(
+ Quaternion(0.7071f, 0f, 0f, 0.7071f),
+ Quaternion(0f, 0.7071f, 0f, 0.7071f),
+ )
+
+ assertRotation(resultantQuaternion, -0.5f, 0.5f, 0.5f, 0.5f)
+ }
+
+ private fun assertRotation(
+ rotation: Quaternion,
+ expectedX: Float,
+ expectedY: Float,
+ expectedZ: Float,
+ expectedW: Float,
+ tolerance: Float = 1.0e-4f,
+ ) {
+ assertThat(rotation.x).isWithin(tolerance).of(expectedX)
+ assertThat(rotation.y).isWithin(tolerance).of(expectedY)
+ assertThat(rotation.z).isWithin(tolerance).of(expectedZ)
+ assertThat(rotation.w).isWithin(tolerance).of(expectedW)
+ }
+
+ @Test
+ fun fromLookTowards_returnsQuaternionFromForwardAndUpVectors() {
+ val resultantQuaternion =
+ Quaternion.fromLookTowards(Vector3(0f, 0f, 1f), Vector3(0f, 1f, 0f))
+
+ assertRotation(resultantQuaternion, 0f, 0f, 0f, 1f)
+
+ val resultantQuaternion2 =
+ Quaternion.fromLookTowards(Vector3(1f, 0f, 0f), Vector3(0f, 1f, 0f))
+
+ assertRotation(resultantQuaternion2, 0f, 0.707107f, 0f, 0.707107f)
+ }
+}
diff --git a/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/RayTest.kt b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/RayTest.kt
new file mode 100644
index 0000000..018eb6d
--- /dev/null
+++ b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/RayTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.math
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class RayTest {
+ @Test
+ fun constructor_noArguments_returnsDefaultVector() {
+ val underTest = Ray()
+
+ assertThat(underTest.origin).isEqualTo(Vector3())
+ assertThat(underTest.direction).isEqualTo(Vector3())
+ }
+
+ @Test
+ fun equals_sameValues_returnsTrue() {
+ val underTest = Ray(Vector3(1f, 2f, 3f), Vector3(4f, 5f, 6f))
+ val underTest2 = Ray(Vector3(1f, 2f, 3f), Vector3(4f, 5f, 6f))
+
+ assertThat(underTest).isEqualTo(underTest2)
+ }
+
+ @Test
+ fun equals_differentValues_returnsFalse() {
+ val underTest = Ray(Vector3(1f, 2f, 3f), Vector3(4f, 5f, 6f))
+ val underTest2 = Ray(Vector3(3f, 4f, 5f), Vector3(6f, 7f, 8f))
+ val underTest3 = Ray()
+
+ assertThat(underTest).isNotEqualTo(underTest2)
+ assertThat(underTest).isNotEqualTo(underTest3)
+ }
+
+ @Test
+ fun hashCodeEquals_sameValues_returnsTrue() {
+ val underTest = Ray(Vector3(1f, 2f, 3f), Vector3(4f, 5f, 6f))
+ val underTest2 = Ray(Vector3(1f, 2f, 3f), Vector3(4f, 5f, 6f))
+
+ assertThat(underTest.hashCode()).isEqualTo(underTest2.hashCode())
+ }
+
+ @Test
+ fun hashCodeEquals_differentValues_returnsFalse() {
+ val underTest = Ray(Vector3(1f, 2f, 3f), Vector3(4f, 5f, 6f))
+ val underTest2 = Ray(Vector3(3f, 4f, 5f), Vector3(6f, 7f, 8f))
+ val underTest3 = Ray()
+
+ assertThat(underTest.hashCode()).isNotEqualTo(underTest2.hashCode())
+ assertThat(underTest.hashCode()).isNotEqualTo(underTest3.hashCode())
+ }
+
+ @Test
+ fun constructorEquals_expectedToString_returnsTrue() {
+ val underTest = Ray(Vector3(1f, 2f, 3f), Vector3(4f, 5f, 6f))
+ val underTest2 = Ray(Vector3(3f, 4f, 5f), Vector3(6f, 7f, 8f))
+
+ assertThat(underTest.toString())
+ .isEqualTo("[origin=[x=1.0, y=2.0, z=3.0], direction=[x=4.0, y=5.0, z=6.0]]")
+ assertThat(underTest2.toString())
+ .isEqualTo("[origin=[x=3.0, y=4.0, z=5.0], direction=[x=6.0, y=7.0, z=8.0]]")
+ }
+
+ @Test
+ fun constructor_fromRay_returnsSameValues() {
+ val underTest = Ray(Vector3(1f, 2f, 3f), Vector3(4f, 5f, 6f))
+ val underTest2 = Ray(underTest)
+
+ assertThat(underTest).isEqualTo(underTest2)
+ }
+}
diff --git a/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/Vector2Test.kt b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/Vector2Test.kt
new file mode 100644
index 0000000..629129c
--- /dev/null
+++ b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/Vector2Test.kt
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.math
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class Vector2Test {
+ @Test
+ fun constructor_noArguments_returnsZeroVector() {
+ val underTest = Vector2()
+
+ assertThat(underTest.x).isEqualTo(0)
+ assertThat(underTest.y).isEqualTo(0)
+ }
+
+ @Test
+ fun equals_sameValues_returnsTrue() {
+ val underTest = Vector2(1f, 2f)
+ val underTest2 = Vector2(1f, 2f)
+
+ assertThat(underTest).isEqualTo(underTest2)
+ }
+
+ @Test
+ fun equals_differentValues_returnsFalse() {
+ val underTest = Vector2(1f, 2f)
+ val underTest2 = Vector2(9f, 10f)
+ val underTest3 = Vector3()
+
+ assertThat(underTest).isNotEqualTo(underTest2)
+ assertThat(underTest).isNotEqualTo(underTest3)
+ }
+
+ @Test
+ fun hashCodeEquals_sameValues_returnsTrue() {
+ val underTest = Vector2(1f, 2f)
+ val underTest2 = Vector2(1f, 2f)
+
+ assertThat(underTest.hashCode()).isEqualTo(underTest2.hashCode())
+ }
+
+ @Test
+ fun hashCodeEquals_differentValues_returnsFalse() {
+ val underTest = Vector2(1f, 2f)
+ val underTest2 = Vector2(9f, 10f)
+ val underTest3 = Vector2()
+
+ assertThat(underTest.hashCode()).isNotEqualTo(underTest2.hashCode())
+ assertThat(underTest.hashCode()).isNotEqualTo(underTest3.hashCode())
+ }
+
+ @Test
+ fun constructorEquals_expectedToString_returnsTrue() {
+ val underTest = Vector2(1f, 2f)
+ val underTest2 = Vector2()
+
+ assertThat(underTest.toString()).isEqualTo("[x=1.0, y=2.0]")
+ assertThat(underTest2.toString()).isEqualTo("[x=0.0, y=0.0]")
+ }
+
+ @Test
+ fun constructor_fromVector2_returnsSameValues() {
+ val underTest = Vector2(1f, 2f)
+ val underTest2 = underTest
+
+ assertThat(underTest).isEqualTo(underTest2)
+ }
+
+ @Test
+ fun normalized_returnsVectorWithUnitLength() {
+ assertThat(Vector2(3f, 4f).toNormalized()).isEqualTo(Vector2(0.6f, 0.8f))
+ assertThat(Vector2(1f, 1f).toNormalized()).isEqualTo(Vector2(0.70710677f, 0.70710677f))
+ }
+
+ @Test
+ fun multiply_returnsVectorScaledByScalar() {
+ assertThat(Vector2(3f, 4f) * 2f).isEqualTo(Vector2(6f, 8f))
+ assertThat(Vector2(1f, 1f) * 0.5f).isEqualTo(Vector2(0.5f, 0.5f))
+ }
+
+ @Test
+ fun add_returnsTwoVectorsAddedTogether() {
+ val underTest = Vector2(1f, 2f)
+ val underTest2 = Vector2(3f, 4f)
+ val underTestAdd = underTest + underTest2
+
+ assertThat(underTestAdd).isEqualTo(Vector2(4f, 6f))
+ }
+
+ @Test
+ fun subtract_returnsTwoVectorsSubtracted() {
+ val underTest = Vector2(1f, 5f)
+ val underTest2 = Vector2(3f, 4f)
+ val underTestSubtract = underTest - underTest2
+
+ assertThat(underTestSubtract).isEqualTo(Vector2(-2f, 1f))
+ }
+
+ @Test
+ fun multiply_returnsTwoVectorsMultiplied() {
+ val underTest = Vector2(1f, 2f)
+ val underTest2 = Vector2(3f, 4f)
+ val underTestMultiply = underTest * underTest2
+
+ assertThat(underTestMultiply).isEqualTo(Vector2(3f, 8f))
+ }
+
+ @Test
+ fun cross_returnsCrossProductOfTwoVectors() {
+ val underTest = Vector2(1f, 2f)
+ val underTest2 = Vector2(3f, 4f)
+ val underTestCross = underTest cross underTest2
+
+ assertThat(underTestCross).isEqualTo(-2f)
+ }
+
+ @Test
+ fun dot_returnsDotProductOfTwoVectors() {
+ assertThat(Vector2(1f, 2f) dot Vector2(3f, 4f)).isEqualTo(11f)
+ assertThat(Vector2(-1f, 2f) dot Vector2(1f, 10f)).isEqualTo(19f)
+ }
+
+ @Test
+ fun clamp_returnsVectorClampedBetweenTwoVectors1() {
+ val underTest = Vector2(3f, 4f).clamp(Vector2(5f, 6f), Vector2(7f, 8f))
+
+ assertThat(underTest).isEqualTo(Vector2(5f, 6f))
+ }
+
+ @Test
+ fun clamp_returnsVectorClampedBetweenTwoVectors2() {
+ val underTest = Vector2(3f, 4f).clamp(Vector2(1f, 2f), Vector2(5f, 6f))
+
+ assertThat(underTest).isEqualTo(Vector2(3f, 4f))
+ }
+
+ @Test
+ fun clamp_returnsVectorClampedBetweenTwoVectors3() {
+ val underTest = Vector2(5f, 6f).clamp(Vector2(1f, 2f), Vector2(3f, 4f))
+
+ assertThat(underTest).isEqualTo(Vector2(3f, 4f))
+ }
+
+ @Test
+ fun unaryMinus_returnsVectorWithNegativeValues() {
+ val underTest = Vector2(1f, 2f)
+ val underTestNegative = -underTest
+
+ assertThat(underTestNegative).isEqualTo(Vector2(-1f, -2f))
+ }
+
+ @Test
+ fun div_returnsVectorDividedByScalar() {
+ val underTest = Vector2(1f, -2f)
+ val underTestDiv = underTest / -2f
+
+ assertThat(underTestDiv).isEqualTo(Vector2(-0.5f, 1f))
+ }
+
+ @Test
+ fun divide_returnsVectorDividedByVector() {
+ val underTest = Vector2(1f, 2f)
+ val underTest2 = Vector2(-2f, 4f)
+ val underTestDiv = underTest / underTest2
+
+ assertThat(underTestDiv).isEqualTo(Vector2(-0.5f, 0.5f))
+ }
+
+ @Test
+ fun distance_returnsDistanceBetweenTwoVectors() {
+ val underTest = Vector2(1f, 0f)
+ val underTest2 = Vector2(2f, 2f)
+ val underTestDistance = Vector2.distance(underTest, underTest2)
+
+ assertThat(underTestDistance).isEqualTo(2.2360679775f) // sqrt(1^2 + 2^2)
+ }
+
+ @Test
+ fun copy_returnsCopyOfVector() {
+ val underTest = Vector2(1f, 2f)
+ val underTestCopy = underTest.copy()
+
+ assertThat(underTestCopy).isEqualTo(underTest)
+ }
+
+ @Test
+ fun angularDistance_returnsAngleBetweenTwoVectors1() {
+ assertThat(Vector2.angularDistance(Vector2(1f, 0f), Vector2(0f, 1f)))
+ .isWithin(1e-5f)
+ .of(90f)
+ }
+
+ @Test
+ fun angularDistance_returnsAngleBetweenTwoVectors2() {
+ assertThat(Vector2.angularDistance(Vector2(1f, 0f), Vector2(-1f, 0f)))
+ .isWithin(1e-5f)
+ .of(180f)
+ }
+
+ @Test
+ fun angularDistance_returnsAngleBetweenTwoVectors3() {
+ assertThat(Vector2.angularDistance(Vector2(2f, 4f), Vector2(4f, 8f))).isWithin(1e-5f).of(0f)
+ }
+
+ @Test
+ fun angularDistance_returnsAngleBetweenTwoVectors4() {
+ assertThat(Vector2.angularDistance(Vector2(2f, 2f), Vector2(0f, 3f)))
+ .isWithin(1e-5f)
+ .of(45f)
+ }
+
+ @Test
+ fun lerp_returnsInterpolatedVector1() {
+ val underTest = Vector2.lerp(Vector2(1f, 2f), Vector2(2f, 4f), 0.5f)
+
+ assertThat(underTest.x).isWithin(1e-5f).of(1.5f)
+ assertThat(underTest.y).isWithin(1e-5f).of(3f)
+ }
+
+ @Test
+ fun lerp_returnsInterpolatedVector2() {
+ val underTest = Vector2.lerp(Vector2(4f, 5f), Vector2(12f, 15f), 0.25f)
+
+ assertThat(underTest.x).isWithin(1e-5f).of(6f)
+ assertThat(underTest.y).isWithin(1e-5f).of(7.5f)
+ }
+
+ @Test
+ fun lerp_returnsInterpolatedVector3() {
+ val underTest = Vector2.lerp(Vector2(2f, 6f), Vector2(12f, 26f), 0.4f)
+
+ assertThat(underTest.x).isWithin(1e-5f).of(6f)
+ assertThat(underTest.y).isWithin(1e-5f).of(14f)
+ }
+
+ @Test
+ fun abs_returnsAbsoluteValueofVector2() {
+ val underTest = Vector2.abs(Vector2(-3f, 4f))
+
+ assertThat(underTest.x).isEqualTo(3f)
+ assertThat(underTest.y).isEqualTo(4f)
+ }
+}
diff --git a/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/Vector3Test.kt b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/Vector3Test.kt
new file mode 100644
index 0000000..925d731
--- /dev/null
+++ b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/Vector3Test.kt
@@ -0,0 +1,310 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.math
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class Vector3Test {
+ @Test
+ fun constructor_noArguments_returnsZeroVector() {
+ val underTest = Vector3()
+
+ assertThat(underTest).isEqualTo(Vector3(0f, 0f, 0f))
+ }
+
+ @Test
+ fun equals_sameValues_returnsTrue() {
+ val underTest = Vector3(1f, 2f, 3f)
+ val underTest2 = Vector3(1f, 2f, 3f)
+
+ assertThat(underTest).isEqualTo(underTest2)
+ }
+
+ @Test
+ fun equals_differentValues_returnsFalse() {
+ val underTest = Vector3(1f, 2f, 3f)
+ val underTest2 = Vector3(9f, 10f, 11f)
+ val underTest3 = Vector2()
+
+ assertThat(underTest).isNotEqualTo(underTest2)
+ assertThat(underTest).isNotEqualTo(underTest3)
+ }
+
+ @Test
+ fun hashCodeEquals_sameValues_returnsTrue() {
+ val underTest = Vector3(1f, 2f, 3f)
+ val underTest2 = Vector3(1f, 2f, 3f)
+
+ assertThat(underTest.hashCode()).isEqualTo(underTest2.hashCode())
+ }
+
+ @Test
+ fun hashCodeEquals_differentValues_returnsFalse() {
+ val underTest = Vector3(1f, 2f, 3f)
+ val underTest2 = Vector3(9f, 10f, 11f)
+ val underTest3 = Vector3()
+
+ assertThat(underTest.hashCode()).isNotEqualTo(underTest2.hashCode())
+ assertThat(underTest.hashCode()).isNotEqualTo(underTest3.hashCode())
+ }
+
+ @Test
+ fun constructorEquals_expectedToString_returnsTrue() {
+ val underTest = Vector3(1f, 2f, 3f)
+ val underTest2 = Vector3()
+
+ assertThat(underTest.toString()).isEqualTo("[x=1.0, y=2.0, z=3.0]")
+ assertThat(underTest2.toString()).isEqualTo("[x=0.0, y=0.0, z=0.0]")
+ }
+
+ @Test
+ fun constructor_fromVector3_returnsSameValues() {
+ val underTest = Vector3(1f, 2f, 3f)
+ val underTest2 = underTest
+
+ assertThat(underTest).isEqualTo(underTest2)
+ }
+
+ @Test
+ fun fromFloat_returnsSameValues() {
+ val underTest = Vector3.fromValue(1f)
+
+ assertThat(underTest).isEqualTo(Vector3.One)
+ }
+
+ @Test
+ fun normalized_returnsVectorWithUnitLength() {
+ assertThat(Vector3(3f, 4f, 5f).toNormalized())
+ .isEqualTo(Vector3(0.42426407f, 0.56568545f, 0.7071068f))
+
+ assertThat(Vector3(1f, 1f, 0.5f).toNormalized())
+ .isEqualTo(Vector3(0.6666667f, 0.6666667f, 0.33333334f))
+ }
+
+ @Test
+ fun multiply_returnsVectorScaledByScalar() {
+ assertThat(Vector3(3f, 4f, 5f) * 2f).isEqualTo(Vector3(6f, 8f, 10f))
+
+ assertThat(Vector3(1f, 1f, 0.5f) * 0.5f).isEqualTo(Vector3(0.5f, 0.5f, 0.25f))
+ }
+
+ @Test
+ fun plus_returnsVectorWithAddedValues() {
+ val underTest = Vector3(1F, 2F, 3F) + Vector3(4F, 5F, 6F)
+
+ assertThat(underTest.x).isEqualTo(5F) // 1 + 4
+ assertThat(underTest.y).isEqualTo(7F) // 2 + 5
+ assertThat(underTest.z).isEqualTo(9F) // 3 + 6
+ }
+
+ @Test
+ fun minus_returnsVectorWithSubtractedValues() {
+ val underTest = Vector3(4F, 5F, 6F) - Vector3(1F, 2F, 3F)
+
+ assertThat(underTest.x).isEqualTo(3F) // 4 - 1
+ assertThat(underTest.y).isEqualTo(3F) // 5 - 2
+ assertThat(underTest.z).isEqualTo(3F) // 6 - 3
+ }
+
+ @Test
+ fun multiply_returnsTwoVectorsMultiplied() {
+ val underTest = Vector3(1f, 2f, 3f)
+ val underTest2 = Vector3(3f, 4f, 5f)
+ val underTestMultiply = underTest * underTest2
+
+ assertThat(underTestMultiply).isEqualTo(Vector3(3f, 8f, 15f))
+ }
+
+ @Test
+ fun dot_returnsDotProductOfTwoVectors() {
+ val underTest = Vector3(1f, 2f, 3f)
+ val underTest2 = Vector3(3f, -4f, 5f)
+ val underTestDot = underTest dot underTest2
+
+ assertThat(underTestDot).isEqualTo(10f)
+ }
+
+ @Test
+ fun cross_returnsCrossProductOfTwoVectors() {
+ val underTest = Vector3(1f, 2f, 3f)
+ val underTest2 = Vector3(3f, -4f, 5f)
+ val underTestCross = underTest cross underTest2
+
+ assertThat(underTestCross).isEqualTo(Vector3(22f, 4f, -10f))
+ }
+
+ @Test
+ fun length_returnsSqrtOfEachComponentSquared() {
+ assertThat(Vector3(0F, 3F, 4F).length).isEqualTo(5F) // sqrt(0^2 + 3^2 + 4^2) = sqrt(25)
+ }
+
+ @Test
+ fun clamp_returnsVectorClampedBetweenTwoVectors1() {
+ val underTest = Vector3(1f, 2f, 3f).clamp(Vector3(4f, 5f, 6f), Vector3(7f, 8f, 9f))
+
+ assertThat(underTest).isEqualTo(Vector3(4f, 5f, 6f))
+ }
+
+ @Test
+ fun clamp_returnsVectorClampedBetweenTwoVectors2() {
+ val underTest = Vector3(1f, 2f, 3f).clamp(Vector3(1f, 2f, 3f), Vector3(5f, 6f, 7f))
+
+ assertThat(underTest).isEqualTo(Vector3(1f, 2f, 3f))
+ }
+
+ @Test
+ fun clamp_returnsVectorClampedBetweenTwoVectors3() {
+ val underTest = Vector3(5f, 6f, 7f).clamp(Vector3(1f, 2f, 3f), Vector3(5f, 6f, 7f))
+
+ assertThat(underTest).isEqualTo(Vector3(5f, 6f, 7f))
+ }
+
+ @Test
+ fun angleBetweenVectors_returnsAngleBetweenTwoVectors1() {
+ assertThat(toDegrees(Vector3.angleBetween(Vector3(1f, 0f, 0f), Vector3(0f, 1f, 0f))))
+ .isWithin(1e-5f)
+ .of(90f)
+ }
+
+ @Test
+ fun angleBetweenVectors_returnsAngleBetweenTwoVectors2() {
+ assertThat(toDegrees(Vector3.angleBetween(Vector3(0f, 0f, 1f), Vector3(0f, 0f, -1f))))
+ .isWithin(1e-5f)
+ .of(180f)
+ }
+
+ @Test
+ fun angleBetweenVectors_returnsAngleBetweenTwoVectors3() {
+ assertThat(toDegrees(Vector3.angleBetween(Vector3(2f, 4f, 0f), Vector3(4f, 8f, 0f))))
+ .isWithin(1e-5f)
+ .of(0f)
+ }
+
+ @Test
+ fun angleBetweenVectors_returnsAngleBetweenTwoVectors4() {
+ assertThat(toDegrees(Vector3.angleBetween(Vector3(2f, 2f, 0f), Vector3(0f, 3f, 0f))))
+ .isWithin(1e-5f)
+ .of(45f)
+ }
+
+ @Test
+ fun lerp_returnsInterpolatedVector1() {
+ val underTest = Vector3.lerp(Vector3(1f, 2f, 3f), Vector3(4f, 5f, 6f), 0.5f)
+
+ assertThat(underTest.x).isWithin(1e-5f).of(2.5f)
+ assertThat(underTest.y).isWithin(1e-5f).of(3.5f)
+ assertThat(underTest.z).isWithin(1e-5f).of(4.5f)
+ }
+
+ @Test
+ fun lerp_returnsInterpolatedVector2() {
+ val underTest = Vector3.lerp(Vector3(4f, 5f, 6f), Vector3(12f, 15f, 18f), 0.25f)
+
+ assertThat(underTest.x).isWithin(1e-5f).of(6f)
+ assertThat(underTest.y).isWithin(1e-5f).of(7.5f)
+ assertThat(underTest.z).isWithin(1e-5f).of(9f)
+ }
+
+ @Test
+ fun lerp_returnsInterpolatedVector3() {
+ val underTest = Vector3.lerp(Vector3(2f, 6f, 10f), Vector3(12f, 26f, 30f), 0.4f)
+
+ assertThat(underTest.x).isWithin(1e-5f).of(6f)
+ assertThat(underTest.y).isWithin(1e-5f).of(14f)
+ assertThat(underTest.z).isWithin(1e-5f).of(18f)
+ }
+
+ @Test
+ fun unaryMinus_returnsVectorWithNegativeValues() {
+ val underTest = Vector3(1f, 2f, 3f)
+ val underTestNegative = -underTest
+
+ assertThat(underTestNegative).isEqualTo(Vector3(-1f, -2f, -3f))
+ }
+
+ @Test
+ fun div_returnsVectorDividedByScalar() {
+ val underTest = Vector3(1f, -2f, 3f)
+ val underTestDiv = underTest / -2f
+
+ assertThat(underTestDiv).isEqualTo(Vector3(-0.5f, 1f, -1.5f))
+ }
+
+ @Test
+ fun divide_returnsVectorDividedByVector() {
+ val underTest = Vector3(1f, 2f, 6f)
+ val underTest2 = Vector3(-2f, 4f, -3f)
+ val underTestDiv = underTest / underTest2
+
+ assertThat(underTestDiv).isEqualTo(Vector3(-0.5f, 0.5f, -2f))
+ }
+
+ @Test
+ fun projectOnPlane_returnsVectorProjectedOnPlane() {
+ val underTest = Vector3(1f, 2f, 3f)
+ val underTest2 = Vector3(1f, 2f, 0f)
+ val underTestProject = Vector3.projectOnPlane(underTest, underTest2)
+
+ assertThat(underTestProject).isEqualTo(Vector3(0f, 0f, 3f))
+ }
+
+ @Test
+ fun distance_returnsDistanceBetweenTwoVectors() {
+ val underTest = Vector3(1f, 0f, 1f)
+ val underTest2 = Vector3(2f, 2f, 5f)
+ val underTestDistance = Vector3.distance(underTest, underTest2)
+
+ assertThat(underTestDistance).isWithin(1.0e-4f).of(4.58257569f) // sqrt(1^2 + 2^2 + 4^2)
+ }
+
+ @Test
+ fun abs_returnsVectorWithAbsoluteValues() {
+ val underTest = Vector3.abs(Vector3(-1f, 2f, -3f))
+
+ assertThat(underTest).isEqualTo(Vector3(1f, 2f, 3f))
+ }
+
+ @Test
+ fun maxVector_returnsMaxVectorFromTwoVectors() {
+ val underTest = Vector3(8f, 2f, -3f)
+ val underTest2 = Vector3(4f, 5f, 6f)
+ val underTestMax = Vector3.max(underTest, underTest2)
+
+ assertThat(underTestMax).isEqualTo(Vector3(8f, 5f, 6f))
+ }
+
+ @Test
+ fun minVector_returnsMinVectorFromTwoVectors() {
+ val underTest = Vector3(8f, 2f, -3f)
+ val underTest2 = Vector3(4f, 5f, 6f)
+ val underTestMin = Vector3.min(underTest, underTest2)
+
+ assertThat(underTestMin).isEqualTo(Vector3(4f, 2f, -3f))
+ }
+
+ @Test
+ fun copy_returnsCopyOfVector() {
+ val underTest = Vector3(1f, 2f, 3f)
+ val underTestCopy = underTest.copy()
+
+ assertThat(underTestCopy).isEqualTo(underTest)
+ }
+}
diff --git a/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/Vector4Test.kt b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/Vector4Test.kt
new file mode 100644
index 0000000..c2af8af
--- /dev/null
+++ b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/math/Vector4Test.kt
@@ -0,0 +1,315 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.runtime.math
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class Vector4Test {
+ @Test
+ fun constructor_noArguments_returnsZeroVector() {
+ val underTest = Vector4()
+
+ assertThat(underTest).isEqualTo(Vector4(0f, 0f, 0f, 0f))
+ }
+
+ @Test
+ fun equals_sameValues_returnsTrue() {
+ val underTest = Vector4(1f, 2f, 3f, 4f)
+ val underTest2 = Vector4(1f, 2f, 3f, 4f)
+
+ assertThat(underTest).isEqualTo(underTest2)
+ }
+
+ @Test
+ fun equals_differentValues_returnsFalse() {
+ val underTest = Vector4(1f, 2f, 3f, 4f)
+ val underTest2 = Vector4(9f, 10f, 11f, 12f)
+ val underTest3 = Vector3()
+
+ assertThat(underTest).isNotEqualTo(underTest2)
+ assertThat(underTest).isNotEqualTo(underTest3)
+ }
+
+ @Test
+ fun hashCodeEquals_sameValues_returnsTrue() {
+ val underTest = Vector4(1f, 2f, 3f, 4f)
+ val underTest2 = Vector4(1f, 2f, 3f, 4f)
+
+ assertThat(underTest.hashCode()).isEqualTo(underTest2.hashCode())
+ }
+
+ @Test
+ fun hashCodeEquals_differentValues_returnsFalse() {
+ val underTest = Vector4(1f, 2f, 3f, 4f)
+ val underTest2 = Vector4(9f, 10f, 11f, 12f)
+ val underTest3 = Vector4()
+
+ assertThat(underTest.hashCode()).isNotEqualTo(underTest2.hashCode())
+ assertThat(underTest.hashCode()).isNotEqualTo(underTest3.hashCode())
+ }
+
+ @Test
+ fun constructorEquals_expectedToString_returnsTrue() {
+ val underTest = Vector4(1f, 2f, 3f, 4f)
+ val underTest2 = Vector4()
+
+ assertThat(underTest.toString()).isEqualTo("[x=1.0, y=2.0, z=3.0, w=4.0]")
+ assertThat(underTest2.toString()).isEqualTo("[x=0.0, y=0.0, z=0.0, w=0.0]")
+ }
+
+ @Test
+ fun constructor_fromVector4_returnsSameValues() {
+ val underTest = Vector4(1f, 2f, 3f, 4f)
+ val underTest2 = underTest
+
+ assertThat(underTest).isEqualTo(underTest2)
+ }
+
+ @Test
+ fun fromFloat_returnsSameValues() {
+ val underTest = Vector4.fromValue(1f)
+
+ assertThat(underTest).isEqualTo(Vector4.One)
+ }
+
+ @Test
+ fun normalized_returnsVectorWithUnitLength1() {
+ val underTest = Vector4(1f, 1f, 2f, 2f).toNormalized()
+
+ assertVector4(underTest, 0.3162f, 0.3162f, 0.6325f, 0.6325f)
+ }
+
+ @Test
+ fun normalized_returnsVectorWithUnitLength2() {
+ val underTest = Vector4(1f, 1f, 1f, 1f).toNormalized()
+
+ assertVector4(underTest, 0.5f, 0.5f, 0.5f, 0.5f)
+ }
+
+ @Test
+ fun multiply_returnsVectorScaledByScalar1() {
+ assertThat(Vector4(3f, 4f, 5f, 6f) * 2f).isEqualTo(Vector4(6f, 8f, 10f, 12f))
+ }
+
+ @Test
+ fun multiply_returnsVectorScaledByScalar2() {
+ assertThat(Vector4(1f, 1f, 0.5f, 10f) * 0.5f).isEqualTo(Vector4(0.5f, 0.5f, 0.25f, 5f))
+ }
+
+ @Test
+ fun plus_returnsVectorWithAddedValues() {
+ val underTest = Vector4(1F, 2F, 3F, 4F) + Vector4(4F, 5F, 6F, 7F)
+
+ assertVector4(underTest, 5F, 7F, 9F, 11F)
+ }
+
+ @Test
+ fun minus_returnsVectorWithSubtractedValues() {
+ val underTest = Vector4(4F, 5F, 6F, 7F) - Vector4(1F, 2F, 3F, 4F)
+
+ assertVector4(underTest, 3F, 3F, 3F, 3F)
+ }
+
+ @Test
+ fun multiply_returnsTwoVectorsMultiplied() {
+ val underTest = Vector4(1f, 2f, 3f, 4f)
+ val underTest2 = Vector4(3f, 4f, 5f, 6f)
+ val underTestMultiply = underTest * underTest2
+
+ assertThat(underTestMultiply).isEqualTo(Vector4(3f, 8f, 15f, 24f))
+ }
+
+ @Test
+ fun dot_returnsDotProductOfTwoVectors() {
+ val underTest = Vector4(1f, 2f, 3f, 4f)
+ val underTest2 = Vector4(3f, -4f, 5f, 2f)
+ val underTestDot = underTest dot underTest2
+
+ assertThat(underTestDot).isEqualTo(18f)
+ }
+
+ @Test
+ fun length_returnsSqrtOfEachComponentSquared() {
+ assertThat(Vector4(0F, 0F, 3F, 4F).length)
+ .isEqualTo(5F) // sqrt(0^2 + 0^2 + 3^2 + 4^2) = sqrt(25)
+ }
+
+ @Test
+ fun clamp_returnsVectorClampedBetweenTwoVectors1() {
+ val underTest =
+ Vector4(1f, 2f, 3f, 4f).clamp(Vector4(4f, 5f, 6f, 7f), Vector4(7f, 8f, 9f, 10f))
+
+ assertVector4(underTest, 4f, 5f, 6f, 7f)
+ }
+
+ @Test
+ fun clamp_returnsVectorClampedBetweenTwoVectors2() {
+ val underTest =
+ Vector4(1f, 2f, 3f, 4f).clamp(Vector4(1f, 2f, 3f, 4f), Vector4(5f, 6f, 7f, 8f))
+
+ assertVector4(underTest, 1f, 2f, 3f, 4f)
+ }
+
+ @Test
+ fun clamp_returnsVectorClampedBetweenTwoVectors3() {
+ val underTest =
+ Vector4(6f, 7f, 8f, 9f).clamp(Vector4(1f, 2f, 3f, 4f), Vector4(5f, 6f, 7f, 8f))
+
+ assertVector4(underTest, 5f, 6f, 7f, 8f)
+ }
+
+ @Test
+ fun unaryMinus_returnsVectorWithNegativeValues() {
+ val underTest = Vector4(1f, 2f, 3f, 4f)
+ val underTestNegative = -underTest
+
+ assertThat(underTestNegative).isEqualTo(Vector4(-1f, -2f, -3f, -4f))
+ }
+
+ @Test
+ fun div_returnsVectorDividedByScalar() {
+ val underTest = Vector4(1f, -2f, 3f, -4f)
+ val underTestDiv = underTest / -2f
+
+ assertThat(underTestDiv).isEqualTo(Vector4(-0.5f, 1f, -1.5f, 2f))
+ }
+
+ @Test
+ fun divide_returnsVectorDividedByVector() {
+ val underTest = Vector4(1f, 2f, 6f, 12f)
+ val underTest2 = Vector4(-2f, 4f, -3f, 2f)
+ val underTestDiv = underTest / underTest2
+
+ assertThat(underTestDiv).isEqualTo(Vector4(-0.5f, 0.5f, -2f, 6f))
+ }
+
+ @Test
+ fun abs_returnsVectorWithAbsoluteValues() {
+ val underTest = Vector4.abs(Vector4(-1f, 2f, -3f, 4f))
+
+ assertThat(underTest).isEqualTo(Vector4(1f, 2f, 3f, 4f))
+ }
+
+ @Test
+ fun copy_returnsCopyOfVector() {
+ val underTest = Vector4(1f, 2f, 3f, 4f)
+ val underTestCopy = underTest.copy()
+
+ assertThat(underTestCopy).isEqualTo(underTest)
+ }
+
+ @Test
+ fun angleBetween_returnsAngleBetweenTwoVectors1() {
+ assertThat(
+ toDegrees(Vector4.angleBetween(Vector4(1f, 0f, 0f, 0f), Vector4(0f, 1f, 0f, 0f)))
+ )
+ .isWithin(1e-5f)
+ .of(90f)
+ }
+
+ @Test
+ fun angleBetween_returnsAngleBetweenTwoVectors2() {
+ assertThat(
+ toDegrees(Vector4.angleBetween(Vector4(0f, 0f, 1f, 0f), Vector4(0f, 0f, -1f, 0f)))
+ )
+ .isWithin(1e-5f)
+ .of(180f)
+ }
+
+ @Test
+ fun angleBetween_returnsAngleBetweenTwoVectors3() {
+ assertThat(
+ toDegrees(Vector4.angleBetween(Vector4(2f, 4f, 0f, 0f), Vector4(4f, 8f, 0f, 0f)))
+ )
+ .isWithin(1e-5f)
+ .of(0f)
+ }
+
+ @Test
+ fun angleBetween_returnsAngleBetweenTwoVectors4() {
+ assertThat(
+ toDegrees(Vector4.angleBetween(Vector4(0f, 0f, 0f, 1f), Vector4(0f, 0f, 0f, 0f)))
+ )
+ .isWithin(1e-5f)
+ .of(0f)
+ }
+
+ @Test
+ fun distance_returnsDistanceBetweenTwoVectors() {
+ val underTest = Vector4(1f, 0f, 1f, 1f)
+ val underTest2 = Vector4(2f, 2f, 5f, 3f)
+ val underTestDistance = Vector4.distance(underTest, underTest2)
+
+ assertThat(underTestDistance).isWithin(1.0e-4f).of(5f) // sqrt(1^2 + 2^2 + 4^2 + 2^2)
+ }
+
+ @Test
+ fun lerp_returnsInterpolatedVector1() {
+ val underTest = Vector4.lerp(Vector4(1f, 2f, 3f, 4f), Vector4(4f, 5f, 6f, 7f), 0.5f)
+
+ assertVector4(underTest, 2.5f, 3.5f, 4.5f, 5.5f)
+ }
+
+ @Test
+ fun lerp_returnsInterpolatedVector2() {
+ val underTest = Vector4.lerp(Vector4(4f, 5f, 6f, 7f), Vector4(12f, 15f, 18f, 21f), 0.25f)
+
+ assertVector4(underTest, 6f, 7.5f, 9f, 10.5f)
+ }
+
+ @Test
+ fun lerp_returnsInterpolatedVector3() {
+ val underTest = Vector4.lerp(Vector4(2f, 6f, 10f, 14f), Vector4(12f, 26f, 30f, 34f), 0.4f)
+
+ assertVector4(underTest, 6f, 14f, 18f, 22f)
+ }
+
+ @Test
+ fun maxVector_returnsMaxVectorFromTwoVectors() {
+ val underTest = Vector4(8f, 2f, -3f, 5f)
+ val underTest2 = Vector4(4f, 5f, 6f, 2f)
+ val underTestMax = Vector4.max(underTest, underTest2)
+
+ assertThat(underTestMax).isEqualTo(Vector4(8f, 5f, 6f, 5f))
+ }
+
+ @Test
+ fun minVector_returnsMinVectorFromTwoVectors() {
+ val underTest = Vector4(8f, 2f, -3f, 0f)
+ val underTest2 = Vector4(4f, 5f, 6f, -1f)
+ val underTestMin = Vector4.min(underTest, underTest2)
+
+ assertThat(underTestMin).isEqualTo(Vector4(4f, 2f, -3f, -1f))
+ }
+
+ private fun assertVector4(
+ vector: Vector4,
+ expectedX: Float,
+ expectedY: Float,
+ expectedZ: Float,
+ expectedW: Float,
+ ) {
+ assertThat(vector.x).isWithin(1.0e-4f).of(expectedX)
+ assertThat(vector.y).isWithin(1.0e-4f).of(expectedY)
+ assertThat(vector.z).isWithin(1.0e-4f).of(expectedZ)
+ assertThat(vector.w).isWithin(1.0e-4f).of(expectedW)
+ }
+}
diff --git a/camera/camera-effects-still-portrait/api/current.txt b/xr/scenecore/scenecore-testing/api/current.txt
similarity index 100%
copy from camera/camera-effects-still-portrait/api/current.txt
copy to xr/scenecore/scenecore-testing/api/current.txt
diff --git a/camera/camera-effects-still-portrait/api/res-current.txt b/xr/scenecore/scenecore-testing/api/res-current.txt
similarity index 100%
copy from camera/camera-effects-still-portrait/api/res-current.txt
copy to xr/scenecore/scenecore-testing/api/res-current.txt
diff --git a/xr/scenecore/scenecore-testing/api/restricted_current.txt b/xr/scenecore/scenecore-testing/api/restricted_current.txt
new file mode 100644
index 0000000..9af083e
--- /dev/null
+++ b/xr/scenecore/scenecore-testing/api/restricted_current.txt
@@ -0,0 +1,318 @@
+// Signature format: 4.0
+package androidx.xr.scenecore.testing {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class FakeImpressApi implements com.google.ar.imp.apibindings.ImpressApi {
+ ctor public FakeImpressApi();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> animateGltfModel(int, String?, boolean);
+ method public int createImpressNode();
+ method public int createStereoSurface(@com.google.ar.imp.apibindings.ImpressApi.StereoMode int);
+ method public void destroyImpressNode(int);
+ method public java.util.List<java.lang.Integer!> getImpressNodesForToken(long);
+ method public android.view.Surface getSurfaceFromStereoSurface(int);
+ method public int impressNodeAnimatingSize();
+ method public boolean impressNodeHasParent(int);
+ method public int impressNodeLoopAnimatingSize();
+ method public int instanceGltfModel(long);
+ method public int instanceGltfModel(long, boolean);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> loadGltfModel(String);
+ method public void onPause();
+ method public void onResume();
+ method public void releaseGltfModel(long);
+ method public void setCanvasDimensionsForStereoSurface(int, float, float);
+ method public void setGltfModelColliderEnabled(int, boolean);
+ method public void setImpressNodeParent(int, int);
+ method public void setStereoModeForStereoSurface(int, @com.google.ar.imp.apibindings.ImpressApi.StereoMode int);
+ method public void setup(com.google.ar.imp.view.View);
+ method public void stopGltfModelAnimation(int);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class FakeScheduledExecutorService extends java.util.concurrent.AbstractExecutorService implements java.lang.AutoCloseable java.util.concurrent.ScheduledExecutorService {
+ ctor public FakeScheduledExecutorService();
+ method public boolean awaitTermination(long, java.util.concurrent.TimeUnit?);
+ method public void close();
+ method public void execute(Runnable?);
+ method public boolean hasNext();
+ method @com.google.errorprone.annotations.CheckReturnValue public boolean isEmpty();
+ method public boolean isShutdown();
+ method public boolean isTerminated();
+ method public void runAll();
+ method public void runNext();
+ method public java.util.concurrent.ScheduledFuture<? extends java.lang.Object!> schedule(Runnable?, long, java.util.concurrent.TimeUnit?);
+ method public <V> java.util.concurrent.ScheduledFuture<V!> schedule(java.util.concurrent.Callable<V!>?, long, java.util.concurrent.TimeUnit?);
+ method public java.util.concurrent.ScheduledFuture<? extends java.lang.Object!> scheduleAtFixedRate(Runnable?, long, long, java.util.concurrent.TimeUnit?);
+ method public java.util.concurrent.ScheduledFuture<? extends java.lang.Object!> scheduleWithFixedDelay(Runnable?, long, long, java.util.concurrent.TimeUnit?);
+ method public void shutdown();
+ method public java.util.List<java.lang.Runnable!> shutdownNow();
+ method public void simulateSleepExecutingAllTasks(java.time.Duration);
+ method public boolean simulateSleepExecutingAtMostOneTask();
+ method public boolean simulateSleepExecutingAtMostOneTask(java.time.Duration);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class FakeXrExtensions implements androidx.xr.extensions.XrExtensions {
+ ctor public FakeXrExtensions();
+ method public void addFindableView(android.view.View, android.view.ViewGroup);
+ method @Deprecated public void attachSpatialEnvironment(android.app.Activity, androidx.xr.extensions.node.Node);
+ method public void attachSpatialEnvironment(android.app.Activity, androidx.xr.extensions.node.Node, androidx.xr.extensions.Consumer<androidx.xr.extensions.XrExtensionResult!>, java.util.concurrent.Executor);
+ method @Deprecated public void attachSpatialScene(android.app.Activity, androidx.xr.extensions.node.Node, androidx.xr.extensions.node.Node);
+ method public void attachSpatialScene(android.app.Activity, androidx.xr.extensions.node.Node, androidx.xr.extensions.node.Node, androidx.xr.extensions.Consumer<androidx.xr.extensions.XrExtensionResult!>, java.util.concurrent.Executor);
+ method @Deprecated public boolean canEmbedActivityPanel(android.app.Activity);
+ method public void clearSpatialStateCallback(android.app.Activity);
+ method public androidx.xr.extensions.space.ActivityPanel createActivityPanel(android.app.Activity, androidx.xr.extensions.space.ActivityPanelLaunchParameters);
+ method public androidx.xr.extensions.node.Node createNode();
+ method public androidx.xr.extensions.node.NodeTransaction createNodeTransaction();
+ method public androidx.xr.extensions.node.ReformOptions createReformOptions(androidx.xr.extensions.Consumer<androidx.xr.extensions.node.ReformEvent!>, java.util.concurrent.Executor);
+ method public androidx.xr.extensions.splitengine.SplitEngineBridge createSplitEngineBridge();
+ method public androidx.xr.extensions.subspace.Subspace createSubspace(androidx.xr.extensions.splitengine.SplitEngineBridge, int);
+ method @Deprecated public void detachSpatialEnvironment(android.app.Activity);
+ method public void detachSpatialEnvironment(android.app.Activity, androidx.xr.extensions.Consumer<androidx.xr.extensions.XrExtensionResult!>, java.util.concurrent.Executor);
+ method @Deprecated public void detachSpatialScene(android.app.Activity);
+ method public void detachSpatialScene(android.app.Activity, androidx.xr.extensions.Consumer<androidx.xr.extensions.XrExtensionResult!>, java.util.concurrent.Executor);
+ method @Deprecated public java.util.concurrent.CompletableFuture<androidx.xr.extensions.XrExtensions.SceneViewerResult!> displayGltfModel(android.app.Activity!, androidx.xr.extensions.asset.GltfModelToken!);
+ method public androidx.xr.scenecore.testing.FakeXrExtensions.FakeActivityPanel getActivityPanelForHost(android.app.Activity);
+ method public int getApiVersion();
+ method public void getBounds(android.app.Activity, androidx.xr.extensions.Consumer<androidx.xr.extensions.space.Bounds!>, java.util.concurrent.Executor);
+ method public androidx.xr.extensions.Config getConfig();
+ method public androidx.xr.scenecore.testing.FakeXrExtensions.FakeNode? getFakeEnvironmentNode();
+ method public androidx.xr.scenecore.testing.FakeXrExtensions.FakeNode? getFakeNodeForMainWindow();
+ method public androidx.xr.scenecore.testing.FakeXrExtensions.FakeNode? getFakeTaskNode();
+ method public int getMainWindowHeight();
+ method public int getMainWindowWidth();
+ method public int getOpenXrWorldSpaceType();
+ method public float getPreferredAspectRatio();
+ method public androidx.xr.scenecore.testing.FakeXrExtensions.SpaceMode getSpaceMode();
+ method public void getSpatialCapabilities(android.app.Activity, androidx.xr.extensions.Consumer<androidx.xr.extensions.space.SpatialCapabilities!>, java.util.concurrent.Executor);
+ method public androidx.xr.extensions.space.SpatialState getSpatialState(android.app.Activity);
+ method public androidx.xr.extensions.Consumer<androidx.xr.extensions.space.SpatialState!>? getSpatialStateCallback();
+ method public androidx.xr.extensions.node.Node? getSurfaceTrackingNode(android.view.View);
+ method public androidx.xr.extensions.media.XrSpatialAudioExtensions getXrSpatialAudioExtensions();
+ method public void hitTest(android.app.Activity, androidx.xr.extensions.node.Vec3, androidx.xr.extensions.node.Vec3, androidx.xr.extensions.Consumer<androidx.xr.extensions.space.HitTestResult!>, java.util.concurrent.Executor);
+ method @Deprecated public java.util.concurrent.CompletableFuture<androidx.xr.extensions.asset.EnvironmentToken!> loadEnvironment(java.io.InputStream?, int, int, String?);
+ method @Deprecated public java.util.concurrent.CompletableFuture<androidx.xr.extensions.asset.EnvironmentToken!> loadEnvironment(java.io.InputStream!, int, int, String!, int, int);
+ method @Deprecated public java.util.concurrent.CompletableFuture<androidx.xr.extensions.asset.GltfModelToken!> loadGltfModel(java.io.InputStream?, int, int, String?);
+ method @Deprecated public java.util.concurrent.CompletableFuture<androidx.xr.extensions.asset.SceneToken!> loadImpressScene(java.io.InputStream!, int, int);
+ method public void registerSpatialStateCallback(android.app.Activity, androidx.xr.extensions.Consumer<androidx.xr.extensions.space.SpatialState!>, java.util.concurrent.Executor);
+ method public void removeFindableView(android.view.View, android.view.ViewGroup);
+ method @Deprecated public boolean requestFullSpaceMode(android.app.Activity);
+ method public void requestFullSpaceMode(android.app.Activity, boolean, androidx.xr.extensions.Consumer<androidx.xr.extensions.XrExtensionResult!>, java.util.concurrent.Executor);
+ method @Deprecated public boolean requestHomeSpaceMode(android.app.Activity);
+ method public void sendSpatialState(androidx.xr.extensions.space.SpatialState);
+ method public android.os.Bundle setFullSpaceMode(android.os.Bundle);
+ method public android.os.Bundle setFullSpaceModeWithEnvironmentInherited(android.os.Bundle);
+ method @Deprecated public android.os.Bundle setMainPanelCurvatureRadius(android.os.Bundle, float);
+ method @Deprecated public void setMainWindowCurvatureRadius(android.app.Activity, float);
+ method @Deprecated public void setMainWindowSize(android.app.Activity, int, int);
+ method public void setMainWindowSize(android.app.Activity, int, int, androidx.xr.extensions.Consumer<androidx.xr.extensions.XrExtensionResult!>, java.util.concurrent.Executor);
+ method public void setOpenXrWorldSpaceType(int);
+ method public void setPreferredAspectRatio(android.app.Activity, float);
+ method public void setPreferredAspectRatio(android.app.Activity, float, androidx.xr.extensions.Consumer<androidx.xr.extensions.XrExtensionResult!>, java.util.concurrent.Executor);
+ method @Deprecated public void setSpatialStateCallback(android.app.Activity, androidx.xr.extensions.Consumer<androidx.xr.extensions.space.SpatialStateEvent!>, java.util.concurrent.Executor);
+ method @Deprecated public androidx.xr.scenecore.testing.FakeXrExtensions.FakeNode testGetNodeWithEnvironmentToken(androidx.xr.extensions.asset.EnvironmentToken);
+ method @Deprecated public androidx.xr.scenecore.testing.FakeXrExtensions.FakeNode testGetNodeWithGltfToken(androidx.xr.extensions.asset.GltfModelToken);
+ field public final java.util.Map<android.app.Activity!,androidx.xr.scenecore.testing.FakeXrExtensions.FakeActivityPanel!> activityPanelMap;
+ field public final java.util.List<androidx.xr.scenecore.testing.FakeXrExtensions.FakeEnvironmentToken!> createdEnvironmentTokens;
+ field public final java.util.List<androidx.xr.scenecore.testing.FakeXrExtensions.FakeGltfModelToken!> createdGltfModelTokens;
+ field public final java.util.List<androidx.xr.scenecore.testing.FakeXrExtensions.FakeNode!> createdNodes;
+ field public final androidx.xr.scenecore.testing.FakeXrExtensions.FakeSpatialAudioExtensions fakeSpatialAudioExtensions;
+ field public final androidx.xr.scenecore.testing.FakeXrExtensions.FakeSpatialState fakeSpatialState;
+ }
+
+ public static class FakeXrExtensions.FakeActivityPanel implements androidx.xr.extensions.space.ActivityPanel {
+ method public void delete();
+ method public android.app.Activity? getActivity();
+ method public android.graphics.Rect? getBounds();
+ method public android.os.Bundle getBundle();
+ method public android.content.Intent? getLaunchIntent();
+ method public androidx.xr.extensions.node.Node getNode();
+ method public boolean isDeleted();
+ method public void launchActivity(android.content.Intent, android.os.Bundle?);
+ method public void moveActivity(android.app.Activity);
+ method public void setWindowBounds(android.graphics.Rect);
+ }
+
+ public static class FakeXrExtensions.FakeAudioTrackExtensions implements androidx.xr.extensions.media.AudioTrackExtensions {
+ ctor public FakeXrExtensions.FakeAudioTrackExtensions();
+ method public androidx.xr.extensions.media.PointSourceAttributes? getPointSourceAttributes();
+ method public androidx.xr.extensions.media.SoundFieldAttributes? getSoundFieldAttributes();
+ method public void setPointSourceAttributes(androidx.xr.extensions.media.PointSourceAttributes?);
+ method public void setSoundFieldAttributes(androidx.xr.extensions.media.SoundFieldAttributes?);
+ method public void setSourceType(@androidx.xr.extensions.media.SpatializerExtensions.SourceType int);
+ }
+
+ public static class FakeXrExtensions.FakeCloseable implements java.io.Closeable {
+ ctor public FakeXrExtensions.FakeCloseable();
+ method public void close();
+ method public boolean isClosed();
+ }
+
+ public static class FakeXrExtensions.FakeConfig implements androidx.xr.extensions.Config {
+ ctor public FakeXrExtensions.FakeConfig();
+ method public float defaultPixelsPerMeter(float);
+ field public static final float DEFAULT_PIXELS_PER_METER = 1.0f;
+ }
+
+ public static class FakeXrExtensions.FakeEnvironmentToken implements androidx.xr.extensions.asset.EnvironmentToken {
+ method public String getUrl();
+ }
+
+ public static class FakeXrExtensions.FakeEnvironmentVisibilityState implements androidx.xr.extensions.environment.EnvironmentVisibilityState {
+ ctor public FakeXrExtensions.FakeEnvironmentVisibilityState(@androidx.xr.extensions.environment.EnvironmentVisibilityState.State int);
+ }
+
+ public static class FakeXrExtensions.FakeGltfModelToken implements androidx.xr.extensions.asset.GltfModelToken {
+ ctor public FakeXrExtensions.FakeGltfModelToken(String);
+ method public String getUrl();
+ }
+
+ public static class FakeXrExtensions.FakeInputEvent implements androidx.xr.extensions.node.InputEvent {
+ ctor public FakeXrExtensions.FakeInputEvent();
+ method public int getAction();
+ method public androidx.xr.extensions.node.Vec3 getDirection();
+ method public int getDispatchFlags();
+ method public androidx.xr.extensions.node.InputEvent.HitInfo? getHitInfo();
+ method public androidx.xr.extensions.node.Vec3 getOrigin();
+ method public int getPointerType();
+ method public androidx.xr.extensions.node.InputEvent.HitInfo? getSecondaryHitInfo();
+ method public int getSource();
+ method public long getTimestamp();
+ method public void setDirection(androidx.xr.extensions.node.Vec3);
+ method public void setDispatchFlags(int);
+ method public void setFakeHitInfo(androidx.xr.scenecore.testing.FakeXrExtensions.FakeInputEvent.FakeHitInfo);
+ method public void setOrigin(androidx.xr.extensions.node.Vec3);
+ method public void setTimestamp(long);
+ }
+
+ public static class FakeXrExtensions.FakeInputEvent.FakeHitInfo implements androidx.xr.extensions.node.InputEvent.HitInfo {
+ ctor public FakeXrExtensions.FakeInputEvent.FakeHitInfo();
+ method public androidx.xr.extensions.node.Vec3? getHitPosition();
+ method public androidx.xr.extensions.node.Node getInputNode();
+ method public int getSubspaceImpressNodeId();
+ method public androidx.xr.extensions.node.Mat4f getTransform();
+ method public void setHitPosition(androidx.xr.extensions.node.Vec3?);
+ method public void setInputNode(androidx.xr.extensions.node.Node);
+ method public void setSubspaceImpressNodeId(int);
+ method public void setTransform(androidx.xr.extensions.node.Mat4f);
+ }
+
+ public static class FakeXrExtensions.FakeMediaPlayerExtensions implements androidx.xr.extensions.media.MediaPlayerExtensions {
+ ctor public FakeXrExtensions.FakeMediaPlayerExtensions();
+ method public androidx.xr.extensions.media.PointSourceAttributes? getPointSourceAttributes();
+ method public androidx.xr.extensions.media.SoundFieldAttributes? getSoundFieldAttributes();
+ }
+
+ public static final class FakeXrExtensions.FakeNode implements androidx.xr.extensions.node.Node {
+ method public int describeContents();
+ method public float getAlpha();
+ method public android.os.IBinder? getAnchorId();
+ method @Deprecated public androidx.xr.extensions.asset.EnvironmentToken? getEnvironment();
+ method public java.util.concurrent.Executor? getExecutor();
+ method @Deprecated public androidx.xr.extensions.asset.GltfModelToken? getGltfModel();
+ method public androidx.xr.extensions.Consumer<androidx.xr.extensions.node.InputEvent!>? getListener();
+ method public String? getName();
+ method public androidx.xr.scenecore.testing.FakeXrExtensions.FakeNode? getParent();
+ method public androidx.xr.extensions.Consumer<java.lang.Integer!>? getPointerCaptureStateCallback();
+ method public androidx.xr.extensions.node.ReformOptions? getReformOptions();
+ method public android.view.SurfaceControlViewHost.SurfacePackage? getSurfacePackage();
+ method public java.util.concurrent.Executor? getTransformExecutor();
+ method public androidx.xr.extensions.Consumer<androidx.xr.extensions.node.NodeTransform!>? getTransformListener();
+ method public float getWOrientation();
+ method public float getXOrientation();
+ method public float getXPosition();
+ method public float getYOrientation();
+ method public float getYPosition();
+ method public float getZOrientation();
+ method public float getZPosition();
+ method public boolean isVisible();
+ method public void listenForInput(androidx.xr.extensions.Consumer<androidx.xr.extensions.node.InputEvent!>, java.util.concurrent.Executor);
+ method public void requestPointerCapture(androidx.xr.extensions.Consumer<java.lang.Integer!>, java.util.concurrent.Executor);
+ method public void sendInputEvent(androidx.xr.extensions.node.InputEvent);
+ method public void sendTransformEvent(androidx.xr.scenecore.testing.FakeXrExtensions.FakeNodeTransform);
+ method public void setNonPointerFocusTarget(android.view.AttachedSurfaceControl);
+ method public void stopListeningForInput();
+ method public void stopPointerCapture();
+ method public java.io.Closeable subscribeToTransform(androidx.xr.extensions.Consumer<androidx.xr.extensions.node.NodeTransform!>, java.util.concurrent.Executor);
+ method public void writeToParcel(android.os.Parcel, int);
+ }
+
+ public static class FakeXrExtensions.FakeNodeTransaction implements androidx.xr.extensions.node.NodeTransaction {
+ }
+
+ public static class FakeXrExtensions.FakeNodeTransform implements androidx.xr.extensions.node.NodeTransform {
+ ctor public FakeXrExtensions.FakeNodeTransform(androidx.xr.extensions.node.Mat4f);
+ method public long getTimestamp();
+ method public androidx.xr.extensions.node.Mat4f getTransform();
+ }
+
+ public static class FakeXrExtensions.FakePassthroughVisibilityState implements androidx.xr.extensions.environment.PassthroughVisibilityState {
+ ctor public FakeXrExtensions.FakePassthroughVisibilityState(@androidx.xr.extensions.environment.PassthroughVisibilityState.State int, float);
+ }
+
+ public static class FakeXrExtensions.FakeReformEvent implements androidx.xr.extensions.node.ReformEvent {
+ ctor public FakeXrExtensions.FakeReformEvent();
+ method public androidx.xr.extensions.node.Vec3 getCurrentRayDirection();
+ method public androidx.xr.extensions.node.Vec3 getCurrentRayOrigin();
+ method public int getId();
+ method public androidx.xr.extensions.node.Vec3 getInitialRayDirection();
+ method public androidx.xr.extensions.node.Vec3 getInitialRayOrigin();
+ method public androidx.xr.extensions.node.Quatf getProposedOrientation();
+ method public androidx.xr.extensions.node.Vec3 getProposedPosition();
+ method public androidx.xr.extensions.node.Vec3 getProposedScale();
+ method public androidx.xr.extensions.node.Vec3 getProposedSize();
+ method public int getState();
+ method public int getType();
+ method public void setProposedOrientation(androidx.xr.extensions.node.Quatf);
+ method public void setProposedPosition(androidx.xr.extensions.node.Vec3);
+ method public void setProposedScale(androidx.xr.extensions.node.Vec3);
+ method public void setState(int);
+ method public void setType(int);
+ }
+
+ public static class FakeXrExtensions.FakeReformOptions implements androidx.xr.extensions.node.ReformOptions {
+ method public androidx.xr.extensions.node.Vec3 getCurrentSize();
+ method public int getEnabledReform();
+ method public androidx.xr.extensions.Consumer<androidx.xr.extensions.node.ReformEvent!> getEventCallback();
+ method public java.util.concurrent.Executor getEventExecutor();
+ method public float getFixedAspectRatio();
+ method public int getFlags();
+ method public androidx.xr.extensions.node.Vec3 getMaximumSize();
+ method public androidx.xr.extensions.node.Vec3 getMinimumSize();
+ method public androidx.xr.extensions.node.ReformOptions setCurrentSize(androidx.xr.extensions.node.Vec3);
+ method public androidx.xr.extensions.node.ReformOptions setEnabledReform(int);
+ method public androidx.xr.extensions.node.ReformOptions setEventCallback(androidx.xr.extensions.Consumer<androidx.xr.extensions.node.ReformEvent!>);
+ method public androidx.xr.extensions.node.ReformOptions setEventExecutor(java.util.concurrent.Executor);
+ method public androidx.xr.extensions.node.ReformOptions setFixedAspectRatio(float);
+ method public androidx.xr.extensions.node.ReformOptions setFlags(int);
+ method public androidx.xr.extensions.node.ReformOptions setMaximumSize(androidx.xr.extensions.node.Vec3);
+ method public androidx.xr.extensions.node.ReformOptions setMinimumSize(androidx.xr.extensions.node.Vec3);
+ }
+
+ public static class FakeXrExtensions.FakeSoundPoolExtensions implements androidx.xr.extensions.media.SoundPoolExtensions {
+ ctor public FakeXrExtensions.FakeSoundPoolExtensions();
+ method public void setPlayAsPointSourceResult(int);
+ method public void setPlayAsSoundFieldResult(int);
+ method public void setSourceType(@androidx.xr.extensions.media.SpatializerExtensions.SourceType int);
+ }
+
+ public static class FakeXrExtensions.FakeSpatialAudioExtensions implements androidx.xr.extensions.media.XrSpatialAudioExtensions {
+ ctor public FakeXrExtensions.FakeSpatialAudioExtensions();
+ method public void setFakeAudioTrackExtensions(androidx.xr.scenecore.testing.FakeXrExtensions.FakeAudioTrackExtensions);
+ field public final androidx.xr.scenecore.testing.FakeXrExtensions.FakeMediaPlayerExtensions mediaPlayerExtensions;
+ field public final androidx.xr.scenecore.testing.FakeXrExtensions.FakeSoundPoolExtensions soundPoolExtensions;
+ }
+
+ public static class FakeXrExtensions.FakeSpatialState implements androidx.xr.extensions.space.SpatialState {
+ ctor public FakeXrExtensions.FakeSpatialState();
+ method public void setAllSpatialCapabilities(boolean);
+ method public void setBounds(androidx.xr.extensions.space.Bounds);
+ method public void setEnvironmentVisibility(androidx.xr.extensions.environment.EnvironmentVisibilityState);
+ method public void setPassthroughVisibility(androidx.xr.extensions.environment.PassthroughVisibilityState);
+ method public void setSpatialCapabilities(androidx.xr.extensions.space.SpatialCapabilities);
+ }
+
+ public enum FakeXrExtensions.SpaceMode {
+ enum_constant public static final androidx.xr.scenecore.testing.FakeXrExtensions.SpaceMode FULL_SPACE;
+ enum_constant public static final androidx.xr.scenecore.testing.FakeXrExtensions.SpaceMode HOME_SPACE;
+ enum_constant public static final androidx.xr.scenecore.testing.FakeXrExtensions.SpaceMode NONE;
+ }
+
+}
+
diff --git a/xr/scenecore/scenecore-testing/build.gradle b/xr/scenecore/scenecore-testing/build.gradle
new file mode 100644
index 0000000..998dd30
--- /dev/null
+++ b/xr/scenecore/scenecore-testing/build.gradle
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+
+import androidx.build.KotlinTarget
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+}
+
+dependencies {
+ api(project(":xr:scenecore:scenecore"))
+
+ implementation(libs.testExtTruth)
+ implementation("androidx.annotation:annotation:1.8.1")
+ implementation("androidx.concurrent:concurrent-futures:1.0.0")
+ implementation("com.google.ar:impress:0.0.1")
+}
+
+android {
+ defaultConfig {
+ // TODO: This should be lower, possibly 21.
+ // Address API calls that require higher versions.
+ minSdkVersion 30
+ }
+ namespace = "androidx.xr.scenecore.testing"
+}
+
+androidx {
+ name = "XR SceneCore Testing"
+ type = LibraryType.PUBLISHED_TEST_LIBRARY
+ inceptionYear = "2024"
+ description = "Libraries to aid in unit testing SceneCore clients."
+
+ // TODO: b/379715750 - Remove this flag once the deprecated methods have been removed from the API.
+ failOnDeprecationWarnings = false
+ kotlinTarget = KotlinTarget.KOTLIN_1_9
+ // TODO: b/326456246
+ optOutJSpecify = true
+}
diff --git a/xr/scenecore/scenecore-testing/src/main/java/androidx/xr/scenecore/testing/FakeImpressApi.java b/xr/scenecore/scenecore-testing/src/main/java/androidx/xr/scenecore/testing/FakeImpressApi.java
new file mode 100644
index 0000000..adf600a6
--- /dev/null
+++ b/xr/scenecore/scenecore-testing/src/main/java/androidx/xr/scenecore/testing/FakeImpressApi.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.testing;
+
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.concurrent.futures.ResolvableFuture;
+
+import com.google.ar.imp.apibindings.ImpressApi;
+import com.google.ar.imp.view.View;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Fake implementation of the JNI API for communicating with the Impress Split Engine instance for
+ * testing purposes.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class FakeImpressApi implements ImpressApi {
+
+ // ResolvableFuture is marked as RestrictTo(LIBRARY_GROUP_PREFIX), which is intended for classes
+ // within AndroidX. We're in the process of migrating to AndroidX. Without suppressing this
+ // warning, however, we get a build error - go/bugpattern/RestrictTo.
+ @SuppressWarnings("RestrictTo")
+ static class AnimationInProgress {
+ public String name;
+ public ResolvableFuture<Void> fireOnDone;
+ }
+
+ // Map of model tokens to the list of impress nodes that are instances of that model.
+ private final Map<Long, List<Integer>> gltfModels = new HashMap<>();
+ // Map of impress nodes to their parent impress nodes.
+ private final Map<Integer, Integer> impressNodes = new HashMap<>();
+
+ // Map of impress nodes and animations that are currently playing (non looping)
+ final Map<Integer, AnimationInProgress> impressAnimatedNodes = new HashMap<>();
+
+ // Map of impress nodes and animations that are currently playing (looping)
+ final Map<Integer, AnimationInProgress> impressLoopAnimatedNodes = new HashMap<>();
+
+ private int nextModelId = 1;
+ private int nextNodeId = 1;
+
+ @Override
+ public void setup(@NonNull View view) {}
+
+ @Override
+ public void onResume() {}
+
+ @Override
+ public void onPause() {}
+
+ @Override
+ @NonNull
+ @SuppressWarnings({"RestrictTo", "AsyncSuffixFuture"})
+ public ListenableFuture<Long> loadGltfModel(@NonNull String name) {
+ long modelToken = nextModelId++;
+ gltfModels.put(modelToken, new ArrayList<>());
+ // TODO(b/352827267): Enforce minSDK API strategy - go/androidx-api-guidelines#compat-newapi
+ ResolvableFuture<Long> ret = ResolvableFuture.create();
+ ret.set(modelToken);
+
+ return ret;
+ }
+
+ @Override
+ public void releaseGltfModel(long modelToken) {
+ if (!gltfModels.containsKey(modelToken)) {
+ throw new IllegalArgumentException("Model token not found");
+ }
+ gltfModels.remove(modelToken);
+ }
+
+ @Override
+ public int instanceGltfModel(long modelToken) {
+ return instanceGltfModel(modelToken, true);
+ }
+
+ @Override
+ public int instanceGltfModel(long modelToken, boolean enableCollider) {
+ if (!gltfModels.containsKey(modelToken)) {
+ throw new IllegalArgumentException("Model token not found");
+ }
+ int entityId = nextNodeId++;
+ gltfModels.get(modelToken).add(entityId);
+ impressNodes.put(entityId, null);
+ return entityId;
+ }
+
+ @Override
+ public void setGltfModelColliderEnabled(int impressNode, boolean enableCollider) {
+ throw new IllegalArgumentException("not implemented");
+ }
+
+ @Override
+ @NonNull
+ @SuppressWarnings({"RestrictTo", "AsyncSuffixFuture"})
+ public ListenableFuture<Void> animateGltfModel(
+ int impressNode, @Nullable String animationName, boolean loop) {
+ ResolvableFuture<Void> future = ResolvableFuture.create();
+ if (impressNodes.get(impressNode) == null) {
+ future.setException(new IllegalArgumentException("Impress node not found"));
+ return future;
+ }
+
+ AnimationInProgress animationInProgress = new AnimationInProgress();
+ animationInProgress.name = animationName;
+ animationInProgress.fireOnDone = future;
+ if (loop) {
+ impressLoopAnimatedNodes.put(impressNode, animationInProgress);
+ } else {
+ impressAnimatedNodes.put(impressNode, animationInProgress);
+ }
+ return future;
+ }
+
+ @Override
+ public void stopGltfModelAnimation(int impressNode) {
+ if (impressNodes.get(impressNode) == null) {
+ throw new IllegalArgumentException("Impress node not found");
+ } else if (!impressAnimatedNodes.containsKey(impressNode)
+ && !impressLoopAnimatedNodes.containsKey(impressNode)) {
+ throw new IllegalArgumentException("Impress node is not animating");
+ } else if (impressAnimatedNodes.containsKey(impressNode)) {
+ impressAnimatedNodes.remove(impressNode);
+ } else if (impressLoopAnimatedNodes.containsKey(impressNode)) {
+ impressLoopAnimatedNodes.remove(impressNode);
+ }
+ }
+
+ @Override
+ public int createImpressNode() {
+ int entityId = nextNodeId++;
+ impressNodes.put(entityId, null);
+ return entityId;
+ }
+
+ @Override
+ public void destroyImpressNode(int impressNode) {
+ if (!impressNodes.containsKey(impressNode)) {
+ throw new IllegalArgumentException("Impress node not found");
+ }
+ for (Map.Entry<Long, List<Integer>> pair : gltfModels.entrySet()) {
+ if (pair.getValue().contains(impressNode)) {
+ pair.getValue().remove(impressNode);
+ }
+ }
+ for (Map.Entry<Integer, Integer> pair : impressNodes.entrySet()) {
+ if (pair.getValue() != null && pair.getValue().equals((Integer) impressNode)) {
+ pair.setValue(null);
+ }
+ }
+ impressNodes.remove(impressNode);
+ }
+
+ @Override
+ public void setImpressNodeParent(int impressNodeChild, int impressNodeParent) {
+ if (!impressNodes.containsKey(impressNodeChild)
+ || !impressNodes.containsKey(impressNodeParent)) {
+ throw new IllegalArgumentException("Impress node(s) not found");
+ }
+ impressNodes.put(impressNodeChild, impressNodeParent);
+ }
+
+ @NonNull
+ public List<Integer> getImpressNodesForToken(long modelToken) {
+ return gltfModels.get(modelToken);
+ }
+
+ public boolean impressNodeHasParent(int impressNode) {
+ return impressNodes.containsKey(impressNode) && impressNodes.get(impressNode) != null;
+ }
+
+ public int impressNodeAnimatingSize() {
+ return impressAnimatedNodes.size();
+ }
+
+ public int impressNodeLoopAnimatingSize() {
+ return impressLoopAnimatedNodes.size();
+ }
+
+ @Override
+ public int createStereoSurface(@StereoMode int mode) {
+ return nextNodeId++;
+ }
+
+ @Override
+ @NonNull
+ public Surface getSurfaceFromStereoSurface(int panelImpressNode) {
+ throw new IllegalArgumentException("not implemented");
+ }
+
+ @Override
+ public void setStereoModeForStereoSurface(int panelImpressNode, @StereoMode int mode) {
+ throw new IllegalArgumentException("not implemented");
+ }
+
+ @Override
+ public void setCanvasDimensionsForStereoSurface(
+ int panelImpressNode, float width, float height) {
+ throw new IllegalArgumentException("not implemented");
+ }
+}
diff --git a/xr/scenecore/scenecore-testing/src/main/java/androidx/xr/scenecore/testing/FakeScheduledExecutorService.java b/xr/scenecore/scenecore-testing/src/main/java/androidx/xr/scenecore/testing/FakeScheduledExecutorService.java
new file mode 100644
index 0000000..802791d
--- /dev/null
+++ b/xr/scenecore/scenecore-testing/src/main/java/androidx/xr/scenecore/testing/FakeScheduledExecutorService.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.testing;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import com.google.common.collect.Lists;
+import com.google.errorprone.annotations.CheckReturnValue;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.AbstractExecutorService;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Delayed;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.PriorityBlockingQueue;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Fake implementation of {@link ScheduledExecutorService} that lets tests control when tasks are
+ * executed.
+ */
+@SuppressWarnings("NotCloseable")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class FakeScheduledExecutorService extends AbstractExecutorService
+ implements ScheduledExecutorService, AutoCloseable {
+
+ private static final TimeUnit CLOCK_UNIT = MILLISECONDS;
+
+ private static long toClockUnit(Duration duration) {
+ return duration.toMillis();
+ }
+
+ private static Duration durationFromClockUnit(long durationClockUnit) {
+ return Duration.ofMillis(durationClockUnit);
+ }
+
+ private final Clock clock;
+ private final Queue<Runnable> executeQueue = new ConcurrentLinkedQueue<>();
+ private final PriorityBlockingQueue<DelayedFuture<?>> scheduledQueue =
+ new PriorityBlockingQueue<>();
+
+ private final AtomicLong nextSequenceId = new AtomicLong(0);
+ private volatile boolean running = true;
+
+ public FakeScheduledExecutorService() {
+ this.clock = new Clock();
+ }
+
+ @Override
+ public boolean isShutdown() {
+ return !running;
+ }
+
+ @Override
+ public boolean isTerminated() {
+ return isShutdown() && isEmpty();
+ }
+
+ @Override
+ public void shutdown() {
+ running = false;
+ }
+
+ @Override
+ public void close() {
+ shutdown();
+ }
+
+ @Override
+ @NonNull
+ public List<Runnable> shutdownNow() {
+ running = false;
+ List<Runnable> commands = Lists.newArrayList();
+ commands.addAll(executeQueue);
+ commands.addAll(scheduledQueue);
+ executeQueue.clear();
+ scheduledQueue.clear();
+ return commands;
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, @Nullable TimeUnit unit) {
+ checkState(!running);
+ while (!executeQueue.isEmpty()) {
+ runNext();
+ }
+ simulateSleepExecutingAllTasks(durationFromClockUnit(timeout));
+ return isEmpty();
+ }
+
+ @Override
+ public void execute(@Nullable Runnable command) {
+ assertRunning();
+ executeQueue.add(command);
+ }
+
+ private void assertRunning() {
+ if (!running) {
+ throw new RejectedExecutionException();
+ }
+ }
+
+ @Override
+ @NonNull
+ public ScheduledFuture<?> schedule(
+ @Nullable Runnable command, long delay, @Nullable TimeUnit unit) {
+ assertRunning();
+ DelayedFuture<?> future = new DelayedFuture<>(command, delay, unit);
+ scheduledQueue.add(future);
+ return future;
+ }
+
+ @Override
+ @NonNull
+ public <V> ScheduledFuture<V> schedule(
+ @Nullable Callable<V> callable, long delay, @Nullable TimeUnit unit) {
+ assertRunning();
+ DelayedFuture<V> future = new DelayedCallable<V>(callable, delay, unit);
+ scheduledQueue.add(future);
+ return future;
+ }
+
+ @Override
+ @NonNull
+ public ScheduledFuture<?> scheduleAtFixedRate(
+ @Nullable Runnable command, long initialDelay, long period, @Nullable TimeUnit unit) {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
+ @NonNull
+ public ScheduledFuture<?> scheduleWithFixedDelay(
+ @Nullable Runnable command, long initialDelay, long delay, @Nullable TimeUnit unit) {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ /** Returns true if the {@link #execute} queue contains at least one runnable. */
+ public boolean hasNext() {
+ return !executeQueue.isEmpty();
+ }
+
+ /** Runs the next runnable in the {@link #execute} queue. */
+ public void runNext() {
+ checkState(!executeQueue.isEmpty(), "execute queue must not be empty");
+ Runnable runnable = executeQueue.remove();
+ runTaskWithInterruptIsolation(runnable);
+ }
+
+ /** Runs all of the runnables that {@link #execute} enqueued. */
+ public void runAll() {
+ while (hasNext()) {
+ runNext();
+ }
+ }
+
+ /** Returns whether any runnable is in the {@link #execute} or {@link #schedule} queue. */
+ @CheckReturnValue
+ public boolean isEmpty() {
+ return executeQueue.isEmpty() && scheduledQueue.isEmpty();
+ }
+
+ /**
+ * Executes tasks from the {@link #schedule} queue until the given amount of simulated time has
+ * passed.
+ */
+ public void simulateSleepExecutingAllTasks(@NonNull Duration duration) {
+ long timeout = toClockUnit(duration);
+ checkArgument(timeout >= 0, "timeout (%s) cannot be negative", timeout);
+
+ long stopTime = clock.currentTimeMillis() + CLOCK_UNIT.toMillis(timeout);
+ boolean done = false;
+
+ while (!done) {
+ long delay = (stopTime - clock.currentTimeMillis());
+ if (delay >= 0 && simulateSleepExecutingAtMostOneTask(durationFromClockUnit(delay))) {
+ continue;
+ } else {
+ done = true;
+ }
+ }
+ }
+
+ /**
+ * Simulates sleeping up to the given timeout before executing the next scheduled task, if any.
+ */
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public boolean simulateSleepExecutingAtMostOneTask(@NonNull Duration duration) {
+ long timeout = toClockUnit(duration);
+ checkArgument(timeout >= 0, "timeout (%s) cannot be negative", timeout);
+ if (scheduledQueue.isEmpty()) {
+ clock.advanceBy(duration);
+ return false;
+ }
+
+ DelayedFuture<?> future = scheduledQueue.peek();
+ long delay = future.getDelay(CLOCK_UNIT);
+ if (delay > timeout) {
+ // Next event is too far in the future; delay the entire time
+ clock.advanceBy(duration);
+ return false;
+ }
+
+ scheduledQueue.poll();
+ runTaskWithInterruptIsolation(future);
+
+ return true;
+ }
+
+ /**
+ * Simulates sleeping as long as necessary before executing the next scheduled task. Does
+ * nothing if the {@link #schedule} queue is empty.
+ */
+ public boolean simulateSleepExecutingAtMostOneTask() {
+ if (scheduledQueue.isEmpty()) {
+ return false;
+ }
+
+ DelayedFuture<?> future = scheduledQueue.poll();
+ runTaskWithInterruptIsolation(future);
+ return true;
+ }
+
+ private class DelayedFuture<T> implements ScheduledFuture<T>, Runnable {
+ protected final long timeToRun;
+ private final long sequenceId;
+ private final Runnable command;
+ private boolean cancelled;
+ private boolean done;
+
+ public DelayedFuture(Runnable command, long delay, TimeUnit unit) {
+ checkArgument(delay >= 0, "delay (%s) cannot be negative", delay);
+
+ this.command = command;
+ timeToRun = clock.currentTimeMillis() + unit.toMillis(delay);
+ sequenceId = nextSequenceId.getAndIncrement();
+ }
+
+ @Override
+ public long getDelay(TimeUnit unit) {
+ return unit.convert(timeToRun - clock.currentTimeMillis(), MILLISECONDS);
+ }
+
+ protected void maybeReschedule() {
+ done = true;
+ }
+
+ @Override
+ public void run() {
+ if (clock.currentTimeMillis() < timeToRun) {
+ clock.advanceBy(durationFromClockUnit(timeToRun - clock.currentTimeMillis()));
+ }
+ command.run();
+ maybeReschedule();
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ cancelled = true;
+ done = true;
+ return scheduledQueue.remove(this);
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public boolean isDone() {
+ return done;
+ }
+
+ @Override
+ public T get() throws InterruptedException, ExecutionException {
+ return null;
+ }
+
+ @Override
+ public T get(long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ return null;
+ }
+
+ @Override
+ public int compareTo(Delayed other) {
+ if (other == this) {
+ return 0;
+ }
+ DelayedFuture<?> that = (DelayedFuture<?>) other;
+ long diff = timeToRun - that.timeToRun;
+ if (diff < 0) {
+ return -1;
+ } else if (diff > 0) {
+ return 1;
+ } else if (sequenceId < that.sequenceId) {
+ return -1;
+ } else {
+ return 1;
+ }
+ }
+ }
+
+ private class DelayedCallable<T> extends DelayedFuture<T> {
+ private final FutureTask<T> task;
+
+ private DelayedCallable(FutureTask<T> task, long delay, TimeUnit unit) {
+ super(task, delay, unit);
+ this.task = task;
+ }
+
+ public DelayedCallable(Callable<T> callable, long delay, TimeUnit unit) {
+ this(new FutureTask<T>(callable), delay, unit);
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ task.cancel(mayInterruptIfRunning);
+ return super.cancel(mayInterruptIfRunning);
+ }
+
+ @Override
+ public T get() throws InterruptedException, ExecutionException {
+ return task.get();
+ }
+
+ @Override
+ public T get(long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ return task.get(timeout, unit);
+ }
+ }
+
+ private static class Clock {
+ private final AtomicReference<Instant> nowReference = new AtomicReference<>();
+
+ public Clock() {
+ setTo(Instant.EPOCH);
+ }
+
+ public long currentTimeMillis() {
+ return nowReference.get().toEpochMilli();
+ }
+
+ public void advanceBy(Duration duration) {
+ nowReference.getAndUpdate(now -> now.plus(duration));
+ }
+
+ public void setTo(Instant instant) {
+ nowReference.set(instant);
+ }
+ }
+
+ /** Clears this thread's interrupt bit, runs the task, and restores any previous interrupt. */
+ private void runTaskWithInterruptIsolation(Runnable task) {
+ boolean interruptBitWasSet = Thread.interrupted();
+ try {
+ task.run();
+ } finally {
+ if (interruptBitWasSet) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore-testing/src/main/java/androidx/xr/scenecore/testing/FakeXrExtensions.java b/xr/scenecore/scenecore-testing/src/main/java/androidx/xr/scenecore/testing/FakeXrExtensions.java
new file mode 100644
index 0000000..843fd12
--- /dev/null
+++ b/xr/scenecore/scenecore-testing/src/main/java/androidx/xr/scenecore/testing/FakeXrExtensions.java
@@ -0,0 +1,1872 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.testing;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.media.AudioTrack;
+import android.media.MediaPlayer;
+import android.media.SoundPool;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.view.AttachedSurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.xr.extensions.Config;
+import androidx.xr.extensions.Consumer;
+import androidx.xr.extensions.XrExtensionResult;
+import androidx.xr.extensions.XrExtensions;
+import androidx.xr.extensions.environment.EnvironmentVisibilityState;
+import androidx.xr.extensions.environment.PassthroughVisibilityState;
+import androidx.xr.extensions.media.AudioTrackExtensions;
+import androidx.xr.extensions.media.MediaPlayerExtensions;
+import androidx.xr.extensions.media.PointSourceAttributes;
+import androidx.xr.extensions.media.SoundFieldAttributes;
+import androidx.xr.extensions.media.SoundPoolExtensions;
+import androidx.xr.extensions.media.SpatializerExtensions;
+import androidx.xr.extensions.media.XrSpatialAudioExtensions;
+import androidx.xr.extensions.node.InputEvent;
+import androidx.xr.extensions.node.Mat4f;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.extensions.node.NodeTransaction;
+import androidx.xr.extensions.node.NodeTransform;
+import androidx.xr.extensions.node.Quatf;
+import androidx.xr.extensions.node.ReformEvent;
+import androidx.xr.extensions.node.ReformOptions;
+import androidx.xr.extensions.node.Vec3;
+import androidx.xr.extensions.passthrough.PassthroughState;
+import androidx.xr.extensions.space.ActivityPanel;
+import androidx.xr.extensions.space.ActivityPanelLaunchParameters;
+import androidx.xr.extensions.space.Bounds;
+import androidx.xr.extensions.space.HitTestResult;
+import androidx.xr.extensions.space.SpatialCapabilities;
+import androidx.xr.extensions.space.SpatialState;
+import androidx.xr.extensions.splitengine.SplitEngineBridge;
+import androidx.xr.extensions.subspace.Subspace;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
+import java.io.Closeable;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+
+/**
+ * A fake for the XrExtensions.
+ *
+ * <p>This has fake implementations for a subset of the XrExtension capability that is used by the
+ * JXRCore runtime for AndroidXR.
+ */
+@SuppressWarnings("deprecation")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class FakeXrExtensions implements XrExtensions {
+ private static final String NOT_IMPLEMENTED_IN_FAKE =
+ "This function is not implemented yet in FakeXrExtensions. Please add an"
+ + " implementation if support is desired for testing.";
+
+ @NonNull public final List<FakeNode> createdNodes = new ArrayList<>();
+
+ @NonNull public final List<FakeGltfModelToken> createdGltfModelTokens = new ArrayList<>();
+
+ @NonNull public final List<FakeEnvironmentToken> createdEnvironmentTokens = new ArrayList<>();
+
+ @NonNull public final Map<Activity, FakeActivityPanel> activityPanelMap = new HashMap<>();
+
+ FakeNode fakeTaskNode = null;
+ FakeNode fakeEnvironmentNode = null;
+ FakeNode fakeNodeForMainWindow = null;
+
+ // TODO: b/370033054 - fakeSpatialState should be updated according to some fake extensions
+ // calls
+ // like requestFullSpaceMode after migration to SpatialState API
+ @NonNull public final FakeSpatialState fakeSpatialState = new FakeSpatialState();
+
+ // Technically this could be set per-activity, but we're assuming that there's a single activity
+ // associated with each JXRCore session, so we're only tracking it once for now.
+ SpaceMode spaceMode = SpaceMode.NONE;
+
+ int mainWindowWidth = 0;
+ int mainWindowHeight = 0;
+
+ Consumer<SpatialState> spatialStateCallback = null;
+
+ float preferredAspectRatioHsm = 0.0f;
+ int openXrWorldSpaceType = 0;
+ FakeNodeTransaction lastFakeNodeTransaction = null;
+
+ @NonNull
+ public final FakeSpatialAudioExtensions fakeSpatialAudioExtensions =
+ new FakeSpatialAudioExtensions();
+
+ @Nullable
+ public FakeNode getFakeEnvironmentNode() {
+ return fakeEnvironmentNode;
+ }
+
+ @Nullable
+ public FakeNode getFakeNodeForMainWindow() {
+ return fakeNodeForMainWindow;
+ }
+
+ @Override
+ public int getApiVersion() {
+ // The API surface is aligned with the initial XRU release.
+ return 1;
+ }
+
+ @Override
+ @NonNull
+ public Node createNode() {
+ FakeNode node = new FakeNode();
+ createdNodes.add(node);
+ return node;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction createNodeTransaction() {
+ lastFakeNodeTransaction = new FakeNodeTransaction();
+ return lastFakeNodeTransaction;
+ }
+
+ @Override
+ @NonNull
+ public Subspace createSubspace(@NonNull SplitEngineBridge splitEngineBridge, int subspaceId) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ /**
+ * @deprecated This method is no longer supported.
+ */
+ @Override
+ @NonNull
+ @Deprecated
+ public Bundle setMainPanelCurvatureRadius(@NonNull Bundle bundle, float panelCurvatureRadius) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ @Override
+ @NonNull
+ public Config getConfig() {
+ return new FakeConfig();
+ }
+
+ @NonNull
+ public SpaceMode getSpaceMode() {
+ return spaceMode;
+ }
+
+ /**
+ * @deprecated This method is no longer supported.
+ */
+ @Override
+ @Deprecated
+ public void setMainWindowSize(@NonNull Activity activity, int width, int height) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ @Override
+ public void setMainWindowSize(
+ @NonNull Activity activity,
+ int width,
+ int height,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor) {
+ mainWindowWidth = width;
+ mainWindowHeight = height;
+ executor.execute(() -> callback.accept(createAsyncResult()));
+ }
+
+ /**
+ * @deprecated This method is no longer supported.
+ */
+ @Override
+ @Deprecated
+ public void setMainWindowCurvatureRadius(@NonNull Activity activity, float curvatureRadius) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ public int getMainWindowWidth() {
+ return mainWindowWidth;
+ }
+
+ public int getMainWindowHeight() {
+ return mainWindowHeight;
+ }
+
+ @Override
+ public void getBounds(
+ @NonNull Activity activity,
+ @NonNull Consumer<Bounds> callback,
+ @NonNull Executor executor) {
+ callback.accept(
+ (spaceMode == SpaceMode.FULL_SPACE
+ ? new Bounds(
+ Float.POSITIVE_INFINITY,
+ Float.POSITIVE_INFINITY,
+ Float.POSITIVE_INFINITY)
+ : new Bounds(1f, 1f, 1f)));
+ }
+
+ private SpatialCapabilities getCapabilities(boolean allowAll) {
+ return new SpatialCapabilities() {
+ @Override
+ public boolean get(int capQuery) {
+ return allowAll;
+ }
+ };
+ }
+
+ @Override
+ public void getSpatialCapabilities(
+ @NonNull Activity activity,
+ @NonNull Consumer<SpatialCapabilities> callback,
+ @NonNull Executor executor) {
+ callback.accept(fakeSpatialState.getSpatialCapabilities());
+ }
+
+ @Override
+ @NonNull
+ public SpatialState getSpatialState(@NonNull Activity activity) {
+ return fakeSpatialState;
+ }
+
+ /**
+ * @deprecated This method is no longer supported.
+ */
+ @Override
+ @Deprecated
+ public boolean canEmbedActivityPanel(@NonNull Activity activity) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ /**
+ * @deprecated This method is no longer supported.
+ */
+ @Override
+ @Deprecated
+ public boolean requestFullSpaceMode(@NonNull Activity activity) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ @Override
+ public void requestFullSpaceMode(
+ @NonNull Activity activity,
+ boolean requestEnter,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor) {
+ FakeSpatialState spatialState = new FakeSpatialState();
+ spatialState.bounds =
+ requestEnter
+ ? new Bounds(
+ Float.POSITIVE_INFINITY,
+ Float.POSITIVE_INFINITY,
+ Float.POSITIVE_INFINITY)
+ : new Bounds(10f, 10f, 10f);
+ spatialState.capabilities = getCapabilities(requestEnter);
+ sendSpatialState(spatialState);
+ spaceMode = requestEnter ? SpaceMode.FULL_SPACE : SpaceMode.HOME_SPACE;
+
+ executor.execute(() -> callback.accept(createAsyncResult()));
+ }
+
+ /**
+ * @deprecated This method is no longer supported.
+ */
+ @Override
+ @Deprecated
+ public boolean requestHomeSpaceMode(@NonNull Activity activity) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ /**
+ * @deprecated This method is no longer supported.
+ */
+ @Override
+ @Deprecated
+ public void setSpatialStateCallback(
+ @NonNull Activity activity,
+ @NonNull Consumer<androidx.xr.extensions.space.SpatialStateEvent> callback,
+ @NonNull Executor executor) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ @Override
+ @SuppressLint("PairedRegistration")
+ public void registerSpatialStateCallback(
+ @NonNull Activity activity,
+ @NonNull Consumer<SpatialState> callback,
+ @NonNull Executor executor) {
+ // note that we assume this is only called for the (single) primary activity associated with
+ // the JXRCore session and we also don't honor the executor here
+ spatialStateCallback = callback;
+ }
+
+ @Override
+ public void clearSpatialStateCallback(@NonNull Activity activity) {
+ spatialStateCallback = null;
+ }
+
+ // Method for tests to call to trigger the spatial state callback.
+ // This should probably be called on the provided executor.
+ public void sendSpatialState(@NonNull SpatialState spatialState) {
+ if (spatialStateCallback != null) {
+ spatialStateCallback.accept(spatialState);
+ }
+ }
+
+ @Nullable
+ public Consumer<SpatialState> getSpatialStateCallback() {
+ return spatialStateCallback;
+ }
+
+ private XrExtensionResult createAsyncResult() {
+ return new XrExtensionResult() {
+ @Override
+ public int getResult() {
+ return XrExtensionResult.XR_RESULT_SUCCESS;
+ }
+ };
+ }
+
+ /**
+ * @deprecated This method is no longer supported.
+ */
+ @Override
+ @Deprecated
+ public void attachSpatialScene(
+ @NonNull Activity activity, @NonNull Node sceneNode, @NonNull Node windowNode) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ @Override
+ public void attachSpatialScene(
+ @NonNull Activity activity,
+ @NonNull Node sceneNode,
+ @NonNull Node windowNode,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor) {
+ fakeTaskNode = (FakeNode) sceneNode;
+ fakeTaskNode.name = "taskNode";
+
+ fakeNodeForMainWindow = (FakeNode) windowNode;
+ fakeNodeForMainWindow.name = "nodeForMainWindow";
+
+ executor.execute(() -> callback.accept(createAsyncResult()));
+ }
+
+ /**
+ * @deprecated This method is no longer supported.
+ */
+ @Override
+ @Deprecated
+ public void detachSpatialScene(@NonNull Activity activity) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ @Override
+ public void detachSpatialScene(
+ @NonNull Activity activity,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor) {
+ fakeTaskNode = null;
+ fakeNodeForMainWindow = null;
+
+ executor.execute(() -> callback.accept(createAsyncResult()));
+ }
+
+ @Nullable
+ public FakeNode getFakeTaskNode() {
+ return fakeTaskNode;
+ }
+
+ /**
+ * @deprecated This method is no longer supported.
+ */
+ @Override
+ @Deprecated
+ public void attachSpatialEnvironment(
+ @NonNull Activity activity, @NonNull Node environmentNode) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ @Override
+ public void attachSpatialEnvironment(
+ @NonNull Activity activity,
+ @NonNull Node environmentNode,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor) {
+ fakeEnvironmentNode = (FakeNode) environmentNode;
+ fakeEnvironmentNode.name = "environmentNode";
+
+ executor.execute(() -> callback.accept(createAsyncResult()));
+ }
+
+ /**
+ * @deprecated This method is no longer supported.
+ */
+ @Override
+ @Deprecated
+ public void detachSpatialEnvironment(@NonNull Activity activity) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ @Override
+ public void detachSpatialEnvironment(
+ @NonNull Activity activity,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor) {
+ fakeEnvironmentNode = null;
+
+ executor.execute(() -> callback.accept(createAsyncResult()));
+ }
+
+ /**
+ * Suppressed to allow CompletableFuture.
+ *
+ * @deprecated This method is no longer supported.
+ */
+ @SuppressWarnings({"AndroidJdkLibsChecker", "BadFuture"})
+ @Override
+ @NonNull
+ @Deprecated
+ public CompletableFuture</* @Nullable */ androidx.xr.extensions.asset.GltfModelToken>
+ loadGltfModel(
+ @Nullable InputStream asset,
+ int regionSizeBytes,
+ int regionOffsetBytes,
+ @Nullable String url) {
+ FakeGltfModelToken modelToken = new FakeGltfModelToken(url);
+ createdGltfModelTokens.add(modelToken);
+ return CompletableFuture.completedFuture(modelToken);
+ }
+
+ /**
+ * Suppressed to allow CompletableFuture.
+ *
+ * @deprecated This method is no longer supported.
+ */
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ @Override
+ @NonNull
+ @Deprecated
+ public CompletableFuture</* @Nullable */ SceneViewerResult> displayGltfModel(
+ Activity activity, androidx.xr.extensions.asset.GltfModelToken gltfModel) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ /**
+ * Suppressed to allow CompletableFuture.
+ *
+ * @deprecated This method is no longer supported.
+ */
+ @SuppressWarnings({"AndroidJdkLibsChecker", "BadFuture"})
+ @Override
+ @NonNull
+ @Deprecated
+ // public ListenableFuture</* @Nullable */ androidx.xr.extensions.asset.EnvironmentToken>
+ // loadEnvironment(
+ public CompletableFuture</* @Nullable */ androidx.xr.extensions.asset.EnvironmentToken>
+ loadEnvironment(
+ @Nullable InputStream asset,
+ int regionSizeBytes,
+ int regionOffsetBytes,
+ @Nullable String url) {
+ FakeEnvironmentToken imageToken = new FakeEnvironmentToken(url);
+ createdEnvironmentTokens.add(imageToken);
+ // return immediateFuture(imageToken);
+ return CompletableFuture.completedFuture(imageToken);
+ }
+
+ /**
+ * Suppressed to allow CompletableFuture.
+ *
+ * @deprecated This method is no longer supported.
+ */
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ @Override
+ @NonNull
+ @Deprecated
+ public CompletableFuture</* @Nullable */ androidx.xr.extensions.asset.EnvironmentToken>
+ loadEnvironment(
+ InputStream asset,
+ int regionSizeBytes,
+ int regionOffsetBytes,
+ String url,
+ int textureWidth,
+ int textureHeight) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ /**
+ * Suppressed to allow CompletableFuture.
+ *
+ * @deprecated This method is no longer supported.
+ */
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ @Override
+ @Deprecated
+ @NonNull
+ public CompletableFuture</* @Nullable */ androidx.xr.extensions.asset.SceneToken>
+ loadImpressScene(InputStream asset, int regionSizeBytes, int regionOffsetBytes) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ @Override
+ @NonNull
+ public SplitEngineBridge createSplitEngineBridge() {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ /**
+ * Returns a FakeNode with corresponding gltfModelToken if it was created and found
+ *
+ * @deprecated This method is no longer supported.
+ */
+ @NonNull
+ @Deprecated
+ public FakeNode testGetNodeWithGltfToken(
+ @NonNull androidx.xr.extensions.asset.GltfModelToken token) {
+ for (FakeNode node : createdNodes) {
+ if (node.gltfModel != null && node.gltfModel.equals(token)) {
+ return node;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a FakeNode with corresponding environmentToken if it was created and found
+ *
+ * @deprecated This method is no longer supported.
+ */
+ @NonNull
+ @Deprecated
+ public FakeNode testGetNodeWithEnvironmentToken(
+ @NonNull androidx.xr.extensions.asset.EnvironmentToken token) {
+ for (FakeNode node : createdNodes) {
+ if (node.environment != null && node.environment.equals(token)) {
+ return node;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ @NonNull
+ public ActivityPanel createActivityPanel(
+ @NonNull Activity host, @NonNull ActivityPanelLaunchParameters launchParameters) {
+ FakeActivityPanel fakeActivityPanel = new FakeActivityPanel(createNode());
+ activityPanelMap.put(host, fakeActivityPanel);
+ return fakeActivityPanel;
+ }
+
+ @NonNull
+ public FakeActivityPanel getActivityPanelForHost(@NonNull Activity host) {
+ return activityPanelMap.get(host);
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions createReformOptions(
+ @NonNull Consumer<ReformEvent> callback, @NonNull Executor executor) {
+ return new FakeReformOptions(callback, executor);
+ }
+
+ @Override
+ public void addFindableView(@NonNull View view, @NonNull ViewGroup group) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ @Override
+ public void removeFindableView(@NonNull View view, @NonNull ViewGroup group) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ @Override
+ @Nullable
+ public Node getSurfaceTrackingNode(@NonNull View view) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ @Override
+ public void hitTest(
+ @NonNull Activity activity,
+ @NonNull Vec3 origin,
+ @NonNull Vec3 direction,
+ @NonNull Consumer<HitTestResult> callback,
+ @NonNull Executor executor) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ @Override
+ public int getOpenXrWorldSpaceType() {
+ return openXrWorldSpaceType;
+ }
+
+ public void setOpenXrWorldSpaceType(int openXrWorldSpaceType) {
+ this.openXrWorldSpaceType = openXrWorldSpaceType;
+ }
+
+ @Override
+ @NonNull
+ public Bundle setFullSpaceMode(@NonNull Bundle bundle) {
+ return bundle;
+ }
+
+ @Override
+ @NonNull
+ public Bundle setFullSpaceModeWithEnvironmentInherited(@NonNull Bundle bundle) {
+ return bundle;
+ }
+
+ @Override
+ public void setPreferredAspectRatio(@NonNull Activity activity, float preferredRatio) {
+ throw new UnsupportedOperationException(NOT_IMPLEMENTED_IN_FAKE);
+ }
+
+ @Override
+ public void setPreferredAspectRatio(
+ @NonNull Activity activity,
+ float preferredRatio,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor) {
+ preferredAspectRatioHsm = preferredRatio;
+
+ executor.execute(() -> callback.accept(createAsyncResult()));
+ }
+
+ public float getPreferredAspectRatio() {
+ return preferredAspectRatioHsm;
+ }
+
+ @NonNull
+ @Override
+ public XrSpatialAudioExtensions getXrSpatialAudioExtensions() {
+ return fakeSpatialAudioExtensions;
+ }
+
+ /** Tracks whether an Activity has requested a mode for when it's focused. */
+ public enum SpaceMode {
+ NONE,
+ HOME_SPACE,
+ FULL_SPACE
+ }
+
+ /** Fake implementation of Extensions Config. */
+ public static class FakeConfig implements Config {
+ public static final float DEFAULT_PIXELS_PER_METER = 1f;
+
+ @Override
+ public float defaultPixelsPerMeter(float density) {
+ return DEFAULT_PIXELS_PER_METER;
+ }
+ }
+
+ /** A fake implementation of Closeable. */
+ @SuppressWarnings("NotCloseable")
+ public static class FakeCloseable implements Closeable {
+ boolean closed = false;
+
+ @Override
+ public void close() {
+ closed = true;
+ }
+
+ public boolean isClosed() {
+ return closed;
+ }
+ }
+
+ /** A fake implementation of the XR extensions Node. */
+ @SuppressWarnings("ParcelCreator")
+ public static final class FakeNode implements Node {
+ FakeNode parent = null;
+ float xPosition = 0.0f;
+ float yPosition = 0.0f;
+ float zPosition = 0.0f;
+ float xOrientation = 0.0f;
+ float yOrientation = 0.0f;
+ float zOrientation = 0.0f;
+ float wOrientation = 1.0f;
+ float xScale = 1.0f;
+ float yScale = 1.0f;
+ float zScale = 1.0f;
+ float cornerRadius = 0.0f;
+ boolean isVisible = false;
+ float alpha = 1.0f;
+ androidx.xr.extensions.asset.GltfModelToken gltfModel = null;
+ IBinder anchorId = null;
+ String name = null;
+ float passthroughOpacity = 1.0f;
+ @PassthroughState.Mode int passthroughMode = 0;
+ SurfaceControlViewHost.SurfacePackage surfacePackage = null;
+ androidx.xr.extensions.asset.EnvironmentToken environment = null;
+ Consumer<InputEvent> listener = null;
+ Consumer<NodeTransform> transformListener = null;
+ Consumer<Integer> pointerCaptureStateCallback = null;
+
+ Executor executor = null;
+ ReformOptions reformOptions;
+ Executor transformExecutor = null;
+
+ private FakeNode() {}
+
+ @Override
+ public void listenForInput(
+ @NonNull Consumer<InputEvent> listener, @NonNull Executor executor) {
+ this.listener = listener;
+ this.executor = executor;
+ }
+
+ @Override
+ public void stopListeningForInput() {
+ listener = null;
+ executor = null;
+ }
+
+ @Override
+ public void setNonPointerFocusTarget(@NonNull AttachedSurfaceControl focusTarget) {}
+
+ @Override
+ public void requestPointerCapture(
+ @NonNull Consumer<Integer> stateCallback, @NonNull Executor executor) {
+ pointerCaptureStateCallback = stateCallback;
+ }
+
+ @Override
+ public void stopPointerCapture() {
+ pointerCaptureStateCallback = null;
+ }
+
+ public void sendInputEvent(@NonNull InputEvent event) {
+ executor.execute(() -> listener.accept(event));
+ }
+
+ public void sendTransformEvent(@NonNull FakeNodeTransform nodeTransform) {
+ transformExecutor.execute(() -> transformListener.accept(nodeTransform));
+ }
+
+ @Override
+ @NonNull
+ public Closeable subscribeToTransform(
+ @NonNull Consumer<NodeTransform> transformCallback, @NonNull Executor executor) {
+ this.transformListener = transformCallback;
+ this.transformExecutor = executor;
+ return new FakeCloseable();
+ }
+
+ @Nullable
+ public Consumer<NodeTransform> getTransformListener() {
+ return transformListener;
+ }
+
+ @Nullable
+ public Executor getTransformExecutor() {
+ return transformExecutor;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel in, int flags) {}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Nullable
+ public FakeNode getParent() {
+ return parent;
+ }
+
+ public float getXPosition() {
+ return xPosition;
+ }
+
+ public float getYPosition() {
+ return yPosition;
+ }
+
+ public float getZPosition() {
+ return zPosition;
+ }
+
+ public float getXOrientation() {
+ return xOrientation;
+ }
+
+ public float getYOrientation() {
+ return yOrientation;
+ }
+
+ public float getZOrientation() {
+ return zOrientation;
+ }
+
+ public float getWOrientation() {
+ return wOrientation;
+ }
+
+ public boolean isVisible() {
+ return isVisible;
+ }
+
+ public float getAlpha() {
+ return alpha;
+ }
+
+ /**
+ * @deprecated This method is no longer supported.
+ */
+ @Nullable
+ @Deprecated
+ public androidx.xr.extensions.asset.GltfModelToken getGltfModel() {
+ return gltfModel;
+ }
+
+ @Nullable
+ public IBinder getAnchorId() {
+ return anchorId;
+ }
+
+ @Nullable
+ public String getName() {
+ return name;
+ }
+
+ @Nullable
+ public SurfaceControlViewHost.SurfacePackage getSurfacePackage() {
+ return surfacePackage;
+ }
+
+ /**
+ * @deprecated This method is no longer supported.
+ */
+ @Nullable
+ @Deprecated
+ public androidx.xr.extensions.asset.EnvironmentToken getEnvironment() {
+ return environment;
+ }
+
+ @Nullable
+ public Consumer<InputEvent> getListener() {
+ return listener;
+ }
+
+ @Nullable
+ public Consumer<Integer> getPointerCaptureStateCallback() {
+ return pointerCaptureStateCallback;
+ }
+
+ @Nullable
+ public Executor getExecutor() {
+ return executor;
+ }
+
+ @Nullable
+ public ReformOptions getReformOptions() {
+ return reformOptions;
+ }
+ }
+
+ /**
+ * A fake implementation of the XR extensions Node transaction.
+ *
+ * <p>All modifications happen immediately and not when the transaction is applied.
+ */
+ @SuppressWarnings("NotCloseable")
+ public static class FakeNodeTransaction implements NodeTransaction {
+ FakeNode lastFakeNode = null;
+ boolean applied = false;
+
+ private FakeNodeTransaction() {}
+
+ @Override
+ @NonNull
+ public NodeTransaction setParent(@NonNull Node node, @Nullable Node parent) {
+ lastFakeNode = (FakeNode) node;
+ ((FakeNode) node).parent = (FakeNode) parent;
+ return this;
+ }
+
+ /**
+ * @deprecated This method is no longer supported.
+ */
+ @Override
+ @NonNull
+ @Deprecated
+ public NodeTransaction setEnvironment(
+ @NonNull Node node, @Nullable androidx.xr.extensions.asset.EnvironmentToken token) {
+ lastFakeNode = (FakeNode) node;
+ ((FakeNode) node).environment = token;
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setPosition(@NonNull Node node, float x, float y, float z) {
+ lastFakeNode = (FakeNode) node;
+ ((FakeNode) node).xPosition = x;
+ ((FakeNode) node).yPosition = y;
+ ((FakeNode) node).zPosition = z;
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setOrientation(
+ @NonNull Node node, float x, float y, float z, float w) {
+ lastFakeNode = (FakeNode) node;
+ ((FakeNode) node).xOrientation = x;
+ ((FakeNode) node).yOrientation = y;
+ ((FakeNode) node).zOrientation = z;
+ ((FakeNode) node).wOrientation = w;
+ return this;
+ }
+
+ @Override
+ @NonNull
+ // TODO(b/354731545): Cover this with an AndroidXREntity test
+ public NodeTransaction setScale(@NonNull Node node, float sx, float sy, float sz) {
+ lastFakeNode = (FakeNode) node;
+ ((FakeNode) node).xScale = sx;
+ ((FakeNode) node).yScale = sy;
+ ((FakeNode) node).zScale = sz;
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setVisibility(@NonNull Node node, boolean isVisible) {
+ lastFakeNode = (FakeNode) node;
+ ((FakeNode) node).isVisible = isVisible;
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setAlpha(@NonNull Node node, float value) {
+ lastFakeNode = (FakeNode) node;
+ ((FakeNode) node).alpha = value;
+ return this;
+ }
+
+ /**
+ * @deprecated This method is no longer supported.
+ */
+ @Override
+ @NonNull
+ @Deprecated
+ public NodeTransaction setGltfModel(
+ @NonNull Node node,
+ @NonNull androidx.xr.extensions.asset.GltfModelToken gltfModelToken) {
+ lastFakeNode = (FakeNode) node;
+ ((FakeNode) node).gltfModel = gltfModelToken;
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setName(@NonNull Node node, @NonNull String name) {
+ lastFakeNode = (FakeNode) node;
+ ((FakeNode) node).name = name;
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setPassthroughState(
+ @NonNull Node node,
+ float passthroughOpacity,
+ @PassthroughState.Mode int passthroughMode) {
+ lastFakeNode = (FakeNode) node;
+ ((FakeNode) node).passthroughOpacity = passthroughOpacity;
+ ((FakeNode) node).passthroughMode = passthroughMode;
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setSurfacePackage(
+ @Nullable Node node,
+ @NonNull SurfaceControlViewHost.SurfacePackage surfacePackage) {
+ lastFakeNode = (FakeNode) node;
+ ((FakeNode) node).surfacePackage = surfacePackage;
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setWindowBounds(
+ @NonNull SurfaceControlViewHost.SurfacePackage surfacePackage,
+ int widthPx,
+ int heightPx) {
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setAnchorId(@NonNull Node node, @Nullable IBinder anchorId) {
+ lastFakeNode = (FakeNode) node;
+ ((FakeNode) node).anchorId = anchorId;
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction enableReform(@NonNull Node node, @NonNull ReformOptions options) {
+ lastFakeNode = (FakeNode) node;
+ ((FakeNode) node).reformOptions = options;
+ ((FakeReformOptions) options).optionsApplied = true;
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setReformSize(@NonNull Node node, @NonNull Vec3 reformSize) {
+ lastFakeNode = (FakeNode) node;
+ ((FakeNode) node).reformOptions.setCurrentSize(reformSize);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction disableReform(@NonNull Node node) {
+ lastFakeNode = (FakeNode) node;
+ ((FakeNode) node).reformOptions = new FakeReformOptions(null, null);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setCornerRadius(@NonNull Node node, float cornerRadius) {
+ lastFakeNode = (FakeNode) node;
+ ((FakeNode) node).cornerRadius = cornerRadius;
+ return this;
+ }
+
+ @Override
+ public void apply() {
+ applied = true;
+ }
+
+ @Override
+ public void close() {}
+ }
+
+ /** A fake implementation of the XR extensions NodeTransform. */
+ public static class FakeNodeTransform implements NodeTransform {
+ Mat4f transform;
+
+ public FakeNodeTransform(@NonNull Mat4f transform) {
+ this.transform = transform;
+ }
+
+ @Override
+ @NonNull
+ public Mat4f getTransform() {
+ return transform;
+ }
+
+ @Override
+ public long getTimestamp() {
+ return 0;
+ }
+ }
+
+ /** A fake implementation of the XR extensions GltfModelToken. */
+ public static class FakeGltfModelToken implements androidx.xr.extensions.asset.GltfModelToken {
+ String url;
+
+ public FakeGltfModelToken(@NonNull String url) {
+ this.url = url;
+ }
+
+ @NonNull
+ public String getUrl() {
+ return url;
+ }
+ }
+
+ /** A fake implementation of the XR extensions EnvironmentToken. */
+ public static class FakeEnvironmentToken
+ implements androidx.xr.extensions.asset.EnvironmentToken {
+ String url;
+
+ private FakeEnvironmentToken(@NonNull String url) {
+ this.url = url;
+ }
+
+ @NonNull
+ public String getUrl() {
+ return url;
+ }
+ }
+
+ /** A fake implementation of the XR extensions EnvironmentVisibilityState. */
+ public static class FakeEnvironmentVisibilityState implements EnvironmentVisibilityState {
+ @EnvironmentVisibilityState.State int state;
+
+ public FakeEnvironmentVisibilityState(@EnvironmentVisibilityState.State int state) {
+ this.state = state;
+ }
+
+ @Override
+ @EnvironmentVisibilityState.State
+ public int getCurrentState() {
+ return state;
+ }
+ }
+
+ /** A fake implementation of the XR extensions EnvironmentVisibilityState. */
+ public static class FakePassthroughVisibilityState implements PassthroughVisibilityState {
+ @PassthroughVisibilityState.State int state;
+ float opacity;
+
+ public FakePassthroughVisibilityState(
+ @PassthroughVisibilityState.State int state, float opacity) {
+ this.state = state;
+ this.opacity = opacity;
+ }
+
+ @Override
+ @PassthroughVisibilityState.State
+ public int getCurrentState() {
+ return state;
+ }
+
+ @Override
+ public float getOpacity() {
+ return opacity;
+ }
+ }
+
+ /** Creates fake activity panel. */
+ public static class FakeActivityPanel implements ActivityPanel {
+ Intent launchIntent;
+ Bundle bundle;
+ Activity activity;
+ Rect bounds;
+ boolean isDeleted = false;
+ Node node;
+
+ FakeActivityPanel(@NonNull Node node) {
+ this.node = node;
+ }
+
+ @Override
+ public void launchActivity(@NonNull Intent intent, @Nullable Bundle options) {
+ launchIntent = intent;
+ this.bundle = options;
+ }
+
+ @Nullable
+ public Intent getLaunchIntent() {
+ return launchIntent;
+ }
+
+ @NonNull
+ public Bundle getBundle() {
+ return bundle;
+ }
+
+ @Override
+ public void moveActivity(@NonNull Activity activity) {
+ this.activity = activity;
+ }
+
+ @Nullable
+ public Activity getActivity() {
+ return activity;
+ }
+
+ @NonNull
+ @Override
+ public Node getNode() {
+ return node;
+ }
+
+ @Override
+ public void setWindowBounds(@NonNull Rect windowBounds) {
+ bounds = windowBounds;
+ }
+
+ @Nullable
+ public Rect getBounds() {
+ return bounds;
+ }
+
+ @Override
+ public void delete() {
+ isDeleted = true;
+ }
+
+ public boolean isDeleted() {
+ return isDeleted;
+ }
+ }
+
+ /** Fake input event. */
+ public static class FakeInputEvent implements InputEvent {
+ int source;
+ int pointerType;
+ long timestamp;
+ Vec3 origin;
+ Vec3 direction;
+ FakeHitInfo hitInfo;
+ FakeHitInfo secondaryHitInfo;
+ int dispatchFlags;
+ int action;
+
+ @Override
+ public int getSource() {
+ return source;
+ }
+
+ @Override
+ public int getPointerType() {
+ return pointerType;
+ }
+
+ @Override
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ @Override
+ @NonNull
+ public Vec3 getOrigin() {
+ return origin;
+ }
+
+ @Override
+ @NonNull
+ public Vec3 getDirection() {
+ return direction;
+ }
+
+ @Override
+ @Nullable
+ public HitInfo getHitInfo() {
+ return hitInfo;
+ }
+
+ @Override
+ @Nullable
+ public HitInfo getSecondaryHitInfo() {
+ return secondaryHitInfo;
+ }
+
+ @Override
+ public int getDispatchFlags() {
+ return dispatchFlags;
+ }
+
+ @Override
+ public int getAction() {
+ return action;
+ }
+
+ public void setDispatchFlags(int dispatchFlags) {
+ this.dispatchFlags = dispatchFlags;
+ }
+
+ public void setOrigin(@NonNull Vec3 origin) {
+ this.origin = origin;
+ }
+
+ public void setDirection(@NonNull Vec3 direction) {
+ this.direction = direction;
+ }
+
+ public void setFakeHitInfo(@NonNull FakeHitInfo hitInfo) {
+ this.hitInfo = hitInfo;
+ }
+
+ public void setTimestamp(long timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ /** Fake hit info. */
+ public static class FakeHitInfo implements InputEvent.HitInfo {
+ int subspaceImpressNodeId;
+ Node inputNode;
+ Vec3 hitPosition;
+ Mat4f transform;
+
+ @Override
+ public int getSubspaceImpressNodeId() {
+ return subspaceImpressNodeId;
+ }
+
+ @Override
+ @NonNull
+ public Node getInputNode() {
+ return inputNode;
+ }
+
+ @Override
+ @Nullable
+ public Vec3 getHitPosition() {
+ return hitPosition;
+ }
+
+ @Override
+ @NonNull
+ public Mat4f getTransform() {
+ return transform;
+ }
+
+ public void setSubspaceImpressNodeId(int subspaceImpressNodeId) {
+ this.subspaceImpressNodeId = subspaceImpressNodeId;
+ }
+
+ public void setInputNode(@NonNull Node inputNode) {
+ this.inputNode = inputNode;
+ }
+
+ public void setHitPosition(@Nullable Vec3 hitPosition) {
+ this.hitPosition = hitPosition;
+ }
+
+ public void setTransform(@NonNull Mat4f transform) {
+ this.transform = transform;
+ }
+ }
+ }
+
+ /** Fake ReformOptions. */
+ public static class FakeReformOptions implements ReformOptions {
+
+ int enabledReforms;
+ int reformFlags;
+ int scaleWithDistanceMode = SCALE_WITH_DISTANCE_MODE_DEFAULT;
+ Vec3 currentSize;
+ Vec3 minimumSize;
+ Vec3 maximumSize;
+ float fixedAspectRatio;
+ Consumer<ReformEvent> consumer;
+ Executor executor;
+
+ boolean optionsApplied = true;
+
+ FakeReformOptions(Consumer<ReformEvent> consumer, Executor executor) {
+ this.consumer = consumer;
+ this.executor = executor;
+ }
+
+ @Override
+ public int getEnabledReform() {
+ return enabledReforms;
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions setEnabledReform(int i) {
+ enabledReforms = i;
+ return this;
+ }
+
+ @Override
+ public int getFlags() {
+ return reformFlags;
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions setFlags(int i) {
+ optionsApplied = false;
+ reformFlags = i;
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public Vec3 getCurrentSize() {
+ return currentSize;
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions setCurrentSize(@NonNull Vec3 vec3) {
+ optionsApplied = false;
+ currentSize = vec3;
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public Vec3 getMinimumSize() {
+ return minimumSize;
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions setMinimumSize(@NonNull Vec3 vec3) {
+ optionsApplied = false;
+ minimumSize = vec3;
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public Vec3 getMaximumSize() {
+ return maximumSize;
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions setMaximumSize(@NonNull Vec3 vec3) {
+ optionsApplied = false;
+ maximumSize = vec3;
+ return this;
+ }
+
+ @Override
+ public float getFixedAspectRatio() {
+ return fixedAspectRatio;
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions setFixedAspectRatio(float fixedAspectRatio) {
+ this.fixedAspectRatio = fixedAspectRatio;
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public Consumer<ReformEvent> getEventCallback() {
+ return consumer;
+ }
+
+ @Override
+ @NonNull
+ @SuppressLint("InvalidNullabilityOverride")
+ public ReformOptions setEventCallback(@NonNull Consumer<ReformEvent> consumer) {
+ this.consumer = consumer;
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public Executor getEventExecutor() {
+ return executor;
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions setEventExecutor(@NonNull Executor executor) {
+ this.executor = executor;
+ return this;
+ }
+
+ @Override
+ public int getScaleWithDistanceMode() {
+ return scaleWithDistanceMode;
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions setScaleWithDistanceMode(int scaleWithDistanceMode) {
+ this.scaleWithDistanceMode = scaleWithDistanceMode;
+ return this;
+ }
+ }
+
+ /** Fake ReformEvent. */
+ public static class FakeReformEvent implements ReformEvent {
+
+ int type;
+ int state;
+ int id;
+ Vec3 origin = new Vec3(0f, 0f, 0f);
+ Vec3 initialRayOrigin = origin;
+ Vec3 proposedPosition = origin;
+ Vec3 ones = new Vec3(1f, 1f, 1f);
+ Vec3 initialRayDirection = ones;
+ Vec3 proposedScale = ones;
+ Vec3 proposedSize = ones;
+ Vec3 twos = new Vec3(2f, 2f, 2f);
+ Vec3 currentRayOrigin = twos;
+ Vec3 threes = new Vec3(3f, 3f, 3f);
+ Vec3 currentRayDirection = threes;
+ Quatf identity = new Quatf(0f, 0f, 0f, 1f);
+ Quatf proposedOrientation = identity;
+
+ public void setType(int type) {
+ this.type = type;
+ }
+
+ public void setState(int state) {
+ this.state = state;
+ }
+
+ public void setProposedPosition(@NonNull Vec3 proposedPosition) {
+ this.proposedPosition = proposedPosition;
+ }
+
+ public void setProposedScale(@NonNull Vec3 proposedScale) {
+ this.proposedScale = proposedScale;
+ }
+
+ public void setProposedOrientation(@NonNull Quatf proposedOrientation) {
+ this.proposedOrientation = proposedOrientation;
+ }
+
+ @Override
+ public int getType() {
+ return type;
+ }
+
+ @Override
+ public int getState() {
+ return state;
+ }
+
+ @Override
+ public int getId() {
+ return id;
+ }
+
+ @NonNull
+ @Override
+ public Vec3 getInitialRayOrigin() {
+ return initialRayOrigin;
+ }
+
+ @NonNull
+ @Override
+ public Vec3 getInitialRayDirection() {
+ return initialRayDirection;
+ }
+
+ @NonNull
+ @Override
+ public Vec3 getCurrentRayOrigin() {
+ return currentRayOrigin;
+ }
+
+ @NonNull
+ @Override
+ public Vec3 getCurrentRayDirection() {
+ return currentRayDirection;
+ }
+
+ @NonNull
+ @Override
+ public Vec3 getProposedPosition() {
+ return proposedPosition;
+ }
+
+ @NonNull
+ @Override
+ public Quatf getProposedOrientation() {
+ return proposedOrientation;
+ }
+
+ @NonNull
+ @Override
+ public Vec3 getProposedScale() {
+ return proposedScale;
+ }
+
+ @NonNull
+ @Override
+ public Vec3 getProposedSize() {
+ return proposedSize;
+ }
+ }
+
+ /** A fake implementation of the XR extensions SpatialState. */
+ public static class FakeSpatialState implements SpatialState {
+ Bounds bounds;
+ SpatialCapabilities capabilities;
+ EnvironmentVisibilityState environmentVisibilityState;
+ PassthroughVisibilityState passthroughVisibilityState;
+
+ public FakeSpatialState() {
+ // Initialize params to any non-null values
+ // TODO: b/370033054 - Revisit the default values for the bounds and capabilities.
+ this.bounds =
+ new Bounds(
+ Float.POSITIVE_INFINITY,
+ Float.POSITIVE_INFINITY,
+ Float.POSITIVE_INFINITY);
+ this.setAllSpatialCapabilities(true);
+ this.environmentVisibilityState =
+ new FakeEnvironmentVisibilityState(EnvironmentVisibilityState.INVISIBLE);
+ this.passthroughVisibilityState =
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.DISABLED, 0.0f);
+ }
+
+ @Override
+ @NonNull
+ public Bounds getBounds() {
+ return bounds;
+ }
+
+ public void setBounds(@NonNull Bounds bounds) {
+ this.bounds = bounds;
+ }
+
+ @Override
+ @NonNull
+ public SpatialCapabilities getSpatialCapabilities() {
+ return capabilities;
+ }
+
+ @Override
+ @NonNull
+ public EnvironmentVisibilityState getEnvironmentVisibility() {
+ return environmentVisibilityState;
+ }
+
+ public void setEnvironmentVisibility(
+ @NonNull EnvironmentVisibilityState environmentVisibilityState) {
+ this.environmentVisibilityState = environmentVisibilityState;
+ }
+
+ @Override
+ @NonNull
+ public PassthroughVisibilityState getPassthroughVisibility() {
+ return passthroughVisibilityState;
+ }
+
+ public void setPassthroughVisibility(
+ @NonNull PassthroughVisibilityState passthroughVisibilityState) {
+ this.passthroughVisibilityState = passthroughVisibilityState;
+ }
+
+ // Methods for tests to set the capabilities.
+ public void setSpatialCapabilities(@NonNull SpatialCapabilities capabilities) {
+ this.capabilities = capabilities;
+ }
+
+ public void setAllSpatialCapabilities(boolean allowAll) {
+ this.capabilities =
+ new SpatialCapabilities() {
+ @Override
+ public boolean get(int capQuery) {
+ return allowAll;
+ }
+ };
+ }
+ }
+
+ /** Fake XrSpatialAudioExtensions. */
+ public static class FakeSpatialAudioExtensions implements XrSpatialAudioExtensions {
+
+ @NonNull
+ public final FakeSoundPoolExtensions soundPoolExtensions = new FakeSoundPoolExtensions();
+
+ FakeAudioTrackExtensions audioTrackExtensions = new FakeAudioTrackExtensions();
+
+ @NonNull
+ public final FakeMediaPlayerExtensions mediaPlayerExtensions =
+ new FakeMediaPlayerExtensions();
+
+ @NonNull
+ @Override
+ public SoundPoolExtensions getSoundPoolExtensions() {
+ return soundPoolExtensions;
+ }
+
+ @NonNull
+ @Override
+ public AudioTrackExtensions getAudioTrackExtensions() {
+ return audioTrackExtensions;
+ }
+
+ public void setFakeAudioTrackExtensions(
+ @NonNull FakeAudioTrackExtensions audioTrackExtensions) {
+ this.audioTrackExtensions = audioTrackExtensions;
+ }
+
+ @NonNull
+ @Override
+ public MediaPlayerExtensions getMediaPlayerExtensions() {
+ return mediaPlayerExtensions;
+ }
+ }
+
+ /** Fake SoundPoolExtensions. */
+ public static class FakeSoundPoolExtensions implements SoundPoolExtensions {
+
+ int playAsPointSourceResult = 0;
+ int playAsSoundFieldResult = 0;
+ int sourceType = SpatializerExtensions.SOURCE_TYPE_BYPASS;
+
+ public void setPlayAsPointSourceResult(int result) {
+ playAsPointSourceResult = result;
+ }
+
+ public void setPlayAsSoundFieldResult(int result) {
+ playAsSoundFieldResult = result;
+ }
+
+ public void setSourceType(@SpatializerExtensions.SourceType int sourceType) {
+ this.sourceType = sourceType;
+ }
+
+ @Override
+ public int playAsPointSource(
+ @NonNull SoundPool soundPool,
+ int soundID,
+ @NonNull PointSourceAttributes attributes,
+ float volume,
+ int priority,
+ int loop,
+ float rate) {
+ return playAsPointSourceResult;
+ }
+
+ @Override
+ public int playAsSoundField(
+ @NonNull SoundPool soundPool,
+ int soundID,
+ @NonNull SoundFieldAttributes attributes,
+ float volume,
+ int priority,
+ int loop,
+ float rate) {
+ return playAsSoundFieldResult;
+ }
+
+ @Override
+ @SpatializerExtensions.SourceType
+ public int getSpatialSourceType(@NonNull SoundPool soundPool, int streamID) {
+ return sourceType;
+ }
+ }
+
+ /** Fake AudioTrackExtensions. */
+ public static class FakeAudioTrackExtensions implements AudioTrackExtensions {
+
+ PointSourceAttributes pointSourceAttributes;
+
+ SoundFieldAttributes soundFieldAttributes;
+
+ @SpatializerExtensions.SourceType int sourceType;
+
+ @CanIgnoreReturnValue
+ @Override
+ @NonNull
+ public AudioTrack.Builder setPointSourceAttributes(
+ @NonNull AudioTrack.Builder builder, @Nullable PointSourceAttributes attributes) {
+ this.pointSourceAttributes = attributes;
+ return builder;
+ }
+
+ public void setPointSourceAttributes(
+ @Nullable PointSourceAttributes pointSourceAttributes) {
+ this.pointSourceAttributes = pointSourceAttributes;
+ }
+
+ @CanIgnoreReturnValue
+ @Override
+ @NonNull
+ public AudioTrack.Builder setSoundFieldAttributes(
+ @NonNull AudioTrack.Builder builder, @Nullable SoundFieldAttributes attributes) {
+ this.soundFieldAttributes = attributes;
+ return builder;
+ }
+
+ public void setSoundFieldAttributes(@Nullable SoundFieldAttributes soundFieldAttributes) {
+ this.soundFieldAttributes = soundFieldAttributes;
+ }
+
+ @Override
+ @Nullable
+ public PointSourceAttributes getPointSourceAttributes(@NonNull AudioTrack track) {
+ return pointSourceAttributes;
+ }
+
+ @Nullable
+ public PointSourceAttributes getPointSourceAttributes() {
+ return pointSourceAttributes;
+ }
+
+ @Override
+ @Nullable
+ public SoundFieldAttributes getSoundFieldAttributes(@NonNull AudioTrack track) {
+ return soundFieldAttributes;
+ }
+
+ @Nullable
+ public SoundFieldAttributes getSoundFieldAttributes() {
+ return soundFieldAttributes;
+ }
+
+ @Override
+ public int getSpatialSourceType(@NonNull AudioTrack track) {
+ return sourceType;
+ }
+
+ public void setSourceType(@SpatializerExtensions.SourceType int sourceType) {
+ this.sourceType = sourceType;
+ }
+ }
+
+ /** Fake MediaPlayerExtensions. */
+ public static class FakeMediaPlayerExtensions implements MediaPlayerExtensions {
+
+ PointSourceAttributes pointSourceAttributes;
+
+ SoundFieldAttributes soundFieldAttributes;
+
+ @CanIgnoreReturnValue
+ @Override
+ @NonNull
+ public MediaPlayer setPointSourceAttributes(
+ @NonNull MediaPlayer mediaPlayer, @Nullable PointSourceAttributes attributes) {
+ this.pointSourceAttributes = attributes;
+ return mediaPlayer;
+ }
+
+ @CanIgnoreReturnValue
+ @Override
+ @NonNull
+ public MediaPlayer setSoundFieldAttributes(
+ @NonNull MediaPlayer mediaPlayer, @Nullable SoundFieldAttributes attributes) {
+ this.soundFieldAttributes = attributes;
+ return mediaPlayer;
+ }
+
+ @Nullable
+ public PointSourceAttributes getPointSourceAttributes() {
+ return pointSourceAttributes;
+ }
+
+ @Nullable
+ public SoundFieldAttributes getSoundFieldAttributes() {
+ return soundFieldAttributes;
+ }
+ }
+}
diff --git a/camera/camera-effects-still-portrait/api/current.txt b/xr/scenecore/scenecore/api/current.txt
similarity index 100%
copy from camera/camera-effects-still-portrait/api/current.txt
copy to xr/scenecore/scenecore/api/current.txt
diff --git a/camera/camera-effects-still-portrait/api/res-current.txt b/xr/scenecore/scenecore/api/res-current.txt
similarity index 100%
copy from camera/camera-effects-still-portrait/api/res-current.txt
copy to xr/scenecore/scenecore/api/res-current.txt
diff --git a/xr/scenecore/scenecore/api/restricted_current.txt b/xr/scenecore/scenecore/api/restricted_current.txt
new file mode 100644
index 0000000..762db0b
--- /dev/null
+++ b/xr/scenecore/scenecore/api/restricted_current.txt
@@ -0,0 +1,1879 @@
+// Signature format: 4.0
+package androidx.xr.extensions {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Config {
+ method public float defaultPixelsPerMeter(float);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.FunctionalInterface public interface Consumer<T> {
+ method public void accept(T!);
+ }
+
+ @SuppressCompatibility @RequiresOptIn(level=androidx.annotation.RequiresOptIn.Level.ERROR) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD}) public @interface ExperimentalExtensionApi {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class IBinderWrapper {
+ ctor public IBinderWrapper(android.os.IBinder);
+ method protected android.os.IBinder getRawToken();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface XrExtensionResult {
+ method @androidx.xr.extensions.XrExtensionResult.ResultType public default int getResult();
+ field @Deprecated public static final int XR_RESULT_ERROR_IGNORED = 3; // 0x3
+ field @Deprecated public static final int XR_RESULT_ERROR_INVALID_STATE = 2; // 0x2
+ field public static final int XR_RESULT_ERROR_NOT_ALLOWED = 3; // 0x3
+ field public static final int XR_RESULT_ERROR_SYSTEM = 4; // 0x4
+ field public static final int XR_RESULT_IGNORED_ALREADY_APPLIED = 2; // 0x2
+ field public static final int XR_RESULT_SUCCESS = 0; // 0x0
+ field public static final int XR_RESULT_SUCCESS_NOT_VISIBLE = 1; // 0x1
+ }
+
+ @IntDef({androidx.xr.extensions.XrExtensionResult.XR_RESULT_SUCCESS, androidx.xr.extensions.XrExtensionResult.XR_RESULT_SUCCESS_NOT_VISIBLE, androidx.xr.extensions.XrExtensionResult.XR_RESULT_IGNORED_ALREADY_APPLIED, androidx.xr.extensions.XrExtensionResult.XR_RESULT_ERROR_INVALID_STATE, androidx.xr.extensions.XrExtensionResult.XR_RESULT_ERROR_NOT_ALLOWED, androidx.xr.extensions.XrExtensionResult.XR_RESULT_ERROR_IGNORED, androidx.xr.extensions.XrExtensionResult.XR_RESULT_ERROR_SYSTEM}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface XrExtensionResult.ResultType {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface XrExtensions {
+ method public void addFindableView(android.view.View, android.view.ViewGroup);
+ method @Deprecated public void attachSpatialEnvironment(android.app.Activity, androidx.xr.extensions.node.Node);
+ method public void attachSpatialEnvironment(android.app.Activity, androidx.xr.extensions.node.Node, androidx.xr.extensions.Consumer<androidx.xr.extensions.XrExtensionResult!>, java.util.concurrent.Executor);
+ method @Deprecated public void attachSpatialScene(android.app.Activity, androidx.xr.extensions.node.Node, androidx.xr.extensions.node.Node);
+ method public void attachSpatialScene(android.app.Activity, androidx.xr.extensions.node.Node, androidx.xr.extensions.node.Node, androidx.xr.extensions.Consumer<androidx.xr.extensions.XrExtensionResult!>, java.util.concurrent.Executor);
+ method @Deprecated public boolean canEmbedActivityPanel(android.app.Activity);
+ method public void clearSpatialStateCallback(android.app.Activity);
+ method public androidx.xr.extensions.space.ActivityPanel createActivityPanel(android.app.Activity, androidx.xr.extensions.space.ActivityPanelLaunchParameters);
+ method public androidx.xr.extensions.node.Node createNode();
+ method public androidx.xr.extensions.node.NodeTransaction createNodeTransaction();
+ method public androidx.xr.extensions.node.ReformOptions createReformOptions(androidx.xr.extensions.Consumer<androidx.xr.extensions.node.ReformEvent!>, java.util.concurrent.Executor);
+ method public androidx.xr.extensions.splitengine.SplitEngineBridge createSplitEngineBridge();
+ method public androidx.xr.extensions.subspace.Subspace createSubspace(androidx.xr.extensions.splitengine.SplitEngineBridge, int);
+ method @Deprecated public void detachSpatialEnvironment(android.app.Activity);
+ method public void detachSpatialEnvironment(android.app.Activity, androidx.xr.extensions.Consumer<androidx.xr.extensions.XrExtensionResult!>, java.util.concurrent.Executor);
+ method @Deprecated public void detachSpatialScene(android.app.Activity);
+ method public void detachSpatialScene(android.app.Activity, androidx.xr.extensions.Consumer<androidx.xr.extensions.XrExtensionResult!>, java.util.concurrent.Executor);
+ method @Deprecated public java.util.concurrent.CompletableFuture<androidx.xr.extensions.XrExtensions.SceneViewerResult!> displayGltfModel(android.app.Activity!, androidx.xr.extensions.asset.GltfModelToken!);
+ method public int getApiVersion();
+ method @Deprecated public void getBounds(android.app.Activity, androidx.xr.extensions.Consumer<androidx.xr.extensions.space.Bounds!>, java.util.concurrent.Executor);
+ method public androidx.xr.extensions.Config getConfig();
+ method public int getOpenXrWorldSpaceType();
+ method @Deprecated public void getSpatialCapabilities(android.app.Activity, androidx.xr.extensions.Consumer<androidx.xr.extensions.space.SpatialCapabilities!>, java.util.concurrent.Executor);
+ method public androidx.xr.extensions.space.SpatialState getSpatialState(android.app.Activity);
+ method public androidx.xr.extensions.node.Node? getSurfaceTrackingNode(android.view.View);
+ method public androidx.xr.extensions.media.XrSpatialAudioExtensions getXrSpatialAudioExtensions();
+ method public void hitTest(android.app.Activity, androidx.xr.extensions.node.Vec3, androidx.xr.extensions.node.Vec3, androidx.xr.extensions.Consumer<androidx.xr.extensions.space.HitTestResult!>, java.util.concurrent.Executor);
+ method @Deprecated public java.util.concurrent.CompletableFuture<androidx.xr.extensions.asset.EnvironmentToken!> loadEnvironment(java.io.InputStream!, int, int, String!);
+ method @Deprecated public java.util.concurrent.CompletableFuture<androidx.xr.extensions.asset.EnvironmentToken!> loadEnvironment(java.io.InputStream!, int, int, String!, int, int);
+ method @Deprecated public java.util.concurrent.CompletableFuture<androidx.xr.extensions.asset.GltfModelToken!> loadGltfModel(java.io.InputStream!, int, int, String!);
+ method @Deprecated public java.util.concurrent.CompletableFuture<androidx.xr.extensions.asset.SceneToken!> loadImpressScene(java.io.InputStream!, int, int);
+ method public void registerSpatialStateCallback(android.app.Activity, androidx.xr.extensions.Consumer<androidx.xr.extensions.space.SpatialState!>, java.util.concurrent.Executor);
+ method public void removeFindableView(android.view.View, android.view.ViewGroup);
+ method @Deprecated public boolean requestFullSpaceMode(android.app.Activity);
+ method public void requestFullSpaceMode(android.app.Activity, boolean, androidx.xr.extensions.Consumer<androidx.xr.extensions.XrExtensionResult!>, java.util.concurrent.Executor);
+ method @Deprecated public boolean requestHomeSpaceMode(android.app.Activity);
+ method public android.os.Bundle setFullSpaceMode(android.os.Bundle);
+ method public android.os.Bundle setFullSpaceModeWithEnvironmentInherited(android.os.Bundle);
+ method @Deprecated public android.os.Bundle setMainPanelCurvatureRadius(android.os.Bundle, float);
+ method @Deprecated public void setMainWindowCurvatureRadius(android.app.Activity, float);
+ method @Deprecated public void setMainWindowSize(android.app.Activity, int, int);
+ method public void setMainWindowSize(android.app.Activity, int, int, androidx.xr.extensions.Consumer<androidx.xr.extensions.XrExtensionResult!>, java.util.concurrent.Executor);
+ method @Deprecated public void setPreferredAspectRatio(android.app.Activity, float);
+ method public void setPreferredAspectRatio(android.app.Activity, float, androidx.xr.extensions.Consumer<androidx.xr.extensions.XrExtensionResult!>, java.util.concurrent.Executor);
+ method @Deprecated public void setSpatialStateCallback(android.app.Activity, androidx.xr.extensions.Consumer<androidx.xr.extensions.space.SpatialStateEvent!>, java.util.concurrent.Executor);
+ field public static final String IMAGE_TOO_OLD = "This device\'s system image doesn\'t include the necessary implementation for this API. Please update to the latest system image. This API requires a corresponding implementation on the device to function correctly.";
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static class XrExtensions.SceneViewerResult {
+ ctor @Deprecated public XrExtensions.SceneViewerResult();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class XrExtensionsProvider {
+ method public static androidx.xr.extensions.XrExtensions? getXrExtensions();
+ }
+
+}
+
+package androidx.xr.extensions.asset {
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface AssetToken {
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface EnvironmentToken extends androidx.xr.extensions.asset.AssetToken {
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface GltfAnimation {
+ }
+
+ @Deprecated public enum GltfAnimation.State {
+ enum_constant @Deprecated public static final androidx.xr.extensions.asset.GltfAnimation.State LOOP;
+ enum_constant @Deprecated public static final androidx.xr.extensions.asset.GltfAnimation.State PLAY;
+ enum_constant @Deprecated public static final androidx.xr.extensions.asset.GltfAnimation.State STOP;
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface GltfModelToken extends androidx.xr.extensions.asset.AssetToken {
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SceneToken extends androidx.xr.extensions.asset.AssetToken {
+ }
+
+}
+
+package androidx.xr.extensions.environment {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface EnvironmentVisibilityState {
+ method @androidx.xr.extensions.environment.EnvironmentVisibilityState.State public default int getCurrentState();
+ field public static final int APP_VISIBLE = 2; // 0x2
+ field public static final int HOME_VISIBLE = 1; // 0x1
+ field public static final int INVISIBLE = 0; // 0x0
+ }
+
+ @IntDef({androidx.xr.extensions.environment.EnvironmentVisibilityState.INVISIBLE, androidx.xr.extensions.environment.EnvironmentVisibilityState.HOME_VISIBLE, androidx.xr.extensions.environment.EnvironmentVisibilityState.APP_VISIBLE}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface EnvironmentVisibilityState.State {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface PassthroughVisibilityState {
+ method @androidx.xr.extensions.environment.PassthroughVisibilityState.State public default int getCurrentState();
+ method public default float getOpacity();
+ field public static final int APP = 2; // 0x2
+ field public static final int DISABLED = 0; // 0x0
+ field public static final int HOME = 1; // 0x1
+ field public static final int SYSTEM = 3; // 0x3
+ }
+
+ @IntDef({androidx.xr.extensions.environment.PassthroughVisibilityState.DISABLED, androidx.xr.extensions.environment.PassthroughVisibilityState.HOME, androidx.xr.extensions.environment.PassthroughVisibilityState.APP, androidx.xr.extensions.environment.PassthroughVisibilityState.SYSTEM}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface PassthroughVisibilityState.State {
+ }
+
+}
+
+package androidx.xr.extensions.media {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface AudioManagerExtensions {
+ method public default void playSoundEffectAsPointSource(android.media.AudioManager, int, androidx.xr.extensions.media.PointSourceAttributes);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface AudioTrackExtensions {
+ method public default androidx.xr.extensions.media.PointSourceAttributes? getPointSourceAttributes(android.media.AudioTrack);
+ method public default androidx.xr.extensions.media.SoundFieldAttributes? getSoundFieldAttributes(android.media.AudioTrack);
+ method @androidx.xr.extensions.media.SpatializerExtensions.SourceType public default int getSpatialSourceType(android.media.AudioTrack);
+ method public default android.media.AudioTrack.Builder setPointSourceAttributes(android.media.AudioTrack.Builder, androidx.xr.extensions.media.PointSourceAttributes);
+ method public default android.media.AudioTrack.Builder setSoundFieldAttributes(android.media.AudioTrack.Builder, androidx.xr.extensions.media.SoundFieldAttributes);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface MediaPlayerExtensions {
+ method public default android.media.MediaPlayer setPointSourceAttributes(android.media.MediaPlayer, androidx.xr.extensions.media.PointSourceAttributes);
+ method public default android.media.MediaPlayer setSoundFieldAttributes(android.media.MediaPlayer, androidx.xr.extensions.media.SoundFieldAttributes);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class PointSourceAttributes {
+ method public androidx.xr.extensions.node.Node getNode();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final class PointSourceAttributes.Builder {
+ ctor public PointSourceAttributes.Builder();
+ method public androidx.xr.extensions.media.PointSourceAttributes build() throws java.lang.UnsupportedOperationException;
+ method public androidx.xr.extensions.media.PointSourceAttributes.Builder setNode(androidx.xr.extensions.node.Node);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SoundFieldAttributes {
+ method @androidx.xr.extensions.media.SpatializerExtensions.AmbisonicsOrder public int getAmbisonicsOrder();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final class SoundFieldAttributes.Builder {
+ ctor public SoundFieldAttributes.Builder();
+ method public androidx.xr.extensions.media.SoundFieldAttributes build() throws java.lang.UnsupportedOperationException;
+ method public androidx.xr.extensions.media.SoundFieldAttributes.Builder setAmbisonicsOrder(@androidx.xr.extensions.media.SpatializerExtensions.AmbisonicsOrder int);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SoundPoolExtensions {
+ method @androidx.xr.extensions.media.SpatializerExtensions.SourceType public default int getSpatialSourceType(android.media.SoundPool, int);
+ method public default int playAsPointSource(android.media.SoundPool, int, androidx.xr.extensions.media.PointSourceAttributes, float, int, int, float);
+ method public default int playAsSoundField(android.media.SoundPool, int, androidx.xr.extensions.media.SoundFieldAttributes, float, int, int, float);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SpatializerExtensions {
+ field public static final int AMBISONICS_ORDER_FIRST_ORDER = 0; // 0x0
+ field public static final int AMBISONICS_ORDER_SECOND_ORDER = 1; // 0x1
+ field public static final int AMBISONICS_ORDER_THIRD_ORDER = 2; // 0x2
+ field public static final int SOURCE_TYPE_BYPASS = 0; // 0x0
+ field public static final int SOURCE_TYPE_POINT_SOURCE = 1; // 0x1
+ field public static final int SOURCE_TYPE_SOUND_FIELD = 2; // 0x2
+ }
+
+ @IntDef({androidx.xr.extensions.media.SpatializerExtensions.AMBISONICS_ORDER_FIRST_ORDER, androidx.xr.extensions.media.SpatializerExtensions.AMBISONICS_ORDER_SECOND_ORDER, androidx.xr.extensions.media.SpatializerExtensions.AMBISONICS_ORDER_THIRD_ORDER}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface SpatializerExtensions.AmbisonicsOrder {
+ }
+
+ @IntDef({androidx.xr.extensions.media.SpatializerExtensions.SOURCE_TYPE_BYPASS, androidx.xr.extensions.media.SpatializerExtensions.SOURCE_TYPE_POINT_SOURCE, androidx.xr.extensions.media.SpatializerExtensions.SOURCE_TYPE_SOUND_FIELD}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface SpatializerExtensions.SourceType {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface XrSpatialAudioExtensions {
+ method public default androidx.xr.extensions.media.AudioManagerExtensions getAudioManagerExtensions();
+ method public default androidx.xr.extensions.media.AudioTrackExtensions getAudioTrackExtensions();
+ method public default androidx.xr.extensions.media.MediaPlayerExtensions getMediaPlayerExtensions();
+ method public default androidx.xr.extensions.media.SoundPoolExtensions getSoundPoolExtensions();
+ }
+
+}
+
+package androidx.xr.extensions.node {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface InputEvent {
+ method @androidx.xr.extensions.node.InputEvent.Action public int getAction();
+ method public androidx.xr.extensions.node.Vec3 getDirection();
+ method @androidx.xr.extensions.node.InputEvent.DispatchFlag public int getDispatchFlags();
+ method public androidx.xr.extensions.node.InputEvent.HitInfo? getHitInfo();
+ method public androidx.xr.extensions.node.Vec3 getOrigin();
+ method @androidx.xr.extensions.node.InputEvent.PointerType public int getPointerType();
+ method public androidx.xr.extensions.node.InputEvent.HitInfo? getSecondaryHitInfo();
+ method @androidx.xr.extensions.node.InputEvent.Source public int getSource();
+ method public long getTimestamp();
+ field public static final int ACTION_CANCEL = 3; // 0x3
+ field public static final int ACTION_DOWN = 0; // 0x0
+ field public static final int ACTION_HOVER_ENTER = 5; // 0x5
+ field public static final int ACTION_HOVER_EXIT = 6; // 0x6
+ field public static final int ACTION_HOVER_MOVE = 4; // 0x4
+ field public static final int ACTION_MOVE = 2; // 0x2
+ field public static final int ACTION_UP = 1; // 0x1
+ field public static final int DISPATCH_FLAG_2D = 2; // 0x2
+ field public static final int DISPATCH_FLAG_CAPTURED_POINTER = 1; // 0x1
+ field public static final int DISPATCH_FLAG_NONE = 0; // 0x0
+ field public static final int POINTER_TYPE_DEFAULT = 0; // 0x0
+ field public static final int POINTER_TYPE_LEFT = 1; // 0x1
+ field public static final int POINTER_TYPE_RIGHT = 2; // 0x2
+ field public static final int SOURCE_CONTROLLER = 2; // 0x2
+ field public static final int SOURCE_GAZE_AND_GESTURE = 5; // 0x5
+ field public static final int SOURCE_HANDS = 3; // 0x3
+ field public static final int SOURCE_HEAD = 1; // 0x1
+ field public static final int SOURCE_MOUSE = 4; // 0x4
+ field public static final int SOURCE_UNKNOWN = 0; // 0x0
+ }
+
+ @IntDef({androidx.xr.extensions.node.InputEvent.ACTION_DOWN, androidx.xr.extensions.node.InputEvent.ACTION_UP, androidx.xr.extensions.node.InputEvent.ACTION_MOVE, androidx.xr.extensions.node.InputEvent.ACTION_CANCEL, androidx.xr.extensions.node.InputEvent.ACTION_HOVER_MOVE, androidx.xr.extensions.node.InputEvent.ACTION_HOVER_ENTER, androidx.xr.extensions.node.InputEvent.ACTION_HOVER_EXIT}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface InputEvent.Action {
+ }
+
+ @IntDef({androidx.xr.extensions.node.InputEvent.DISPATCH_FLAG_NONE, androidx.xr.extensions.node.InputEvent.DISPATCH_FLAG_CAPTURED_POINTER, androidx.xr.extensions.node.InputEvent.DISPATCH_FLAG_2D}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface InputEvent.DispatchFlag {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static interface InputEvent.HitInfo {
+ method public androidx.xr.extensions.node.Vec3? getHitPosition();
+ method public androidx.xr.extensions.node.Node getInputNode();
+ method public int getSubspaceImpressNodeId();
+ method public androidx.xr.extensions.node.Mat4f getTransform();
+ }
+
+ @IntDef({androidx.xr.extensions.node.InputEvent.POINTER_TYPE_DEFAULT, androidx.xr.extensions.node.InputEvent.POINTER_TYPE_LEFT, androidx.xr.extensions.node.InputEvent.POINTER_TYPE_RIGHT}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface InputEvent.PointerType {
+ }
+
+ @IntDef({androidx.xr.extensions.node.InputEvent.SOURCE_UNKNOWN, androidx.xr.extensions.node.InputEvent.SOURCE_HEAD, androidx.xr.extensions.node.InputEvent.SOURCE_CONTROLLER, androidx.xr.extensions.node.InputEvent.SOURCE_HANDS, androidx.xr.extensions.node.InputEvent.SOURCE_MOUSE, androidx.xr.extensions.node.InputEvent.SOURCE_GAZE_AND_GESTURE}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface InputEvent.Source {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class Mat4f {
+ ctor public Mat4f(float[]);
+ field public float[]![] m;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Node extends android.os.Parcelable {
+ method public void listenForInput(androidx.xr.extensions.Consumer<androidx.xr.extensions.node.InputEvent!>, java.util.concurrent.Executor);
+ method public void requestPointerCapture(androidx.xr.extensions.Consumer<java.lang.Integer!>, java.util.concurrent.Executor);
+ method public void setNonPointerFocusTarget(android.view.AttachedSurfaceControl);
+ method public void stopListeningForInput();
+ method public void stopPointerCapture();
+ method public java.io.Closeable subscribeToTransform(androidx.xr.extensions.Consumer<androidx.xr.extensions.node.NodeTransform!>, java.util.concurrent.Executor);
+ field public static final int POINTER_CAPTURE_STATE_ACTIVE = 1; // 0x1
+ field public static final int POINTER_CAPTURE_STATE_PAUSED = 0; // 0x0
+ field public static final int POINTER_CAPTURE_STATE_STOPPED = 2; // 0x2
+ }
+
+ @IntDef({androidx.xr.extensions.node.Node.POINTER_CAPTURE_STATE_PAUSED, androidx.xr.extensions.node.Node.POINTER_CAPTURE_STATE_ACTIVE, androidx.xr.extensions.node.Node.POINTER_CAPTURE_STATE_STOPPED}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface Node.PointerCaptureState {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface NodeTransaction extends java.io.Closeable {
+ method public default void apply();
+ method public default void close();
+ method public default androidx.xr.extensions.node.NodeTransaction disableReform(androidx.xr.extensions.node.Node);
+ method public default androidx.xr.extensions.node.NodeTransaction enableReform(androidx.xr.extensions.node.Node, androidx.xr.extensions.node.ReformOptions);
+ method public default androidx.xr.extensions.node.NodeTransaction merge(androidx.xr.extensions.node.NodeTransaction);
+ method public default androidx.xr.extensions.node.NodeTransaction removeCornerRadius(androidx.xr.extensions.node.Node);
+ method public default androidx.xr.extensions.node.NodeTransaction setAlpha(androidx.xr.extensions.node.Node, float);
+ method public default androidx.xr.extensions.node.NodeTransaction setAnchorId(androidx.xr.extensions.node.Node, android.os.IBinder?);
+ method public default androidx.xr.extensions.node.NodeTransaction setCornerRadius(androidx.xr.extensions.node.Node, float);
+ method @Deprecated public default androidx.xr.extensions.node.NodeTransaction setCurvature(androidx.xr.extensions.node.Node, float);
+ method @Deprecated public default androidx.xr.extensions.node.NodeTransaction setEnvironment(androidx.xr.extensions.node.Node, androidx.xr.extensions.asset.EnvironmentToken);
+ method @Deprecated public default androidx.xr.extensions.node.NodeTransaction setGltfAnimation(androidx.xr.extensions.node.Node, String, androidx.xr.extensions.asset.GltfAnimation.State);
+ method @Deprecated public default androidx.xr.extensions.node.NodeTransaction setGltfModel(androidx.xr.extensions.node.Node, androidx.xr.extensions.asset.GltfModelToken);
+ method @Deprecated public default androidx.xr.extensions.node.NodeTransaction setImpressScene(androidx.xr.extensions.node.Node, androidx.xr.extensions.asset.SceneToken);
+ method public default androidx.xr.extensions.node.NodeTransaction setName(androidx.xr.extensions.node.Node, String);
+ method public default androidx.xr.extensions.node.NodeTransaction setOrientation(androidx.xr.extensions.node.Node, float, float, float, float);
+ method public default androidx.xr.extensions.node.NodeTransaction setParent(androidx.xr.extensions.node.Node, androidx.xr.extensions.node.Node?);
+ method public default androidx.xr.extensions.node.NodeTransaction setPassthroughState(androidx.xr.extensions.node.Node, float, @androidx.xr.extensions.passthrough.PassthroughState.Mode int);
+ method public default androidx.xr.extensions.node.NodeTransaction setPixelPositioning(androidx.xr.extensions.node.Node, @androidx.xr.extensions.node.NodeTransaction.PixelPositionFlags int);
+ method public default androidx.xr.extensions.node.NodeTransaction setPixelResolution(androidx.xr.extensions.node.Node, float);
+ method public default androidx.xr.extensions.node.NodeTransaction setPosition(androidx.xr.extensions.node.Node, float, float, float);
+ method public default androidx.xr.extensions.node.NodeTransaction setReformSize(androidx.xr.extensions.node.Node, androidx.xr.extensions.node.Vec3);
+ method public default androidx.xr.extensions.node.NodeTransaction setScale(androidx.xr.extensions.node.Node, float, float, float);
+ method public default androidx.xr.extensions.node.NodeTransaction setSubspace(androidx.xr.extensions.node.Node, androidx.xr.extensions.subspace.Subspace);
+ method public default androidx.xr.extensions.node.NodeTransaction setSurfaceControl(androidx.xr.extensions.node.Node?, android.view.SurfaceControl);
+ method public default androidx.xr.extensions.node.NodeTransaction setSurfacePackage(androidx.xr.extensions.node.Node?, android.view.SurfaceControlViewHost.SurfacePackage);
+ method public default androidx.xr.extensions.node.NodeTransaction setVisibility(androidx.xr.extensions.node.Node, boolean);
+ method public default androidx.xr.extensions.node.NodeTransaction setWindowBounds(android.view.SurfaceControl, int, int);
+ method public default androidx.xr.extensions.node.NodeTransaction setWindowBounds(android.view.SurfaceControlViewHost.SurfacePackage, int, int);
+ field public static final int POSITION_FROM_PARENT_TOP_LEFT = 64; // 0x40
+ field public static final int X_POSITION_IN_PIXELS = 1; // 0x1
+ field public static final int Y_POSITION_IN_PIXELS = 2; // 0x2
+ field public static final int Z_POSITION_IN_PIXELS = 4; // 0x4
+ }
+
+ @IntDef(flag=true, value={androidx.xr.extensions.node.NodeTransaction.X_POSITION_IN_PIXELS, androidx.xr.extensions.node.NodeTransaction.Y_POSITION_IN_PIXELS, androidx.xr.extensions.node.NodeTransaction.Z_POSITION_IN_PIXELS, androidx.xr.extensions.node.NodeTransaction.POSITION_FROM_PARENT_TOP_LEFT}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface NodeTransaction.PixelPositionFlags {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface NodeTransform {
+ method public long getTimestamp();
+ method public androidx.xr.extensions.node.Mat4f getTransform();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class Quatf {
+ ctor public Quatf(float, float, float, float);
+ field public float w;
+ field public float x;
+ field public float y;
+ field public float z;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface ReformEvent {
+ method public androidx.xr.extensions.node.Vec3 getCurrentRayDirection();
+ method public androidx.xr.extensions.node.Vec3 getCurrentRayOrigin();
+ method public int getId();
+ method public androidx.xr.extensions.node.Vec3 getInitialRayDirection();
+ method public androidx.xr.extensions.node.Vec3 getInitialRayOrigin();
+ method public androidx.xr.extensions.node.Quatf getProposedOrientation();
+ method public androidx.xr.extensions.node.Vec3 getProposedPosition();
+ method public androidx.xr.extensions.node.Vec3 getProposedScale();
+ method public androidx.xr.extensions.node.Vec3 getProposedSize();
+ method @androidx.xr.extensions.node.ReformEvent.ReformState public int getState();
+ method @androidx.xr.extensions.node.ReformEvent.ReformType public int getType();
+ field public static final int REFORM_STATE_END = 3; // 0x3
+ field public static final int REFORM_STATE_ONGOING = 2; // 0x2
+ field public static final int REFORM_STATE_START = 1; // 0x1
+ field public static final int REFORM_STATE_UNKNOWN = 0; // 0x0
+ field public static final int REFORM_TYPE_MOVE = 1; // 0x1
+ field public static final int REFORM_TYPE_RESIZE = 2; // 0x2
+ field public static final int REFORM_TYPE_UNKNOWN = 0; // 0x0
+ }
+
+ @IntDef({androidx.xr.extensions.node.ReformEvent.REFORM_STATE_UNKNOWN, androidx.xr.extensions.node.ReformEvent.REFORM_STATE_START, androidx.xr.extensions.node.ReformEvent.REFORM_STATE_ONGOING, androidx.xr.extensions.node.ReformEvent.REFORM_STATE_END}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ReformEvent.ReformState {
+ }
+
+ @IntDef({androidx.xr.extensions.node.ReformEvent.REFORM_TYPE_UNKNOWN, androidx.xr.extensions.node.ReformEvent.REFORM_TYPE_MOVE, androidx.xr.extensions.node.ReformEvent.REFORM_TYPE_RESIZE}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ReformEvent.ReformType {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface ReformOptions {
+ method public androidx.xr.extensions.node.Vec3 getCurrentSize();
+ method @androidx.xr.extensions.node.ReformOptions.AllowedReformTypes public int getEnabledReform();
+ method public androidx.xr.extensions.Consumer<androidx.xr.extensions.node.ReformEvent!> getEventCallback();
+ method public java.util.concurrent.Executor getEventExecutor();
+ method public float getFixedAspectRatio();
+ method @androidx.xr.extensions.node.ReformOptions.ReformFlags public int getFlags();
+ method public default boolean getForceShowResizeOverlay();
+ method public androidx.xr.extensions.node.Vec3 getMaximumSize();
+ method public androidx.xr.extensions.node.Vec3 getMinimumSize();
+ method @androidx.xr.extensions.node.ReformOptions.ScaleWithDistanceMode public default int getScaleWithDistanceMode();
+ method public androidx.xr.extensions.node.ReformOptions setCurrentSize(androidx.xr.extensions.node.Vec3);
+ method public androidx.xr.extensions.node.ReformOptions setEnabledReform(@androidx.xr.extensions.node.ReformOptions.AllowedReformTypes int);
+ method public androidx.xr.extensions.node.ReformOptions setEventCallback(androidx.xr.extensions.Consumer<androidx.xr.extensions.node.ReformEvent!>);
+ method public androidx.xr.extensions.node.ReformOptions setEventExecutor(java.util.concurrent.Executor);
+ method public androidx.xr.extensions.node.ReformOptions setFixedAspectRatio(float);
+ method public androidx.xr.extensions.node.ReformOptions setFlags(@androidx.xr.extensions.node.ReformOptions.ReformFlags int);
+ method public default androidx.xr.extensions.node.ReformOptions setForceShowResizeOverlay(boolean);
+ method public androidx.xr.extensions.node.ReformOptions setMaximumSize(androidx.xr.extensions.node.Vec3);
+ method public androidx.xr.extensions.node.ReformOptions setMinimumSize(androidx.xr.extensions.node.Vec3);
+ method public default androidx.xr.extensions.node.ReformOptions setScaleWithDistanceMode(@androidx.xr.extensions.node.ReformOptions.ScaleWithDistanceMode int);
+ field public static final int ALLOW_MOVE = 1; // 0x1
+ field public static final int ALLOW_RESIZE = 2; // 0x2
+ field public static final int FLAG_ALLOW_SYSTEM_MOVEMENT = 2; // 0x2
+ field public static final int FLAG_POSE_RELATIVE_TO_PARENT = 4; // 0x4
+ field public static final int FLAG_SCALE_WITH_DISTANCE = 1; // 0x1
+ field public static final int SCALE_WITH_DISTANCE_MODE_DEFAULT = 3; // 0x3
+ field public static final int SCALE_WITH_DISTANCE_MODE_DMM = 2; // 0x2
+ }
+
+ @IntDef(flag=true, value={androidx.xr.extensions.node.ReformOptions.ALLOW_MOVE, androidx.xr.extensions.node.ReformOptions.ALLOW_RESIZE}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ReformOptions.AllowedReformTypes {
+ }
+
+ @IntDef(flag=true, value={androidx.xr.extensions.node.ReformOptions.FLAG_SCALE_WITH_DISTANCE, androidx.xr.extensions.node.ReformOptions.FLAG_ALLOW_SYSTEM_MOVEMENT, androidx.xr.extensions.node.ReformOptions.FLAG_POSE_RELATIVE_TO_PARENT}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ReformOptions.ReformFlags {
+ }
+
+ @IntDef({androidx.xr.extensions.node.ReformOptions.SCALE_WITH_DISTANCE_MODE_DEFAULT, androidx.xr.extensions.node.ReformOptions.SCALE_WITH_DISTANCE_MODE_DMM}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ReformOptions.ScaleWithDistanceMode {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class Vec3 {
+ ctor public Vec3(float, float, float);
+ field public float x;
+ field public float y;
+ field public float z;
+ }
+
+}
+
+package androidx.xr.extensions.passthrough {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class PassthroughState {
+ ctor public PassthroughState();
+ field public static final int PASSTHROUGH_MODE_MAX = 1; // 0x1
+ field public static final int PASSTHROUGH_MODE_MIN = 2; // 0x2
+ field public static final int PASSTHROUGH_MODE_OFF = 0; // 0x0
+ }
+
+ @IntDef({androidx.xr.extensions.passthrough.PassthroughState.PASSTHROUGH_MODE_OFF, androidx.xr.extensions.passthrough.PassthroughState.PASSTHROUGH_MODE_MAX, androidx.xr.extensions.passthrough.PassthroughState.PASSTHROUGH_MODE_MIN}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface PassthroughState.Mode {
+ }
+
+}
+
+package androidx.xr.extensions.space {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface ActivityPanel {
+ method public void delete();
+ method public androidx.xr.extensions.node.Node getNode();
+ method public void launchActivity(android.content.Intent, android.os.Bundle?);
+ method public void moveActivity(android.app.Activity);
+ method public void setWindowBounds(android.graphics.Rect);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class ActivityPanelLaunchParameters {
+ ctor public ActivityPanelLaunchParameters(android.graphics.Rect);
+ method public android.graphics.Rect getWindowBounds();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class Bounds {
+ ctor public Bounds(float, float, float);
+ field public final float depth;
+ field public final float height;
+ field public final float width;
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class BoundsChangeEvent extends androidx.xr.extensions.space.SpatialStateEvent {
+ ctor @Deprecated public BoundsChangeEvent(androidx.xr.extensions.space.Bounds!);
+ field @Deprecated public androidx.xr.extensions.space.Bounds! bounds;
+ field @Deprecated public float depth;
+ field @Deprecated public float height;
+ field @Deprecated public float width;
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class EnvironmentControlChangeEvent extends androidx.xr.extensions.space.SpatialStateEvent {
+ ctor @Deprecated public EnvironmentControlChangeEvent(boolean);
+ field @Deprecated public boolean environmentControlAllowed;
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class EnvironmentVisibilityChangeEvent extends androidx.xr.extensions.space.SpatialStateEvent {
+ ctor @Deprecated public EnvironmentVisibilityChangeEvent(@androidx.xr.extensions.environment.EnvironmentVisibilityState.State int);
+ field @Deprecated @androidx.xr.extensions.environment.EnvironmentVisibilityState.State public int environmentState;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class HitTestResult {
+ ctor public HitTestResult();
+ field public static final int SURFACE_3D_OBJECT = 2; // 0x2
+ field public static final int SURFACE_PANEL = 1; // 0x1
+ field public static final int SURFACE_UNKNOWN = 0; // 0x0
+ field public float distance;
+ field public androidx.xr.extensions.node.Vec3 hitPosition;
+ field public androidx.xr.extensions.node.Vec3? surfaceNormal;
+ field @androidx.xr.extensions.space.HitTestResult.SurfaceType public int surfaceType;
+ field public boolean virtualEnvironmentIsVisible;
+ }
+
+ @IntDef({androidx.xr.extensions.space.HitTestResult.SURFACE_UNKNOWN, androidx.xr.extensions.space.HitTestResult.SURFACE_PANEL, androidx.xr.extensions.space.HitTestResult.SURFACE_3D_OBJECT}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface HitTestResult.SurfaceType {
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class PassthroughVisibilityChangeEvent extends androidx.xr.extensions.space.SpatialStateEvent {
+ ctor @Deprecated public PassthroughVisibilityChangeEvent(@androidx.xr.extensions.environment.PassthroughVisibilityState.State int);
+ field @Deprecated @androidx.xr.extensions.environment.PassthroughVisibilityState.State public int passthroughState;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SpatialCapabilities {
+ method public default boolean get(@androidx.xr.extensions.space.SpatialCapabilities.CapabilityType int);
+ field public static final int APP_ENVIRONMENTS_CAPABLE = 3; // 0x3
+ field public static final int PASSTHROUGH_CONTROL_CAPABLE = 2; // 0x2
+ field public static final int SPATIAL_3D_CONTENTS_CAPABLE = 1; // 0x1
+ field public static final int SPATIAL_ACTIVITY_EMBEDDING_CAPABLE = 5; // 0x5
+ field public static final int SPATIAL_AUDIO_CAPABLE = 4; // 0x4
+ field public static final int SPATIAL_UI_CAPABLE = 0; // 0x0
+ }
+
+ @IntDef({androidx.xr.extensions.space.SpatialCapabilities.SPATIAL_UI_CAPABLE, androidx.xr.extensions.space.SpatialCapabilities.SPATIAL_3D_CONTENTS_CAPABLE, androidx.xr.extensions.space.SpatialCapabilities.PASSTHROUGH_CONTROL_CAPABLE, androidx.xr.extensions.space.SpatialCapabilities.APP_ENVIRONMENTS_CAPABLE, androidx.xr.extensions.space.SpatialCapabilities.SPATIAL_AUDIO_CAPABLE, androidx.xr.extensions.space.SpatialCapabilities.SPATIAL_ACTIVITY_EMBEDDING_CAPABLE}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface SpatialCapabilities.CapabilityType {
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SpatialCapabilityChangeEvent extends androidx.xr.extensions.space.SpatialStateEvent {
+ ctor @Deprecated public SpatialCapabilityChangeEvent(androidx.xr.extensions.space.SpatialCapabilities!);
+ field @Deprecated public androidx.xr.extensions.space.SpatialCapabilities! currentCapabilities;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SpatialState {
+ method public default androidx.xr.extensions.space.Bounds getBounds();
+ method public default androidx.xr.extensions.environment.EnvironmentVisibilityState getEnvironmentVisibility();
+ method public default android.util.Size getMainWindowSize();
+ method public default androidx.xr.extensions.environment.PassthroughVisibilityState getPassthroughVisibility();
+ method public default float getPreferredAspectRatio();
+ method public default androidx.xr.extensions.space.SpatialCapabilities getSpatialCapabilities();
+ method public default boolean isActiveEnvironmentNode(androidx.xr.extensions.node.Node?);
+ method public default boolean isActiveSceneNode(androidx.xr.extensions.node.Node?);
+ method public default boolean isActiveWindowLeashNode(androidx.xr.extensions.node.Node?);
+ method public default boolean isEnvironmentInherited();
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class SpatialStateEvent {
+ ctor @Deprecated public SpatialStateEvent();
+ }
+
+ @Deprecated @SuppressCompatibility @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.xr.extensions.ExperimentalExtensionApi public final class VisibilityChangeEvent extends androidx.xr.extensions.space.SpatialStateEvent {
+ ctor @Deprecated public VisibilityChangeEvent(@androidx.xr.extensions.space.VisibilityChangeEvent.SpatialVisibility int);
+ field @Deprecated public static final int HIDDEN = 1; // 0x1
+ field @Deprecated public static final int PARTIALLY_VISIBLE = 2; // 0x2
+ field @Deprecated public static final int UNKNOWN = 0; // 0x0
+ field @Deprecated public static final int VISIBLE = 3; // 0x3
+ field @Deprecated @androidx.xr.extensions.space.VisibilityChangeEvent.SpatialVisibility public int visibility;
+ }
+
+ @Deprecated @IntDef({androidx.xr.extensions.space.VisibilityChangeEvent.UNKNOWN, androidx.xr.extensions.space.VisibilityChangeEvent.HIDDEN, androidx.xr.extensions.space.VisibilityChangeEvent.PARTIALLY_VISIBLE, androidx.xr.extensions.space.VisibilityChangeEvent.VISIBLE}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface VisibilityChangeEvent.SpatialVisibility {
+ }
+
+}
+
+package androidx.xr.extensions.splitengine {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SplitEngineBridge {
+ }
+
+}
+
+package androidx.xr.extensions.subspace {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Subspace {
+ }
+
+}
+
+package androidx.xr.scenecore {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class ActivityPanelEntity extends androidx.xr.scenecore.PanelEntity {
+ method public void launchActivity(android.content.Intent intent);
+ method public void launchActivity(android.content.Intent intent, optional android.os.Bundle? bundle);
+ method public void moveActivity(android.app.Activity activity);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface ActivityPose {
+ method public androidx.xr.runtime.math.Pose getActivitySpacePose();
+ method public androidx.xr.runtime.math.Pose transformPoseTo(androidx.xr.runtime.math.Pose pose, androidx.xr.scenecore.ActivityPose destination);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class ActivitySpace extends androidx.xr.scenecore.BaseEntity<androidx.xr.scenecore.JxrPlatformAdapter.ActivitySpace> {
+ method public void addBoundsChangedListener(java.util.concurrent.Executor callbackExecutor, java.util.function.Consumer<androidx.xr.scenecore.Dimensions> listener);
+ method public void addBoundsChangedListener(java.util.function.Consumer<androidx.xr.scenecore.Dimensions> listener);
+ method public androidx.xr.scenecore.Dimensions getBounds();
+ method @Deprecated public void registerOnBoundsChangedListener(androidx.xr.scenecore.OnBoundsChangeListener listener);
+ method public void removeBoundsChangedListener(java.util.function.Consumer<androidx.xr.scenecore.Dimensions> listener);
+ method public void setOnSpaceUpdatedListener(androidx.xr.scenecore.OnSpaceUpdatedListener? listener);
+ method public void setOnSpaceUpdatedListener(androidx.xr.scenecore.OnSpaceUpdatedListener? listener, optional java.util.concurrent.Executor? executor);
+ method @Deprecated public void unregisterOnBoundsChangedListener();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class AnchorEntity extends androidx.xr.scenecore.BaseEntity<androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity> {
+ method public androidx.xr.arcore.Anchor getAnchor(androidx.xr.runtime.Session session);
+ method public androidx.xr.scenecore.AnchorEntity.PersistState getPersistState();
+ method public int getState();
+ method public java.util.UUID? persist();
+ method public void setOnSpaceUpdatedListener(androidx.xr.scenecore.OnSpaceUpdatedListener? listener);
+ method public void setOnSpaceUpdatedListener(androidx.xr.scenecore.OnSpaceUpdatedListener? listener, optional java.util.concurrent.Executor? executor);
+ method public void setOnStateChangedListener(androidx.xr.scenecore.OnStateChangedListener? onStateChangedListener);
+ }
+
+ public enum AnchorEntity.PersistState {
+ enum_constant public static final androidx.xr.scenecore.AnchorEntity.PersistState PERSISTED;
+ enum_constant public static final androidx.xr.scenecore.AnchorEntity.PersistState PERSIST_NOT_REQUESTED;
+ enum_constant public static final androidx.xr.scenecore.AnchorEntity.PersistState PERSIST_PENDING;
+ }
+
+ public static final class AnchorEntity.State {
+ property public static final int ANCHORED;
+ property public static final int ERROR;
+ property public static final int TIMEDOUT;
+ property public static final int UNANCHORED;
+ field public static final int ANCHORED = 0; // 0x0
+ field public static final int ERROR = 3; // 0x3
+ field public static final androidx.xr.scenecore.AnchorEntity.State INSTANCE;
+ field public static final int TIMEDOUT = 2; // 0x2
+ field public static final int UNANCHORED = 1; // 0x1
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class AnchorPlacement {
+ ctor public AnchorPlacement();
+ method public static androidx.xr.scenecore.AnchorPlacement createForPlanes();
+ method public static androidx.xr.scenecore.AnchorPlacement createForPlanes(optional java.util.Set<java.lang.Integer> planeTypeFilter);
+ method public static androidx.xr.scenecore.AnchorPlacement createForPlanes(optional java.util.Set<java.lang.Integer> planeTypeFilter, optional java.util.Set<java.lang.Integer> planeSemanticFilter);
+ field public static final androidx.xr.scenecore.AnchorPlacement.Companion Companion;
+ }
+
+ public static final class AnchorPlacement.Companion {
+ method public androidx.xr.scenecore.AnchorPlacement createForPlanes();
+ method public androidx.xr.scenecore.AnchorPlacement createForPlanes(optional java.util.Set<java.lang.Integer> planeTypeFilter);
+ method public androidx.xr.scenecore.AnchorPlacement createForPlanes(optional java.util.Set<java.lang.Integer> planeTypeFilter, optional java.util.Set<java.lang.Integer> planeSemanticFilter);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract sealed class BaseActivityPose<RtActivityPoseType extends androidx.xr.scenecore.JxrPlatformAdapter.ActivityPose> implements androidx.xr.scenecore.ActivityPose {
+ method public androidx.xr.runtime.math.Pose getActivitySpacePose();
+ method public androidx.xr.runtime.math.Pose transformPoseTo(androidx.xr.runtime.math.Pose pose, androidx.xr.scenecore.ActivityPose destination);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract sealed class BaseEntity<RtEntityType extends androidx.xr.scenecore.JxrPlatformAdapter.Entity> extends androidx.xr.scenecore.BaseActivityPose<androidx.xr.scenecore.JxrPlatformAdapter.ActivityPose> implements androidx.xr.scenecore.Entity {
+ method public void addChild(androidx.xr.scenecore.Entity child);
+ method public boolean addComponent(androidx.xr.scenecore.Component component);
+ method public void dispose();
+ method public float getActivitySpaceAlpha();
+ method public float getAlpha();
+ method public java.util.List<androidx.xr.scenecore.Component> getComponents();
+ method public <T extends androidx.xr.scenecore.Component> java.util.List<T> getComponentsOfType(Class<? extends T> type);
+ method public androidx.xr.scenecore.Entity? getParent();
+ method public androidx.xr.runtime.math.Pose getPose();
+ method public float getScale();
+ method public androidx.xr.scenecore.Dimensions getSize();
+ method public float getWorldSpaceScale();
+ method public boolean isHidden(boolean includeParents);
+ method public void removeAllComponents();
+ method public void removeComponent(androidx.xr.scenecore.Component component);
+ method public void setAlpha(float alpha);
+ method public void setContentDescription(String text);
+ method public void setHidden(boolean hidden);
+ method public void setParent(androidx.xr.scenecore.Entity? parent);
+ method public void setPose(androidx.xr.runtime.math.Pose pose);
+ method public void setScale(float scale);
+ method public void setSize(androidx.xr.scenecore.Dimensions dimensions);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract sealed class BasePanelEntity<RtPanelEntityType extends androidx.xr.scenecore.JxrPlatformAdapter.PanelEntity> extends androidx.xr.scenecore.BaseEntity<androidx.xr.scenecore.JxrPlatformAdapter.PanelEntity> {
+ method public final float getCornerRadius();
+ method public final androidx.xr.runtime.math.Vector3 getPixelDensity();
+ method public final androidx.xr.scenecore.PixelDimensions getPixelDimensions();
+ method public final void setCornerRadius(float radius);
+ method public final void setPixelDimensions(androidx.xr.scenecore.PixelDimensions pxDimensions);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class CameraView extends androidx.xr.scenecore.BaseActivityPose<androidx.xr.scenecore.JxrPlatformAdapter.CameraViewActivityPose> {
+ method public androidx.xr.scenecore.CameraView.CameraType getCameraType();
+ method public androidx.xr.scenecore.Fov getFov();
+ property public final androidx.xr.scenecore.CameraView.CameraType cameraType;
+ property public final androidx.xr.scenecore.Fov fov;
+ }
+
+ public enum CameraView.CameraType {
+ enum_constant public static final androidx.xr.scenecore.CameraView.CameraType LEFT_EYE;
+ enum_constant public static final androidx.xr.scenecore.CameraView.CameraType RIGHT_EYE;
+ enum_constant public static final androidx.xr.scenecore.CameraView.CameraType UNKNOWN;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Component {
+ method public boolean onAttach(androidx.xr.scenecore.Entity entity);
+ method public void onDetach(androidx.xr.scenecore.Entity entity);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class ContentlessEntity extends androidx.xr.scenecore.BaseEntity<androidx.xr.scenecore.JxrPlatformAdapter.Entity> {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class Dimensions {
+ ctor public Dimensions();
+ ctor public Dimensions(optional float width, optional float height, optional float depth);
+ method public float component1();
+ method public float component2();
+ method public float component3();
+ method public androidx.xr.scenecore.Dimensions copy(float width, float height, float depth);
+ method public float getDepth();
+ method public float getHeight();
+ method public float getWidth();
+ property public final float depth;
+ property public final float height;
+ property public final float width;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Entity extends androidx.xr.scenecore.ActivityPose {
+ method public void addChild(androidx.xr.scenecore.Entity child);
+ method public boolean addComponent(androidx.xr.scenecore.Component component);
+ method public void dispose();
+ method public float getActivitySpaceAlpha();
+ method public float getAlpha();
+ method public java.util.List<androidx.xr.scenecore.Component> getComponents();
+ method public <T extends androidx.xr.scenecore.Component> java.util.List<T> getComponentsOfType(Class<? extends T> type);
+ method public androidx.xr.scenecore.Entity? getParent();
+ method public androidx.xr.runtime.math.Pose getPose();
+ method public float getScale();
+ method public androidx.xr.scenecore.Dimensions getSize();
+ method public float getWorldSpaceScale();
+ method public boolean isHidden(optional boolean includeParents);
+ method public void removeAllComponents();
+ method public void removeComponent(androidx.xr.scenecore.Component component);
+ method public void setAlpha(float alpha);
+ method public void setContentDescription(String text);
+ method public void setHidden(boolean hidden);
+ method public void setParent(androidx.xr.scenecore.Entity? parent);
+ method public void setPose(androidx.xr.runtime.math.Pose pose);
+ method public void setScale(float scale);
+ method public void setSize(androidx.xr.scenecore.Dimensions dimensions);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class ExrImage implements androidx.xr.scenecore.Image {
+ method public androidx.xr.scenecore.JxrPlatformAdapter.ExrImageResource getImage();
+ property public final androidx.xr.scenecore.JxrPlatformAdapter.ExrImageResource image;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class Fov {
+ ctor public Fov(float angleLeft, float angleRight, float angleUp, float angleDown);
+ method public float component1();
+ method public float component2();
+ method public float component3();
+ method public float component4();
+ method public androidx.xr.scenecore.Fov copy(float angleLeft, float angleRight, float angleUp, float angleDown);
+ method public float getAngleDown();
+ method public float getAngleLeft();
+ method public float getAngleRight();
+ method public float getAngleUp();
+ property public final float angleDown;
+ property public final float angleLeft;
+ property public final float angleRight;
+ property public final float angleUp;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class GltfModel implements androidx.xr.scenecore.Model {
+ method public androidx.xr.scenecore.JxrPlatformAdapter.GltfModelResource getModel();
+ property public final androidx.xr.scenecore.JxrPlatformAdapter.GltfModelResource model;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class GltfModelEntity extends androidx.xr.scenecore.BaseEntity<androidx.xr.scenecore.JxrPlatformAdapter.GltfEntity> {
+ method public int getAnimationState();
+ method @MainThread public void startAnimation(boolean loop);
+ method @MainThread public void startAnimation(boolean loop, optional String? animationName);
+ method @MainThread public void stopAnimation();
+ }
+
+ public static final class GltfModelEntity.AnimationState {
+ property public static final int PLAYING;
+ property public static final int STOPPED;
+ field public static final androidx.xr.scenecore.GltfModelEntity.AnimationState INSTANCE;
+ field public static final int PLAYING = 0; // 0x0
+ field public static final int STOPPED = 1; // 0x1
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class Head extends androidx.xr.scenecore.BaseActivityPose<androidx.xr.scenecore.JxrPlatformAdapter.HeadActivityPose> {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Image {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class InputEvent {
+ ctor public InputEvent(int source, int pointerType, long timestamp, androidx.xr.runtime.math.Vector3 origin, androidx.xr.runtime.math.Vector3 direction, int action, optional androidx.xr.scenecore.InputEvent.HitInfo? hitInfo, optional androidx.xr.scenecore.InputEvent.HitInfo? secondaryHitInfo);
+ method public int getAction();
+ method public androidx.xr.runtime.math.Vector3 getDirection();
+ method public androidx.xr.scenecore.InputEvent.HitInfo? getHitInfo();
+ method public androidx.xr.runtime.math.Vector3 getOrigin();
+ method public int getPointerType();
+ method public androidx.xr.scenecore.InputEvent.HitInfo? getSecondaryHitInfo();
+ method public int getSource();
+ method public long getTimestamp();
+ property public final int action;
+ property public final androidx.xr.runtime.math.Vector3 direction;
+ property public final androidx.xr.scenecore.InputEvent.HitInfo? hitInfo;
+ property public final androidx.xr.runtime.math.Vector3 origin;
+ property public final int pointerType;
+ property public final androidx.xr.scenecore.InputEvent.HitInfo? secondaryHitInfo;
+ property public final int source;
+ property public final long timestamp;
+ field public static final int ACTION_CANCEL = 3; // 0x3
+ field public static final int ACTION_DOWN = 0; // 0x0
+ field public static final int ACTION_HOVER_ENTER = 5; // 0x5
+ field public static final int ACTION_HOVER_EXIT = 6; // 0x6
+ field public static final int ACTION_HOVER_MOVE = 4; // 0x4
+ field public static final int ACTION_MOVE = 2; // 0x2
+ field public static final int ACTION_UP = 1; // 0x1
+ field public static final androidx.xr.scenecore.InputEvent.Companion Companion;
+ field public static final int POINTER_TYPE_DEFAULT = 0; // 0x0
+ field public static final int POINTER_TYPE_LEFT = 1; // 0x1
+ field public static final int POINTER_TYPE_RIGHT = 2; // 0x2
+ field public static final int SOURCE_CONTROLLER = 2; // 0x2
+ field public static final int SOURCE_GAZE_AND_GESTURE = 5; // 0x5
+ field public static final int SOURCE_HANDS = 3; // 0x3
+ field public static final int SOURCE_HEAD = 1; // 0x1
+ field public static final int SOURCE_MOUSE = 4; // 0x4
+ field public static final int SOURCE_UNKNOWN = 0; // 0x0
+ }
+
+ public static final class InputEvent.Companion {
+ property public static final int ACTION_CANCEL;
+ property public static final int ACTION_DOWN;
+ property public static final int ACTION_HOVER_ENTER;
+ property public static final int ACTION_HOVER_EXIT;
+ property public static final int ACTION_HOVER_MOVE;
+ property public static final int ACTION_MOVE;
+ property public static final int ACTION_UP;
+ property public static final int POINTER_TYPE_DEFAULT;
+ property public static final int POINTER_TYPE_LEFT;
+ property public static final int POINTER_TYPE_RIGHT;
+ property public static final int SOURCE_CONTROLLER;
+ property public static final int SOURCE_GAZE_AND_GESTURE;
+ property public static final int SOURCE_HANDS;
+ property public static final int SOURCE_HEAD;
+ property public static final int SOURCE_MOUSE;
+ property public static final int SOURCE_UNKNOWN;
+ }
+
+ public static final class InputEvent.HitInfo {
+ ctor public InputEvent.HitInfo(androidx.xr.scenecore.Entity inputEntity, androidx.xr.runtime.math.Vector3? hitPosition, androidx.xr.runtime.math.Matrix4 transform);
+ method public androidx.xr.runtime.math.Vector3? getHitPosition();
+ method public androidx.xr.scenecore.Entity getInputEntity();
+ method public androidx.xr.runtime.math.Matrix4 getTransform();
+ property public final androidx.xr.runtime.math.Vector3? hitPosition;
+ property public final androidx.xr.scenecore.Entity inputEntity;
+ property public final androidx.xr.runtime.math.Matrix4 transform;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public fun interface InputEventListener {
+ method public void onInputEvent(androidx.xr.scenecore.InputEvent inputEvent);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class InteractableComponent implements androidx.xr.scenecore.Component {
+ method public boolean onAttach(androidx.xr.scenecore.Entity entity);
+ method public void onDetach(androidx.xr.scenecore.Entity entity);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface JxrPlatformAdapter {
+ method public void addSpatialCapabilitiesChangedListener(java.util.concurrent.Executor, java.util.function.Consumer<androidx.xr.scenecore.JxrPlatformAdapter.SpatialCapabilities!>);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.ActivityPanelEntity createActivityPanelEntity(androidx.xr.runtime.math.Pose, androidx.xr.scenecore.JxrPlatformAdapter.PixelDimensions, String, android.app.Activity, androidx.xr.scenecore.JxrPlatformAdapter.Entity);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity createAnchorEntity(androidx.xr.arcore.Anchor);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity createAnchorEntity(androidx.xr.scenecore.JxrPlatformAdapter.Dimensions, androidx.xr.scenecore.JxrPlatformAdapter.PlaneType, androidx.xr.scenecore.JxrPlatformAdapter.PlaneSemantic, java.time.Duration);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.AnchorPlacement createAnchorPlacementForPlanes(java.util.Set<androidx.xr.scenecore.JxrPlatformAdapter.PlaneType!>, java.util.Set<androidx.xr.scenecore.JxrPlatformAdapter.PlaneSemantic!>);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.Entity createEntity(androidx.xr.runtime.math.Pose, String, androidx.xr.scenecore.JxrPlatformAdapter.Entity);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.GltfEntity createGltfEntity(androidx.xr.runtime.math.Pose, androidx.xr.scenecore.JxrPlatformAdapter.GltfModelResource, androidx.xr.scenecore.JxrPlatformAdapter.Entity?);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.InteractableComponent createInteractableComponent(java.util.concurrent.Executor, androidx.xr.scenecore.JxrPlatformAdapter.InputEventListener);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.LoggingEntity createLoggingEntity(androidx.xr.runtime.math.Pose);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.MovableComponent createMovableComponent(boolean, boolean, java.util.Set<androidx.xr.scenecore.JxrPlatformAdapter.AnchorPlacement!>, boolean);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.PanelEntity createPanelEntity(androidx.xr.runtime.math.Pose, android.view.View, androidx.xr.scenecore.JxrPlatformAdapter.PixelDimensions, androidx.xr.scenecore.JxrPlatformAdapter.Dimensions, String, android.content.Context, androidx.xr.scenecore.JxrPlatformAdapter.Entity);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity createPersistedAnchorEntity(java.util.UUID, java.time.Duration);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.PointerCaptureComponent createPointerCaptureComponent(java.util.concurrent.Executor, androidx.xr.scenecore.JxrPlatformAdapter.PointerCaptureComponent.StateListener, androidx.xr.scenecore.JxrPlatformAdapter.InputEventListener);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.ResizableComponent createResizableComponent(androidx.xr.scenecore.JxrPlatformAdapter.Dimensions, androidx.xr.scenecore.JxrPlatformAdapter.Dimensions);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.StereoSurfaceEntity createStereoSurfaceEntity(int, androidx.xr.scenecore.JxrPlatformAdapter.Dimensions, androidx.xr.runtime.math.Pose, androidx.xr.scenecore.JxrPlatformAdapter.Entity);
+ method public void dispose();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.ActivitySpace getActivitySpace();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.Entity getActivitySpaceRootImpl();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.AudioTrackExtensionsWrapper getAudioTrackExtensionsWrapper();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.CameraViewActivityPose? getCameraViewActivityPose(int);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.HeadActivityPose? getHeadActivityPose();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.PanelEntity getMainPanelEntity();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.MediaPlayerExtensionsWrapper getMediaPlayerExtensionsWrapper();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.PerceptionSpaceActivityPose getPerceptionSpaceActivityPose();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.SoundPoolExtensionsWrapper getSoundPoolExtensionsWrapper();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.SpatialCapabilities getSpatialCapabilities();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.SpatialEnvironment getSpatialEnvironment();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.scenecore.JxrPlatformAdapter.ExrImageResource!>? loadExrImageByAssetName(String);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.scenecore.JxrPlatformAdapter.GltfModelResource!>? loadGltfByAssetName(String);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.scenecore.JxrPlatformAdapter.GltfModelResource!>? loadGltfByAssetNameSplitEngine(String);
+ method public void removeSpatialCapabilitiesChangedListener(java.util.function.Consumer<androidx.xr.scenecore.JxrPlatformAdapter.SpatialCapabilities!>);
+ method public void requestFullSpaceMode();
+ method public void requestHomeSpaceMode();
+ method public android.os.Bundle setFullSpaceMode(android.os.Bundle);
+ method public android.os.Bundle setFullSpaceModeWithEnvironmentInherited(android.os.Bundle);
+ method public void setPreferredAspectRatio(android.app.Activity, float);
+ method public void startRenderer();
+ method public void stopRenderer();
+ method public boolean unpersistAnchor(java.util.UUID);
+ }
+
+ public static interface JxrPlatformAdapter.ActivityPanelEntity extends androidx.xr.scenecore.JxrPlatformAdapter.PanelEntity {
+ method public void launchActivity(android.content.Intent, android.os.Bundle?);
+ method public void moveActivity(android.app.Activity);
+ }
+
+ public static interface JxrPlatformAdapter.ActivityPose {
+ method public androidx.xr.runtime.math.Pose getActivitySpacePose();
+ method public androidx.xr.runtime.math.Vector3 getActivitySpaceScale();
+ method public androidx.xr.runtime.math.Vector3 getWorldSpaceScale();
+ method public androidx.xr.runtime.math.Pose transformPoseTo(androidx.xr.runtime.math.Pose, androidx.xr.scenecore.JxrPlatformAdapter.ActivityPose);
+ }
+
+ public static interface JxrPlatformAdapter.ActivitySpace extends androidx.xr.scenecore.JxrPlatformAdapter.SystemSpaceEntity {
+ method public void addOnBoundsChangedListener(androidx.xr.scenecore.JxrPlatformAdapter.ActivitySpace.OnBoundsChangedListener);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.Dimensions getBounds();
+ method public void removeOnBoundsChangedListener(androidx.xr.scenecore.JxrPlatformAdapter.ActivitySpace.OnBoundsChangedListener);
+ }
+
+ public static interface JxrPlatformAdapter.ActivitySpace.OnBoundsChangedListener {
+ method public void onBoundsChanged(androidx.xr.scenecore.JxrPlatformAdapter.Dimensions);
+ }
+
+ public static interface JxrPlatformAdapter.AnchorEntity extends androidx.xr.scenecore.JxrPlatformAdapter.SystemSpaceEntity {
+ method public androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.PersistState getPersistState();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.State getState();
+ method public long nativePointer();
+ method public java.util.UUID? persist();
+ method public void registerPersistStateChangeListener(androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.PersistStateChangeListener);
+ method public void setOnStateChangedListener(androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.OnStateChangedListener?);
+ }
+
+ public static interface JxrPlatformAdapter.AnchorEntity.OnStateChangedListener {
+ method public void onStateChanged(androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.State);
+ }
+
+ public enum JxrPlatformAdapter.AnchorEntity.PersistState {
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.PersistState PERSISTED;
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.PersistState PERSIST_NOT_REQUESTED;
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.PersistState PERSIST_PENDING;
+ }
+
+ public static interface JxrPlatformAdapter.AnchorEntity.PersistStateChangeListener {
+ method public void onPersistStateChanged(androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.PersistState);
+ }
+
+ public enum JxrPlatformAdapter.AnchorEntity.State {
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.State ANCHORED;
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.State ERROR;
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.State TIMED_OUT;
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.State UNANCHORED;
+ }
+
+ public static interface JxrPlatformAdapter.AnchorPlacement {
+ }
+
+ public static interface JxrPlatformAdapter.AudioTrackExtensionsWrapper {
+ method public androidx.xr.scenecore.JxrPlatformAdapter.PointSourceAttributes? getPointSourceAttributes(android.media.AudioTrack);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.SoundFieldAttributes? getSoundFieldAttributes(android.media.AudioTrack);
+ method public int getSpatialSourceType(android.media.AudioTrack);
+ method public android.media.AudioTrack.Builder setPointSourceAttributes(android.media.AudioTrack.Builder, androidx.xr.scenecore.JxrPlatformAdapter.PointSourceAttributes);
+ method public android.media.AudioTrack.Builder setSoundFieldAttributes(android.media.AudioTrack.Builder, androidx.xr.scenecore.JxrPlatformAdapter.SoundFieldAttributes);
+ }
+
+ public static interface JxrPlatformAdapter.CameraViewActivityPose extends androidx.xr.scenecore.JxrPlatformAdapter.ActivityPose {
+ method public int getCameraType();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.CameraViewActivityPose.Fov getFov();
+ field public static final int CAMERA_TYPE_LEFT_EYE = 1; // 0x1
+ field public static final int CAMERA_TYPE_RIGHT_EYE = 2; // 0x2
+ field public static final int CAMERA_TYPE_UNKNOWN = 0; // 0x0
+ }
+
+ public static class JxrPlatformAdapter.CameraViewActivityPose.Fov {
+ ctor public JxrPlatformAdapter.CameraViewActivityPose.Fov(float, float, float, float);
+ field public final float angleDown;
+ field public final float angleLeft;
+ field public final float angleRight;
+ field public final float angleUp;
+ }
+
+ public static interface JxrPlatformAdapter.Component {
+ method public boolean onAttach(androidx.xr.scenecore.JxrPlatformAdapter.Entity);
+ method public void onDetach(androidx.xr.scenecore.JxrPlatformAdapter.Entity);
+ }
+
+ public static class JxrPlatformAdapter.Dimensions {
+ ctor public JxrPlatformAdapter.Dimensions(float, float, float);
+ field public float depth;
+ field public float height;
+ field public float width;
+ }
+
+ public static interface JxrPlatformAdapter.Entity extends androidx.xr.scenecore.JxrPlatformAdapter.ActivityPose {
+ method public void addChild(androidx.xr.scenecore.JxrPlatformAdapter.Entity);
+ method public void addChildren(java.util.List<androidx.xr.scenecore.JxrPlatformAdapter.Entity!>);
+ method public boolean addComponent(androidx.xr.scenecore.JxrPlatformAdapter.Component);
+ method public void addInputEventListener(java.util.concurrent.Executor, androidx.xr.scenecore.JxrPlatformAdapter.InputEventListener);
+ method public void dispose();
+ method public float getActivitySpaceAlpha();
+ method public float getAlpha();
+ method public java.util.List<androidx.xr.scenecore.JxrPlatformAdapter.Entity!> getChildren();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.Entity? getParent();
+ method public androidx.xr.runtime.math.Pose getPose();
+ method public androidx.xr.runtime.math.Vector3 getScale();
+ method public boolean isHidden(boolean);
+ method public void removeAllComponents();
+ method public void removeComponent(androidx.xr.scenecore.JxrPlatformAdapter.Component);
+ method public void removeInputEventListener(androidx.xr.scenecore.JxrPlatformAdapter.InputEventListener);
+ method public void setAlpha(float);
+ method public void setContentDescription(String);
+ method public void setHidden(boolean);
+ method public void setParent(androidx.xr.scenecore.JxrPlatformAdapter.Entity?);
+ method public void setPose(androidx.xr.runtime.math.Pose);
+ method public void setScale(androidx.xr.runtime.math.Vector3);
+ method public void setSize(androidx.xr.scenecore.JxrPlatformAdapter.Dimensions);
+ }
+
+ public static interface JxrPlatformAdapter.ExrImageResource extends androidx.xr.scenecore.JxrPlatformAdapter.Resource {
+ }
+
+ public static interface JxrPlatformAdapter.GltfEntity extends androidx.xr.scenecore.JxrPlatformAdapter.Entity {
+ method public int getAnimationState();
+ method public void startAnimation(boolean, String?);
+ method public void stopAnimation();
+ }
+
+ public static interface JxrPlatformAdapter.GltfModelResource extends androidx.xr.scenecore.JxrPlatformAdapter.Resource {
+ }
+
+ public static interface JxrPlatformAdapter.HeadActivityPose extends androidx.xr.scenecore.JxrPlatformAdapter.ActivityPose {
+ }
+
+ public static class JxrPlatformAdapter.InputEvent {
+ ctor public JxrPlatformAdapter.InputEvent(int, int, long, androidx.xr.runtime.math.Vector3, androidx.xr.runtime.math.Vector3, int, androidx.xr.scenecore.JxrPlatformAdapter.InputEvent.HitInfo?, androidx.xr.scenecore.JxrPlatformAdapter.InputEvent.HitInfo?);
+ field public static final int ACTION_CANCEL = 3; // 0x3
+ field public static final int ACTION_DOWN = 0; // 0x0
+ field public static final int ACTION_HOVER_ENTER = 5; // 0x5
+ field public static final int ACTION_HOVER_EXIT = 6; // 0x6
+ field public static final int ACTION_HOVER_MOVE = 4; // 0x4
+ field public static final int ACTION_MOVE = 2; // 0x2
+ field public static final int ACTION_UP = 1; // 0x1
+ field public static final int POINTER_TYPE_DEFAULT = 0; // 0x0
+ field public static final int POINTER_TYPE_LEFT = 1; // 0x1
+ field public static final int POINTER_TYPE_RIGHT = 2; // 0x2
+ field public static final int SOURCE_CONTROLLER = 2; // 0x2
+ field public static final int SOURCE_GAZE_AND_GESTURE = 5; // 0x5
+ field public static final int SOURCE_HANDS = 3; // 0x3
+ field public static final int SOURCE_HEAD = 1; // 0x1
+ field public static final int SOURCE_MOUSE = 4; // 0x4
+ field public static final int SOURCE_UNKNOWN = 0; // 0x0
+ field public int action;
+ field public androidx.xr.runtime.math.Vector3 direction;
+ field public androidx.xr.scenecore.JxrPlatformAdapter.InputEvent.HitInfo? hitInfo;
+ field public androidx.xr.runtime.math.Vector3 origin;
+ field public int pointerType;
+ field public androidx.xr.scenecore.JxrPlatformAdapter.InputEvent.HitInfo? secondaryHitInfo;
+ field public int source;
+ field public long timestamp;
+ }
+
+ public static class JxrPlatformAdapter.InputEvent.HitInfo {
+ ctor public JxrPlatformAdapter.InputEvent.HitInfo(androidx.xr.scenecore.JxrPlatformAdapter.Entity?, androidx.xr.runtime.math.Vector3?, androidx.xr.runtime.math.Matrix4);
+ field public final androidx.xr.runtime.math.Vector3? hitPosition;
+ field public final androidx.xr.scenecore.JxrPlatformAdapter.Entity? inputEntity;
+ field public final androidx.xr.runtime.math.Matrix4 transform;
+ }
+
+ @java.lang.FunctionalInterface public static interface JxrPlatformAdapter.InputEventListener {
+ method public void onInputEvent(androidx.xr.scenecore.JxrPlatformAdapter.InputEvent);
+ }
+
+ public static interface JxrPlatformAdapter.InteractableComponent extends androidx.xr.scenecore.JxrPlatformAdapter.Component {
+ }
+
+ public static interface JxrPlatformAdapter.LoggingEntity extends androidx.xr.scenecore.JxrPlatformAdapter.Entity {
+ }
+
+ public static interface JxrPlatformAdapter.MediaPlayerExtensionsWrapper {
+ method public void setPointSourceAttributes(android.media.MediaPlayer, androidx.xr.scenecore.JxrPlatformAdapter.PointSourceAttributes);
+ method public void setSoundFieldAttributes(android.media.MediaPlayer, androidx.xr.scenecore.JxrPlatformAdapter.SoundFieldAttributes);
+ }
+
+ public static interface JxrPlatformAdapter.MovableComponent extends androidx.xr.scenecore.JxrPlatformAdapter.Component {
+ method public void addMoveEventListener(java.util.concurrent.Executor, androidx.xr.scenecore.JxrPlatformAdapter.MoveEventListener);
+ method public int getScaleWithDistanceMode();
+ method public void removeMoveEventListener(androidx.xr.scenecore.JxrPlatformAdapter.MoveEventListener);
+ method public void setScaleWithDistanceMode(int);
+ method public void setSize(androidx.xr.scenecore.JxrPlatformAdapter.Dimensions);
+ }
+
+ public static class JxrPlatformAdapter.MoveEvent {
+ ctor public JxrPlatformAdapter.MoveEvent(int, androidx.xr.scenecore.JxrPlatformAdapter.Ray, androidx.xr.scenecore.JxrPlatformAdapter.Ray, androidx.xr.runtime.math.Pose, androidx.xr.runtime.math.Pose, androidx.xr.runtime.math.Vector3, androidx.xr.runtime.math.Vector3, androidx.xr.scenecore.JxrPlatformAdapter.Entity, androidx.xr.scenecore.JxrPlatformAdapter.Entity?, androidx.xr.scenecore.JxrPlatformAdapter.Entity?);
+ field public static final int MOVE_STATE_END = 3; // 0x3
+ field public static final int MOVE_STATE_ONGOING = 2; // 0x2
+ field public static final int MOVE_STATE_START = 1; // 0x1
+ field public final androidx.xr.scenecore.JxrPlatformAdapter.Ray currentInputRay;
+ field public final androidx.xr.runtime.math.Pose currentPose;
+ field public final androidx.xr.runtime.math.Vector3 currentScale;
+ field public final androidx.xr.scenecore.JxrPlatformAdapter.Entity? disposedEntity;
+ field public final androidx.xr.scenecore.JxrPlatformAdapter.Ray initialInputRay;
+ field public final androidx.xr.scenecore.JxrPlatformAdapter.Entity initialParent;
+ field public final int moveState;
+ field public final androidx.xr.runtime.math.Pose previousPose;
+ field public final androidx.xr.runtime.math.Vector3 previousScale;
+ field public final androidx.xr.scenecore.JxrPlatformAdapter.Entity? updatedParent;
+ }
+
+ @java.lang.FunctionalInterface public static interface JxrPlatformAdapter.MoveEventListener {
+ method public void onMoveEvent(androidx.xr.scenecore.JxrPlatformAdapter.MoveEvent);
+ }
+
+ public static interface JxrPlatformAdapter.PanelEntity extends androidx.xr.scenecore.JxrPlatformAdapter.Entity {
+ method public float getCornerRadius();
+ method public androidx.xr.runtime.math.Vector3 getPixelDensity();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.PixelDimensions getPixelDimensions();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.Dimensions getSize();
+ method public void setCornerRadius(float);
+ method public void setPixelDimensions(androidx.xr.scenecore.JxrPlatformAdapter.PixelDimensions);
+ }
+
+ public static interface JxrPlatformAdapter.PerceptionSpaceActivityPose extends androidx.xr.scenecore.JxrPlatformAdapter.ActivityPose {
+ }
+
+ public static class JxrPlatformAdapter.PixelDimensions {
+ ctor public JxrPlatformAdapter.PixelDimensions(int, int);
+ field public final int height;
+ field public final int width;
+ }
+
+ public enum JxrPlatformAdapter.PlaneSemantic {
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.PlaneSemantic ANY;
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.PlaneSemantic CEILING;
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.PlaneSemantic FLOOR;
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.PlaneSemantic TABLE;
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.PlaneSemantic WALL;
+ }
+
+ public enum JxrPlatformAdapter.PlaneType {
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.PlaneType ANY;
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.PlaneType HORIZONTAL;
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.PlaneType VERTICAL;
+ }
+
+ public static class JxrPlatformAdapter.PointSourceAttributes {
+ ctor public JxrPlatformAdapter.PointSourceAttributes(androidx.xr.scenecore.JxrPlatformAdapter.Entity);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.Entity getEntity();
+ }
+
+ public static interface JxrPlatformAdapter.PointerCaptureComponent extends androidx.xr.scenecore.JxrPlatformAdapter.Component {
+ field public static final int POINTER_CAPTURE_STATE_ACTIVE = 1; // 0x1
+ field public static final int POINTER_CAPTURE_STATE_PAUSED = 0; // 0x0
+ field public static final int POINTER_CAPTURE_STATE_STOPPED = 2; // 0x2
+ }
+
+ public static interface JxrPlatformAdapter.PointerCaptureComponent.StateListener {
+ method public void onStateChanged(int);
+ }
+
+ public static class JxrPlatformAdapter.Ray {
+ ctor public JxrPlatformAdapter.Ray(androidx.xr.runtime.math.Vector3, androidx.xr.runtime.math.Vector3);
+ field public final androidx.xr.runtime.math.Vector3 direction;
+ field public final androidx.xr.runtime.math.Vector3 origin;
+ }
+
+ public static interface JxrPlatformAdapter.ResizableComponent extends androidx.xr.scenecore.JxrPlatformAdapter.Component {
+ method public void addResizeEventListener(java.util.concurrent.Executor, androidx.xr.scenecore.JxrPlatformAdapter.ResizeEventListener);
+ method public void removeResizeEventListener(androidx.xr.scenecore.JxrPlatformAdapter.ResizeEventListener);
+ method public void setFixedAspectRatio(float);
+ method public void setMaximumSize(androidx.xr.scenecore.JxrPlatformAdapter.Dimensions);
+ method public void setMinimumSize(androidx.xr.scenecore.JxrPlatformAdapter.Dimensions);
+ method public void setSize(androidx.xr.scenecore.JxrPlatformAdapter.Dimensions);
+ }
+
+ public static class JxrPlatformAdapter.ResizeEvent {
+ ctor public JxrPlatformAdapter.ResizeEvent(int, androidx.xr.scenecore.JxrPlatformAdapter.Dimensions);
+ field public static final int RESIZE_STATE_END = 3; // 0x3
+ field public static final int RESIZE_STATE_ONGOING = 2; // 0x2
+ field public static final int RESIZE_STATE_START = 1; // 0x1
+ field public static final int RESIZE_STATE_UNKNOWN = 0; // 0x0
+ field public final androidx.xr.scenecore.JxrPlatformAdapter.Dimensions newSize;
+ field public final int resizeState;
+ }
+
+ @java.lang.FunctionalInterface public static interface JxrPlatformAdapter.ResizeEventListener {
+ method public void onResizeEvent(androidx.xr.scenecore.JxrPlatformAdapter.ResizeEvent);
+ }
+
+ public static interface JxrPlatformAdapter.Resource {
+ }
+
+ public static class JxrPlatformAdapter.SoundFieldAttributes {
+ ctor public JxrPlatformAdapter.SoundFieldAttributes(int);
+ method public int getAmbisonicsOrder();
+ }
+
+ public static interface JxrPlatformAdapter.SoundPoolExtensionsWrapper {
+ method public int getSpatialSourceType(android.media.SoundPool, int);
+ method public int play(android.media.SoundPool, int, androidx.xr.scenecore.JxrPlatformAdapter.PointSourceAttributes, float, int, int, float);
+ method public int play(android.media.SoundPool, int, androidx.xr.scenecore.JxrPlatformAdapter.SoundFieldAttributes, float, int, int, float);
+ }
+
+ public static class JxrPlatformAdapter.SpatialCapabilities {
+ ctor public JxrPlatformAdapter.SpatialCapabilities(int);
+ method public boolean hasCapability(int);
+ field public static final int SPATIAL_CAPABILITY_3D_CONTENT = 2; // 0x2
+ field public static final int SPATIAL_CAPABILITY_APP_ENVIRONMENT = 8; // 0x8
+ field public static final int SPATIAL_CAPABILITY_EMBED_ACTIVITY = 32; // 0x20
+ field public static final int SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL = 4; // 0x4
+ field public static final int SPATIAL_CAPABILITY_SPATIAL_AUDIO = 16; // 0x10
+ field public static final int SPATIAL_CAPABILITY_UI = 1; // 0x1
+ field public int capabilities;
+ }
+
+ public static interface JxrPlatformAdapter.SpatialEnvironment {
+ method public void addOnPassthroughOpacityChangedListener(java.util.function.Consumer<java.lang.Float!>);
+ method public void addOnSpatialEnvironmentChangedListener(java.util.function.Consumer<java.lang.Boolean!>);
+ method public float getCurrentPassthroughOpacity();
+ method public Float? getPassthroughOpacityPreference();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.SpatialEnvironment.SpatialEnvironmentPreference? getSpatialEnvironmentPreference();
+ method public boolean isSpatialEnvironmentPreferenceActive();
+ method public void removeOnPassthroughOpacityChangedListener(java.util.function.Consumer<java.lang.Float!>);
+ method public void removeOnSpatialEnvironmentChangedListener(java.util.function.Consumer<java.lang.Boolean!>);
+ method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.xr.scenecore.JxrPlatformAdapter.SpatialEnvironment.SetPassthroughOpacityPreferenceResult setPassthroughOpacityPreference(Float?);
+ method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.xr.scenecore.JxrPlatformAdapter.SpatialEnvironment.SetSpatialEnvironmentPreferenceResult setSpatialEnvironmentPreference(androidx.xr.scenecore.JxrPlatformAdapter.SpatialEnvironment.SpatialEnvironmentPreference?);
+ }
+
+ public enum JxrPlatformAdapter.SpatialEnvironment.SetPassthroughOpacityPreferenceResult {
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.SpatialEnvironment.SetPassthroughOpacityPreferenceResult CHANGE_APPLIED;
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.SpatialEnvironment.SetPassthroughOpacityPreferenceResult CHANGE_PENDING;
+ }
+
+ public enum JxrPlatformAdapter.SpatialEnvironment.SetSpatialEnvironmentPreferenceResult {
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.SpatialEnvironment.SetSpatialEnvironmentPreferenceResult CHANGE_APPLIED;
+ enum_constant public static final androidx.xr.scenecore.JxrPlatformAdapter.SpatialEnvironment.SetSpatialEnvironmentPreferenceResult CHANGE_PENDING;
+ }
+
+ public static class JxrPlatformAdapter.SpatialEnvironment.SpatialEnvironmentPreference {
+ ctor public JxrPlatformAdapter.SpatialEnvironment.SpatialEnvironmentPreference(androidx.xr.scenecore.JxrPlatformAdapter.ExrImageResource?, androidx.xr.scenecore.JxrPlatformAdapter.GltfModelResource?);
+ field public final androidx.xr.scenecore.JxrPlatformAdapter.GltfModelResource? geometry;
+ field public final androidx.xr.scenecore.JxrPlatformAdapter.ExrImageResource? skybox;
+ }
+
+ public static final class JxrPlatformAdapter.SpatializerConstants {
+ field public static final int AMBISONICS_ORDER_FIRST_ORDER = 0; // 0x0
+ field public static final int AMBISONICS_ORDER_SECOND_ORDER = 1; // 0x1
+ field public static final int AMBISONICS_ORDER_THIRD_ORDER = 2; // 0x2
+ field public static final int SOURCE_TYPE_BYPASS = 0; // 0x0
+ field public static final int SOURCE_TYPE_POINT_SOURCE = 1; // 0x1
+ field public static final int SOURCE_TYPE_SOUND_FIELD = 2; // 0x2
+ }
+
+ public static interface JxrPlatformAdapter.StereoSurfaceEntity extends androidx.xr.scenecore.JxrPlatformAdapter.Entity {
+ method public androidx.xr.scenecore.JxrPlatformAdapter.Dimensions getDimensions();
+ method public int getStereoMode();
+ method public android.view.Surface getSurface();
+ method public void setDimensions(androidx.xr.scenecore.JxrPlatformAdapter.Dimensions);
+ method public void setStereoMode(int);
+ }
+
+ public static interface JxrPlatformAdapter.SystemSpaceEntity extends androidx.xr.scenecore.JxrPlatformAdapter.Entity {
+ method public void setOnSpaceUpdatedListener(androidx.xr.scenecore.JxrPlatformAdapter.SystemSpaceEntity.OnSpaceUpdatedListener?, java.util.concurrent.Executor?);
+ }
+
+ @java.lang.FunctionalInterface public static interface JxrPlatformAdapter.SystemSpaceEntity.OnSpaceUpdatedListener {
+ method public void onSpaceUpdated();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Model {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class MovableComponent implements androidx.xr.scenecore.Component {
+ method public void addMoveListener(java.util.concurrent.Executor executor, androidx.xr.scenecore.MoveListener moveListener);
+ method public androidx.xr.scenecore.Dimensions getSize();
+ method public boolean onAttach(androidx.xr.scenecore.Entity entity);
+ method public void onDetach(androidx.xr.scenecore.Entity entity);
+ method public void removeMoveListener(androidx.xr.scenecore.MoveListener moveListener);
+ method public void setSize(androidx.xr.scenecore.Dimensions);
+ property public final androidx.xr.scenecore.Dimensions size;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface MoveListener {
+ method public default void onMoveEnd(androidx.xr.scenecore.Entity entity, androidx.xr.runtime.math.Ray finalInputRay, androidx.xr.runtime.math.Pose finalPose, float finalScale, androidx.xr.scenecore.Entity? updatedParent);
+ method public default void onMoveStart(androidx.xr.scenecore.Entity entity, androidx.xr.runtime.math.Ray initialInputRay, androidx.xr.runtime.math.Pose initialPose, float initialScale, androidx.xr.scenecore.Entity initialParent);
+ method public default void onMoveUpdate(androidx.xr.scenecore.Entity entity, androidx.xr.runtime.math.Ray currentInputRay, androidx.xr.runtime.math.Pose currentPose, float currentScale);
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public fun interface OnBoundsChangeListener {
+ method @Deprecated public void onBoundsChanged(androidx.xr.scenecore.Dimensions bounds);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public fun interface OnSpaceUpdatedListener {
+ method public void onSpaceUpdated();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public fun interface OnStateChangedListener {
+ method public void onStateChanged(int newState);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class PanelEntity extends androidx.xr.scenecore.BasePanelEntity<androidx.xr.scenecore.JxrPlatformAdapter.PanelEntity> {
+ method public final boolean isMainPanelEntity();
+ property public final boolean isMainPanelEntity;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class PerceptionSpace extends androidx.xr.scenecore.BaseActivityPose<androidx.xr.scenecore.JxrPlatformAdapter.PerceptionSpaceActivityPose> {
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class PermissionHelper {
+ method public boolean hasPermission(android.app.Activity activity, String permission);
+ method public void launchPermissionSettings(android.app.Activity activity);
+ method public void requestPermission(android.app.Activity activity, String permission, int permissionCode);
+ method public boolean shouldShowRequestPermissionRationale(android.app.Activity activity, String permission);
+ property public static final String SCENE_UNDERSTANDING_PERMISSION;
+ property public static final int SCENE_UNDERSTANDING_PERMISSION_CODE;
+ field public static final androidx.xr.scenecore.PermissionHelper INSTANCE;
+ field public static final String SCENE_UNDERSTANDING_PERMISSION = "android.permission.SCENE_UNDERSTANDING";
+ field public static final int SCENE_UNDERSTANDING_PERMISSION_CODE = 0; // 0x0
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class PixelDimensions {
+ ctor public PixelDimensions();
+ ctor public PixelDimensions(optional int width, optional int height);
+ method public int component1();
+ method public int component2();
+ method public androidx.xr.scenecore.PixelDimensions copy(int width, int height);
+ method public int getHeight();
+ method public int getWidth();
+ property public final int height;
+ property public final int width;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class PlaneSemantic {
+ property public static final int ANY;
+ property public static final int CEILING;
+ property public static final int FLOOR;
+ property public static final int TABLE;
+ property public static final int WALL;
+ field public static final int ANY = 4; // 0x4
+ field public static final int CEILING = 2; // 0x2
+ field public static final int FLOOR = 1; // 0x1
+ field public static final androidx.xr.scenecore.PlaneSemantic INSTANCE;
+ field public static final int TABLE = 3; // 0x3
+ field public static final int WALL = 0; // 0x0
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class PlaneType {
+ property public static final int ANY;
+ property public static final int HORIZONTAL;
+ property public static final int VERTICAL;
+ field public static final int ANY = 2; // 0x2
+ field public static final int HORIZONTAL = 0; // 0x0
+ field public static final androidx.xr.scenecore.PlaneType INSTANCE;
+ field public static final int VERTICAL = 1; // 0x1
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class PointSourceAttributes {
+ ctor public PointSourceAttributes(androidx.xr.scenecore.Entity entity);
+ method public androidx.xr.scenecore.Entity getEntity();
+ property public final androidx.xr.scenecore.Entity entity;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class PointerCaptureComponent implements androidx.xr.scenecore.Component {
+ method public static androidx.xr.scenecore.PointerCaptureComponent create(androidx.xr.scenecore.Session session, java.util.concurrent.Executor executor, androidx.xr.scenecore.PointerCaptureComponent.StateListener stateListener, androidx.xr.scenecore.InputEventListener inputListener);
+ method public boolean onAttach(androidx.xr.scenecore.Entity entity);
+ method public void onDetach(androidx.xr.scenecore.Entity entity);
+ field public static final androidx.xr.scenecore.PointerCaptureComponent.Companion Companion;
+ field public static final int POINTER_CAPTURE_STATE_ACTIVE = 1; // 0x1
+ field public static final int POINTER_CAPTURE_STATE_PAUSED = 0; // 0x0
+ field public static final int POINTER_CAPTURE_STATE_STOPPED = 2; // 0x2
+ }
+
+ public static final class PointerCaptureComponent.Companion {
+ method public androidx.xr.scenecore.PointerCaptureComponent create(androidx.xr.scenecore.Session session, java.util.concurrent.Executor executor, androidx.xr.scenecore.PointerCaptureComponent.StateListener stateListener, androidx.xr.scenecore.InputEventListener inputListener);
+ property public static final int POINTER_CAPTURE_STATE_ACTIVE;
+ property public static final int POINTER_CAPTURE_STATE_PAUSED;
+ property public static final int POINTER_CAPTURE_STATE_STOPPED;
+ }
+
+ public static interface PointerCaptureComponent.StateListener {
+ method public void onStateChanged(int newState);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class ResizableComponent implements androidx.xr.scenecore.Component {
+ method public void addResizeListener(java.util.concurrent.Executor executor, androidx.xr.scenecore.ResizeListener resizeListener);
+ method public float getFixedAspectRatio();
+ method public androidx.xr.scenecore.Dimensions getMaximumSize();
+ method public androidx.xr.scenecore.Dimensions getMinimumSize();
+ method public androidx.xr.scenecore.Dimensions getSize();
+ method public boolean onAttach(androidx.xr.scenecore.Entity entity);
+ method public void onDetach(androidx.xr.scenecore.Entity entity);
+ method public void removeResizeListener(androidx.xr.scenecore.ResizeListener resizeListener);
+ method public void setFixedAspectRatio(float);
+ method public void setMaximumSize(androidx.xr.scenecore.Dimensions);
+ method public void setMinimumSize(androidx.xr.scenecore.Dimensions);
+ method public void setSize(androidx.xr.scenecore.Dimensions);
+ property public final float fixedAspectRatio;
+ property public final androidx.xr.scenecore.Dimensions maximumSize;
+ property public final androidx.xr.scenecore.Dimensions minimumSize;
+ property public final androidx.xr.scenecore.Dimensions size;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface ResizeListener {
+ method public default void onResizeEnd(androidx.xr.scenecore.Entity entity, androidx.xr.scenecore.Dimensions finalSize);
+ method public default void onResizeStart(androidx.xr.scenecore.Entity entity, androidx.xr.scenecore.Dimensions originalSize);
+ method public default void onResizeUpdate(androidx.xr.scenecore.Entity entity, androidx.xr.scenecore.Dimensions newSize);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class Session {
+ ctor public Session(android.app.Activity activity, androidx.xr.scenecore.JxrPlatformAdapter runtime, androidx.xr.scenecore.SpatialEnvironment spatialEnvironment);
+ method public void addSpatialCapabilitiesChangedListener(java.util.concurrent.Executor callbackExecutor, java.util.function.Consumer<androidx.xr.scenecore.SpatialCapabilities> listener);
+ method public void addSpatialCapabilitiesChangedListener(java.util.function.Consumer<androidx.xr.scenecore.SpatialCapabilities> listener);
+ method @Deprecated public boolean canEmbedActivityPanel(android.app.Activity activity);
+ method public static androidx.xr.scenecore.Session create(android.app.Activity activity);
+ method public static androidx.xr.scenecore.Session create(android.app.Activity activity, optional androidx.xr.scenecore.JxrPlatformAdapter? runtime);
+ method public androidx.xr.scenecore.ActivityPanelEntity createActivityPanelEntity(android.graphics.Rect windowBoundsPx, String name);
+ method public androidx.xr.scenecore.ActivityPanelEntity createActivityPanelEntity(android.graphics.Rect windowBoundsPx, String name, optional androidx.xr.runtime.math.Pose pose);
+ method public androidx.xr.scenecore.AnchorEntity createAnchorEntity(androidx.xr.arcore.Anchor anchor);
+ method public androidx.xr.scenecore.AnchorEntity createAnchorEntity(androidx.xr.scenecore.Dimensions bounds, int planeType, int planeSemantic);
+ method public androidx.xr.scenecore.AnchorEntity createAnchorEntity(androidx.xr.scenecore.Dimensions bounds, int planeType, int planeSemantic, optional java.time.Duration timeout);
+ method public androidx.xr.scenecore.Entity createEntity(String name);
+ method public androidx.xr.scenecore.Entity createEntity(String name, optional androidx.xr.runtime.math.Pose pose);
+ method public androidx.xr.scenecore.ExrImage createExrImageResource(String name);
+ method @MainThread public androidx.xr.scenecore.GltfModelEntity createGltfEntity(androidx.xr.scenecore.GltfModel model);
+ method @MainThread public androidx.xr.scenecore.GltfModelEntity createGltfEntity(androidx.xr.scenecore.GltfModel model, optional androidx.xr.runtime.math.Pose pose);
+ method @MainThread public com.google.common.util.concurrent.ListenableFuture<androidx.xr.scenecore.GltfModel> createGltfResourceAsync(String name);
+ method public androidx.xr.scenecore.InteractableComponent createInteractableComponent(java.util.concurrent.Executor executor, androidx.xr.scenecore.InputEventListener inputEventListener);
+ method public androidx.xr.scenecore.MovableComponent createMovableComponent();
+ method public androidx.xr.scenecore.MovableComponent createMovableComponent(optional boolean systemMovable);
+ method public androidx.xr.scenecore.MovableComponent createMovableComponent(optional boolean systemMovable, optional boolean scaleInZ);
+ method public androidx.xr.scenecore.MovableComponent createMovableComponent(optional boolean systemMovable, optional boolean scaleInZ, optional java.util.Set<androidx.xr.scenecore.AnchorPlacement> anchorPlacement);
+ method public androidx.xr.scenecore.MovableComponent createMovableComponent(optional boolean systemMovable, optional boolean scaleInZ, optional java.util.Set<androidx.xr.scenecore.AnchorPlacement> anchorPlacement, optional boolean shouldDisposeParentAnchor);
+ method public androidx.xr.scenecore.PanelEntity createPanelEntity(android.view.View view, androidx.xr.scenecore.Dimensions surfaceDimensionsPx, androidx.xr.scenecore.Dimensions dimensions, String name);
+ method public androidx.xr.scenecore.PanelEntity createPanelEntity(android.view.View view, androidx.xr.scenecore.Dimensions surfaceDimensionsPx, androidx.xr.scenecore.Dimensions dimensions, String name, optional androidx.xr.runtime.math.Pose pose);
+ method public androidx.xr.scenecore.AnchorEntity createPersistedAnchorEntity(java.util.UUID uuid);
+ method public androidx.xr.scenecore.ResizableComponent createResizableComponent();
+ method public androidx.xr.scenecore.ResizableComponent createResizableComponent(optional androidx.xr.scenecore.Dimensions minimumSize);
+ method public androidx.xr.scenecore.ResizableComponent createResizableComponent(optional androidx.xr.scenecore.Dimensions minimumSize, optional androidx.xr.scenecore.Dimensions maximumSize);
+ method @MainThread public androidx.xr.scenecore.StereoSurfaceEntity createStereoSurfaceEntity();
+ method @MainThread public androidx.xr.scenecore.StereoSurfaceEntity createStereoSurfaceEntity(optional int stereoMode);
+ method @MainThread public androidx.xr.scenecore.StereoSurfaceEntity createStereoSurfaceEntity(optional int stereoMode, optional androidx.xr.scenecore.Dimensions dimensions);
+ method @MainThread public androidx.xr.scenecore.StereoSurfaceEntity createStereoSurfaceEntity(optional int stereoMode, optional androidx.xr.scenecore.Dimensions dimensions, optional androidx.xr.runtime.math.Pose pose);
+ method public android.app.Activity getActivity();
+ method public androidx.xr.scenecore.ActivitySpace getActivitySpace();
+ method public androidx.xr.scenecore.Entity getActivitySpaceRoot();
+ method public <T extends androidx.xr.scenecore.Entity> java.util.List<T> getEntitiesOfType(Class<? extends T> type);
+ method public androidx.xr.scenecore.PanelEntity getMainPanelEntity();
+ method public androidx.xr.scenecore.PerceptionSpace getPerceptionSpace();
+ method public androidx.xr.scenecore.JxrPlatformAdapter getRuntime();
+ method public androidx.xr.scenecore.SpatialCapabilities getSpatialCapabilities();
+ method public androidx.xr.scenecore.SpatialEnvironment getSpatialEnvironment();
+ method public androidx.xr.scenecore.SpatialUser getSpatialUser();
+ method @Deprecated public boolean hasSpatialCapability(int capability);
+ method public void removeSpatialCapabilitiesChangedListener(java.util.function.Consumer<androidx.xr.scenecore.SpatialCapabilities> listener);
+ method public void requestFullSpaceMode();
+ method public void requestHomeSpaceMode();
+ method public android.os.Bundle setFullSpaceMode(android.os.Bundle bundle);
+ method public android.os.Bundle setFullSpaceModeWithEnvironmentInherited(android.os.Bundle bundle);
+ method public void setPreferredAspectRatio(android.app.Activity activity, float preferredRatio);
+ method public boolean unpersistAnchor(java.util.UUID uuid);
+ property public final android.app.Activity activity;
+ property public final androidx.xr.scenecore.ActivitySpace activitySpace;
+ property public final androidx.xr.scenecore.Entity activitySpaceRoot;
+ property public final androidx.xr.scenecore.PanelEntity mainPanelEntity;
+ property public final androidx.xr.scenecore.PerceptionSpace perceptionSpace;
+ property public final androidx.xr.scenecore.JxrPlatformAdapter runtime;
+ property public final androidx.xr.scenecore.SpatialEnvironment spatialEnvironment;
+ property public final androidx.xr.scenecore.SpatialUser spatialUser;
+ field public static final androidx.xr.scenecore.Session.Companion Companion;
+ }
+
+ public static final class Session.Companion {
+ method public androidx.xr.scenecore.Session create(android.app.Activity activity);
+ method public androidx.xr.scenecore.Session create(android.app.Activity activity, optional androidx.xr.scenecore.JxrPlatformAdapter? runtime);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SoundFieldAttributes {
+ ctor public SoundFieldAttributes(int order);
+ method public int getOrder();
+ property public final int order;
+ field public static final String TAG = "SoundFieldAttributes";
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SpatialAudioTrack {
+ ctor public SpatialAudioTrack();
+ method public static androidx.xr.scenecore.PointSourceAttributes? getPointSourceAttributes(androidx.xr.scenecore.Session session, android.media.AudioTrack track);
+ method public static androidx.xr.scenecore.SoundFieldAttributes? getSoundFieldAttributes(androidx.xr.scenecore.Session session, android.media.AudioTrack track);
+ method public static int getSpatialSourceType(androidx.xr.scenecore.Session session, android.media.AudioTrack track);
+ field public static final androidx.xr.scenecore.SpatialAudioTrack.Companion Companion;
+ }
+
+ public static final class SpatialAudioTrack.Companion {
+ method public androidx.xr.scenecore.PointSourceAttributes? getPointSourceAttributes(androidx.xr.scenecore.Session session, android.media.AudioTrack track);
+ method public androidx.xr.scenecore.SoundFieldAttributes? getSoundFieldAttributes(androidx.xr.scenecore.Session session, android.media.AudioTrack track);
+ method public int getSpatialSourceType(androidx.xr.scenecore.Session session, android.media.AudioTrack track);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SpatialAudioTrackBuilder {
+ method public static android.media.AudioTrack.Builder setPointSourceAttributes(androidx.xr.scenecore.Session session, android.media.AudioTrack.Builder builder, androidx.xr.scenecore.PointSourceAttributes attributes);
+ method public static android.media.AudioTrack.Builder setSoundFieldAttributes(androidx.xr.scenecore.Session session, android.media.AudioTrack.Builder builder, androidx.xr.scenecore.SoundFieldAttributes attributes);
+ field public static final androidx.xr.scenecore.SpatialAudioTrackBuilder.Companion Companion;
+ }
+
+ public static final class SpatialAudioTrackBuilder.Companion {
+ method public android.media.AudioTrack.Builder setPointSourceAttributes(androidx.xr.scenecore.Session session, android.media.AudioTrack.Builder builder, androidx.xr.scenecore.PointSourceAttributes attributes);
+ method public android.media.AudioTrack.Builder setSoundFieldAttributes(androidx.xr.scenecore.Session session, android.media.AudioTrack.Builder builder, androidx.xr.scenecore.SoundFieldAttributes attributes);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SpatialCapabilities {
+ ctor public SpatialCapabilities(int capabilities);
+ method public boolean hasCapability(int capability);
+ field public static final androidx.xr.scenecore.SpatialCapabilities.Companion Companion;
+ field public static final int SPATIAL_CAPABILITY_3D_CONTENT = 2; // 0x2
+ field public static final int SPATIAL_CAPABILITY_APP_ENVIRONMENT = 8; // 0x8
+ field public static final int SPATIAL_CAPABILITY_EMBED_ACTIVITY = 32; // 0x20
+ field public static final int SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL = 4; // 0x4
+ field public static final int SPATIAL_CAPABILITY_SPATIAL_AUDIO = 16; // 0x10
+ field public static final int SPATIAL_CAPABILITY_UI = 1; // 0x1
+ }
+
+ public static final class SpatialCapabilities.Companion {
+ property public static final int SPATIAL_CAPABILITY_3D_CONTENT;
+ property public static final int SPATIAL_CAPABILITY_APP_ENVIRONMENT;
+ property public static final int SPATIAL_CAPABILITY_EMBED_ACTIVITY;
+ property public static final int SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL;
+ property public static final int SPATIAL_CAPABILITY_SPATIAL_AUDIO;
+ property public static final int SPATIAL_CAPABILITY_UI;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SpatialEnvironment {
+ ctor public SpatialEnvironment(androidx.xr.scenecore.JxrPlatformAdapter runtime);
+ method public void addOnPassthroughOpacityChangedListener(java.util.function.Consumer<java.lang.Float> listener);
+ method public void addOnSpatialEnvironmentChangedListener(java.util.function.Consumer<java.lang.Boolean> listener);
+ method public float getCurrentPassthroughOpacity();
+ method @Deprecated public androidx.xr.scenecore.SpatialEnvironment.PassthroughMode getPassthroughMode();
+ method @Deprecated public float getPassthroughOpacity();
+ method public Float? getPassthroughOpacityPreference();
+ method public androidx.xr.scenecore.SpatialEnvironment.SpatialEnvironmentPreference? getSpatialEnvironmentPreference();
+ method public boolean isSpatialEnvironmentPreferenceActive();
+ method public void removeOnPassthroughOpacityChangedListener(java.util.function.Consumer<java.lang.Float> listener);
+ method public void removeOnSpatialEnvironmentChangedListener(java.util.function.Consumer<java.lang.Boolean> listener);
+ method @Deprecated public void setGeometry(androidx.xr.scenecore.GltfModel? gltfModel);
+ method @Deprecated public void setPassthrough(androidx.xr.scenecore.SpatialEnvironment.PassthroughMode passthroughMode);
+ method @Deprecated public void setPassthroughOpacity(float passthroughOpacity);
+ method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.xr.scenecore.SpatialEnvironment.SetPassthroughOpacityPreferenceResult setPassthroughOpacityPreference(Float? passthroughOpacityPreference);
+ method @Deprecated public void setSkybox(androidx.xr.scenecore.ExrImage? exrImage);
+ method @com.google.errorprone.annotations.CanIgnoreReturnValue public androidx.xr.scenecore.SpatialEnvironment.SetSpatialEnvironmentPreferenceResult setSpatialEnvironmentPreference(androidx.xr.scenecore.SpatialEnvironment.SpatialEnvironmentPreference? environmentPreference);
+ }
+
+ @Deprecated public static final class SpatialEnvironment.PassthroughMode {
+ method @Deprecated public int getValue();
+ property @Deprecated public final int value;
+ field @Deprecated public static final androidx.xr.scenecore.SpatialEnvironment.PassthroughMode.Companion Companion;
+ field @Deprecated public static final androidx.xr.scenecore.SpatialEnvironment.PassthroughMode Disabled;
+ field @Deprecated public static final androidx.xr.scenecore.SpatialEnvironment.PassthroughMode Enabled;
+ field @Deprecated public static final androidx.xr.scenecore.SpatialEnvironment.PassthroughMode Uninitialized;
+ }
+
+ @Deprecated public static final class SpatialEnvironment.PassthroughMode.Companion {
+ property @Deprecated public final androidx.xr.scenecore.SpatialEnvironment.PassthroughMode Disabled;
+ property @Deprecated public final androidx.xr.scenecore.SpatialEnvironment.PassthroughMode Enabled;
+ property @Deprecated public final androidx.xr.scenecore.SpatialEnvironment.PassthroughMode Uninitialized;
+ }
+
+ public static final class SpatialEnvironment.SetPassthroughOpacityPreferenceChangeApplied extends androidx.xr.scenecore.SpatialEnvironment.SetPassthroughOpacityPreferenceResult {
+ ctor public SpatialEnvironment.SetPassthroughOpacityPreferenceChangeApplied();
+ }
+
+ public static final class SpatialEnvironment.SetPassthroughOpacityPreferenceChangePending extends androidx.xr.scenecore.SpatialEnvironment.SetPassthroughOpacityPreferenceResult {
+ ctor public SpatialEnvironment.SetPassthroughOpacityPreferenceChangePending();
+ }
+
+ public abstract static sealed class SpatialEnvironment.SetPassthroughOpacityPreferenceResult {
+ }
+
+ public static final class SpatialEnvironment.SetSpatialEnvironmentPreferenceChangeApplied extends androidx.xr.scenecore.SpatialEnvironment.SetSpatialEnvironmentPreferenceResult {
+ ctor public SpatialEnvironment.SetSpatialEnvironmentPreferenceChangeApplied();
+ }
+
+ public static final class SpatialEnvironment.SetSpatialEnvironmentPreferenceChangePending extends androidx.xr.scenecore.SpatialEnvironment.SetSpatialEnvironmentPreferenceResult {
+ ctor public SpatialEnvironment.SetSpatialEnvironmentPreferenceChangePending();
+ }
+
+ public abstract static sealed class SpatialEnvironment.SetSpatialEnvironmentPreferenceResult {
+ }
+
+ public static final class SpatialEnvironment.SpatialEnvironmentPreference {
+ ctor public SpatialEnvironment.SpatialEnvironmentPreference(androidx.xr.scenecore.ExrImage? skybox, androidx.xr.scenecore.GltfModel? geometry);
+ method public androidx.xr.scenecore.GltfModel? getGeometry();
+ method public androidx.xr.scenecore.ExrImage? getSkybox();
+ property public final androidx.xr.scenecore.GltfModel? geometry;
+ property public final androidx.xr.scenecore.ExrImage? skybox;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SpatialMediaPlayer {
+ ctor public SpatialMediaPlayer();
+ method public static void setPointSourceAttributes(androidx.xr.scenecore.Session session, android.media.MediaPlayer mediaPlayer, androidx.xr.scenecore.PointSourceAttributes attributes);
+ method public static void setSoundFieldAttributes(androidx.xr.scenecore.Session session, android.media.MediaPlayer mediaPlayer, androidx.xr.scenecore.SoundFieldAttributes attributes);
+ field public static final androidx.xr.scenecore.SpatialMediaPlayer.Companion Companion;
+ }
+
+ public static final class SpatialMediaPlayer.Companion {
+ method public void setPointSourceAttributes(androidx.xr.scenecore.Session session, android.media.MediaPlayer mediaPlayer, androidx.xr.scenecore.PointSourceAttributes attributes);
+ method public void setSoundFieldAttributes(androidx.xr.scenecore.Session session, android.media.MediaPlayer mediaPlayer, androidx.xr.scenecore.SoundFieldAttributes attributes);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SpatialSoundPool {
+ method public static int getSpatialSourceType(androidx.xr.scenecore.Session session, android.media.SoundPool soundPool, int streamId);
+ method public static int play(androidx.xr.scenecore.Session session, android.media.SoundPool soundPool, int soundID, androidx.xr.scenecore.PointSourceAttributes attributes, float volume, int priority, int loop, float rate);
+ method public static int play(androidx.xr.scenecore.Session session, android.media.SoundPool soundPool, int soundID, androidx.xr.scenecore.SoundFieldAttributes attributes, float volume, int priority, int loop, float rate);
+ field public static final androidx.xr.scenecore.SpatialSoundPool.Companion Companion;
+ }
+
+ public static final class SpatialSoundPool.Companion {
+ method public int getSpatialSourceType(androidx.xr.scenecore.Session session, android.media.SoundPool soundPool, int streamId);
+ method public int play(androidx.xr.scenecore.Session session, android.media.SoundPool soundPool, int soundID, androidx.xr.scenecore.PointSourceAttributes attributes, float volume, int priority, int loop, float rate);
+ method public int play(androidx.xr.scenecore.Session session, android.media.SoundPool soundPool, int soundID, androidx.xr.scenecore.SoundFieldAttributes attributes, float volume, int priority, int loop, float rate);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SpatialUser {
+ ctor public SpatialUser(androidx.xr.scenecore.JxrPlatformAdapter runtime);
+ method public androidx.xr.scenecore.CameraView? getCameraView(androidx.xr.scenecore.CameraView.CameraType cameraType);
+ method public java.util.List<androidx.xr.scenecore.CameraView> getCameraViews();
+ method public androidx.xr.scenecore.Head? getHead();
+ method public void setHead(androidx.xr.scenecore.Head?);
+ property public final androidx.xr.scenecore.Head? head;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SpatializerConstants {
+ field public static final int AMBISONICS_ORDER_FIRST_ORDER = 0; // 0x0
+ field public static final int AMBISONICS_ORDER_SECOND_ORDER = 1; // 0x1
+ field public static final int AMBISONICS_ORDER_THIRD_ORDER = 2; // 0x2
+ field public static final androidx.xr.scenecore.SpatializerConstants.Companion Companion;
+ field public static final int SOURCE_TYPE_BYPASS = 0; // 0x0
+ field public static final int SOURCE_TYPE_POINT_SOURCE = 1; // 0x1
+ field public static final int SOURCE_TYPE_SOUND_FIELD = 2; // 0x2
+ }
+
+ public static final class SpatializerConstants.Companion {
+ property public static final int AMBISONICS_ORDER_FIRST_ORDER;
+ property public static final int AMBISONICS_ORDER_SECOND_ORDER;
+ property public static final int AMBISONICS_ORDER_THIRD_ORDER;
+ property public static final int SOURCE_TYPE_BYPASS;
+ property public static final int SOURCE_TYPE_POINT_SOURCE;
+ property public static final int SOURCE_TYPE_SOUND_FIELD;
+ field public static final int AMBISONICS_ORDER_FIRST_ORDER = 0; // 0x0
+ field public static final int AMBISONICS_ORDER_SECOND_ORDER = 1; // 0x1
+ field public static final int AMBISONICS_ORDER_THIRD_ORDER = 2; // 0x2
+ field public static final int SOURCE_TYPE_BYPASS = 0; // 0x0
+ field public static final int SOURCE_TYPE_POINT_SOURCE = 1; // 0x1
+ field public static final int SOURCE_TYPE_SOUND_FIELD = 2; // 0x2
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class StereoSurfaceEntity extends androidx.xr.scenecore.BaseEntity<androidx.xr.scenecore.JxrPlatformAdapter.StereoSurfaceEntity> {
+ method public androidx.xr.scenecore.Dimensions getDimensions();
+ method @MainThread public android.view.Surface getSurface();
+ method public void setDimensions(androidx.xr.scenecore.Dimensions);
+ property public final androidx.xr.scenecore.Dimensions dimensions;
+ }
+
+ public static final class StereoSurfaceEntity.StereoMode {
+ property public static final int MONO;
+ property public static final int SIDE_BY_SIDE;
+ property public static final int TOP_BOTTOM;
+ field public static final androidx.xr.scenecore.StereoSurfaceEntity.StereoMode INSTANCE;
+ field public static final int MONO = 0; // 0x0
+ field public static final int SIDE_BY_SIDE = 2; // 0x2
+ field public static final int TOP_BOTTOM = 1; // 0x1
+ }
+
+}
+
+package androidx.xr.scenecore.common {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class BaseActivityPose implements androidx.xr.scenecore.JxrPlatformAdapter.ActivityPose {
+ ctor public BaseActivityPose();
+ method public androidx.xr.runtime.math.Pose getActivitySpacePose();
+ method public androidx.xr.runtime.math.Vector3 getActivitySpaceScale();
+ method public androidx.xr.runtime.math.Pose getPoseInActivitySpace();
+ method public androidx.xr.runtime.math.Vector3 getWorldSpaceScale();
+ method public androidx.xr.runtime.math.Pose transformPoseTo(androidx.xr.runtime.math.Pose, androidx.xr.scenecore.JxrPlatformAdapter.ActivityPose);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class BaseEntity extends androidx.xr.scenecore.common.BaseActivityPose implements androidx.xr.scenecore.JxrPlatformAdapter.Entity {
+ ctor public BaseEntity();
+ method public void addChild(androidx.xr.scenecore.JxrPlatformAdapter.Entity);
+ method protected void addChildInternal(androidx.xr.scenecore.JxrPlatformAdapter.Entity);
+ method public void addChildren(java.util.List<androidx.xr.scenecore.JxrPlatformAdapter.Entity!>);
+ method public boolean addComponent(androidx.xr.scenecore.JxrPlatformAdapter.Component);
+ method public void dispose();
+ method public float getActivitySpaceAlpha();
+ method public float getAlpha();
+ method public java.util.List<androidx.xr.scenecore.JxrPlatformAdapter.Entity!> getChildren();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.Entity? getParent();
+ method public androidx.xr.runtime.math.Pose getPose();
+ method public androidx.xr.runtime.math.Vector3 getScale();
+ method public boolean isHidden(boolean);
+ method public void removeAllComponents();
+ method protected void removeChildInternal(androidx.xr.scenecore.JxrPlatformAdapter.Entity);
+ method public void removeComponent(androidx.xr.scenecore.JxrPlatformAdapter.Component);
+ method public void setAlpha(float);
+ method public void setContentDescription(String);
+ method public void setHidden(boolean);
+ method public void setParent(androidx.xr.scenecore.JxrPlatformAdapter.Entity?);
+ method public void setPose(androidx.xr.runtime.math.Pose);
+ method public void setScale(androidx.xr.runtime.math.Vector3);
+ method protected final void setScaleInternal(androidx.xr.runtime.math.Vector3);
+ }
+
+}
+
+package androidx.xr.scenecore.impl {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class JxrPlatformAdapterAxr implements androidx.xr.scenecore.JxrPlatformAdapter {
+ method public void addSpatialCapabilitiesChangedListener(java.util.concurrent.Executor, java.util.function.Consumer<androidx.xr.scenecore.JxrPlatformAdapter.SpatialCapabilities!>);
+ method public static androidx.xr.scenecore.impl.JxrPlatformAdapterAxr create(android.app.Activity, java.util.concurrent.ScheduledExecutorService);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.scenecore.impl.JxrPlatformAdapterAxr create(android.app.Activity, java.util.concurrent.ScheduledExecutorService, androidx.xr.extensions.node.Node, androidx.xr.extensions.node.Node);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.scenecore.impl.JxrPlatformAdapterAxr create(android.app.Activity, java.util.concurrent.ScheduledExecutorService, androidx.xr.extensions.XrExtensions, com.google.ar.imp.apibindings.ImpressApi?, androidx.xr.scenecore.impl.perception.PerceptionLibrary, com.google.androidxr.splitengine.SplitEngineSubspaceManager?, com.google.ar.imp.view.splitengine.ImpSplitEngineRenderer?);
+ method public static androidx.xr.scenecore.impl.JxrPlatformAdapterAxr create(android.app.Activity, java.util.concurrent.ScheduledExecutorService, boolean);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.ActivityPanelEntity createActivityPanelEntity(androidx.xr.runtime.math.Pose, androidx.xr.scenecore.JxrPlatformAdapter.PixelDimensions, String, android.app.Activity, androidx.xr.scenecore.JxrPlatformAdapter.Entity);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity createAnchorEntity(androidx.xr.arcore.Anchor);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity createAnchorEntity(androidx.xr.scenecore.JxrPlatformAdapter.Dimensions, androidx.xr.scenecore.JxrPlatformAdapter.PlaneType, androidx.xr.scenecore.JxrPlatformAdapter.PlaneSemantic, java.time.Duration);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.AnchorPlacement createAnchorPlacementForPlanes(java.util.Set<androidx.xr.scenecore.JxrPlatformAdapter.PlaneType!>, java.util.Set<androidx.xr.scenecore.JxrPlatformAdapter.PlaneSemantic!>);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.Entity createEntity(androidx.xr.runtime.math.Pose, String, androidx.xr.scenecore.JxrPlatformAdapter.Entity);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.GltfEntity createGltfEntity(androidx.xr.runtime.math.Pose, androidx.xr.scenecore.JxrPlatformAdapter.GltfModelResource, androidx.xr.scenecore.JxrPlatformAdapter.Entity?);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.InteractableComponent createInteractableComponent(java.util.concurrent.Executor, androidx.xr.scenecore.JxrPlatformAdapter.InputEventListener);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.LoggingEntity createLoggingEntity(androidx.xr.runtime.math.Pose);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.MovableComponent createMovableComponent(boolean, boolean, java.util.Set<androidx.xr.scenecore.JxrPlatformAdapter.AnchorPlacement!>, boolean);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.PanelEntity createPanelEntity(androidx.xr.runtime.math.Pose, android.view.View, androidx.xr.scenecore.JxrPlatformAdapter.PixelDimensions, androidx.xr.scenecore.JxrPlatformAdapter.Dimensions, String, android.content.Context, androidx.xr.scenecore.JxrPlatformAdapter.Entity);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity createPersistedAnchorEntity(java.util.UUID, java.time.Duration);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.PointerCaptureComponent createPointerCaptureComponent(java.util.concurrent.Executor, androidx.xr.scenecore.JxrPlatformAdapter.PointerCaptureComponent.StateListener, androidx.xr.scenecore.JxrPlatformAdapter.InputEventListener);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.ResizableComponent createResizableComponent(androidx.xr.scenecore.JxrPlatformAdapter.Dimensions, androidx.xr.scenecore.JxrPlatformAdapter.Dimensions);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.StereoSurfaceEntity createStereoSurfaceEntity(int, androidx.xr.scenecore.JxrPlatformAdapter.Dimensions, androidx.xr.runtime.math.Pose, androidx.xr.scenecore.JxrPlatformAdapter.Entity);
+ method public void dispose();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.ActivitySpace getActivitySpace();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.Entity getActivitySpaceRootImpl();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.AudioTrackExtensionsWrapper getAudioTrackExtensionsWrapper();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.CameraViewActivityPose? getCameraViewActivityPose(int);
+ method public androidx.xr.scenecore.JxrPlatformAdapter.HeadActivityPose? getHeadActivityPose();
+ method public androidx.xr.runtime.math.Pose? getHeadPoseInOpenXrUnboundedSpace();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.PanelEntity getMainPanelEntity();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.MediaPlayerExtensionsWrapper getMediaPlayerExtensionsWrapper();
+ method public long getNativeInstance();
+ method public long getNativeSession();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.PerceptionSpaceActivityPose getPerceptionSpaceActivityPose();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.SoundPoolExtensionsWrapper getSoundPoolExtensionsWrapper();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.SpatialCapabilities getSpatialCapabilities();
+ method public androidx.xr.scenecore.JxrPlatformAdapter.SpatialEnvironment getSpatialEnvironment();
+ method public androidx.xr.scenecore.impl.perception.ViewProjections? getStereoViewsInOpenXrUnboundedSpace();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.scenecore.JxrPlatformAdapter.ExrImageResource!>? loadExrImageByAssetName(String);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.scenecore.JxrPlatformAdapter.GltfModelResource!>? loadGltfByAssetName(String);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.scenecore.JxrPlatformAdapter.GltfModelResource!>? loadGltfByAssetNameSplitEngine(String);
+ method public void removeSpatialCapabilitiesChangedListener(java.util.function.Consumer<androidx.xr.scenecore.JxrPlatformAdapter.SpatialCapabilities!>);
+ method public void requestFullSpaceMode();
+ method public void requestHomeSpaceMode();
+ method public android.os.Bundle setFullSpaceMode(android.os.Bundle);
+ method public android.os.Bundle setFullSpaceModeWithEnvironmentInherited(android.os.Bundle);
+ method public void setPreferredAspectRatio(android.app.Activity, float);
+ method public void setSplitEngineSubspaceManager(com.google.androidxr.splitengine.SplitEngineSubspaceManager?);
+ method public void startRenderer();
+ method public void stopRenderer();
+ method public boolean unpersistAnchor(java.util.UUID);
+ }
+
+ public final class Matrix4Ext {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.runtime.math.Matrix4 getUnscaled(androidx.xr.runtime.math.Matrix4);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class MediaUtils {
+ }
+
+}
+
+package androidx.xr.scenecore.impl.perception {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class Anchor {
+ ctor public Anchor(long, android.os.IBinder);
+ method public boolean detach();
+ method public long getAnchorId();
+ method public android.os.IBinder getAnchorToken();
+ method public androidx.xr.scenecore.impl.perception.Anchor.PersistState getPersistState();
+ method public java.util.UUID? persist();
+ }
+
+ public enum Anchor.PersistState {
+ enum_constant public static final androidx.xr.scenecore.impl.perception.Anchor.PersistState NOT_VALID;
+ enum_constant public static final androidx.xr.scenecore.impl.perception.Anchor.PersistState PERSISTED;
+ enum_constant public static final androidx.xr.scenecore.impl.perception.Anchor.PersistState PERSIST_NOT_REQUESTED;
+ enum_constant public static final androidx.xr.scenecore.impl.perception.Anchor.PersistState PERSIST_PENDING;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class Fov {
+ ctor public Fov(float, float, float, float);
+ method public float getAngleDown();
+ method public float getAngleLeft();
+ method public float getAngleRight();
+ method public float getAngleUp();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class PerceptionLibrary {
+ ctor public PerceptionLibrary();
+ method public androidx.xr.scenecore.impl.perception.Session? getSession();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.scenecore.impl.perception.Session!>? initSession(android.app.Activity, int, java.util.concurrent.ExecutorService);
+ method protected static void loadLibraryAsync(String);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class PerceptionLibraryConstants {
+ field public static final int OPEN_XR_SPACE_TYPE_LOCAL = 2; // 0x2
+ field public static final int OPEN_XR_SPACE_TYPE_LOCAL_FLOOR = 1000426000; // 0x3ba14a10
+ field public static final int OPEN_XR_SPACE_TYPE_STAGE = 3; // 0x3
+ field public static final int OPEN_XR_SPACE_TYPE_UNBOUNDED = 1000467000; // 0x3ba1ea38
+ field public static final int OPEN_XR_SPACE_TYPE_VIEW = 1; // 0x1
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class Plane implements androidx.xr.scenecore.impl.perception.Trackable {
+ ctor public Plane(Long, int);
+ method public androidx.xr.scenecore.impl.perception.Anchor? createAnchor(androidx.xr.scenecore.impl.perception.Pose, Long?);
+ method public java.util.List<androidx.xr.scenecore.impl.perception.Anchor!> getAnchors();
+ method public androidx.xr.scenecore.impl.perception.Plane.PlaneData? getData(Long?);
+ }
+
+ public enum Plane.Label {
+ enum_constant public static final androidx.xr.scenecore.impl.perception.Plane.Label CEILING;
+ enum_constant public static final androidx.xr.scenecore.impl.perception.Plane.Label FLOOR;
+ enum_constant public static final androidx.xr.scenecore.impl.perception.Plane.Label TABLE;
+ enum_constant public static final androidx.xr.scenecore.impl.perception.Plane.Label UNKNOWN;
+ enum_constant public static final androidx.xr.scenecore.impl.perception.Plane.Label WALL;
+ field public final int intValue;
+ }
+
+ public static class Plane.PlaneData {
+ ctor public Plane.PlaneData(androidx.xr.scenecore.impl.perception.Pose, float, float, int, int);
+ field public final androidx.xr.scenecore.impl.perception.Pose centerPose;
+ field public final float extentHeight;
+ field public final float extentWidth;
+ field public final androidx.xr.scenecore.impl.perception.Plane.Label label;
+ field public final androidx.xr.scenecore.impl.perception.Plane.Type type;
+ }
+
+ public enum Plane.Type {
+ enum_constant public static final androidx.xr.scenecore.impl.perception.Plane.Type ARBITRARY;
+ enum_constant public static final androidx.xr.scenecore.impl.perception.Plane.Type HORIZONTAL_DOWNWARD_FACING;
+ enum_constant public static final androidx.xr.scenecore.impl.perception.Plane.Type HORIZONTAL_UPWARD_FACING;
+ enum_constant public static final androidx.xr.scenecore.impl.perception.Plane.Type VERTICAL;
+ field public final int intValue;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class Pose {
+ ctor public Pose(float, float, float, float, float, float, float);
+ method public static androidx.xr.scenecore.impl.perception.Pose identity();
+ method public float qw();
+ method public float qx();
+ method public float qy();
+ method public float qz();
+ method public float tx();
+ method public float ty();
+ method public float tz();
+ method public void updateRotation(float, float, float, float);
+ method public void updateTranslation(float, float, float);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class Session {
+ method public androidx.xr.scenecore.impl.perception.Anchor? createAnchor(float, float, androidx.xr.scenecore.impl.perception.Plane.Type, androidx.xr.scenecore.impl.perception.Plane.Label);
+ method public androidx.xr.scenecore.impl.perception.Anchor? createAnchorFromUuid(java.util.UUID?);
+ method public java.util.List<androidx.xr.scenecore.impl.perception.Plane!> getAllPlanes();
+ method public androidx.xr.scenecore.impl.perception.Pose? getHeadPose();
+ method public long getNativeInstance();
+ method public long getNativeSession();
+ method public androidx.xr.scenecore.impl.perception.ViewProjections? getStereoViews();
+ method public boolean unpersistAnchor(java.util.UUID?);
+ field public static final long XR_NULL_HANDLE = 0L; // 0x0L
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Trackable {
+ method public androidx.xr.scenecore.impl.perception.Anchor? createAnchor(androidx.xr.scenecore.impl.perception.Pose, Long?);
+ method public java.util.List<androidx.xr.scenecore.impl.perception.Anchor!> getAnchors();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class ViewProjection {
+ ctor public ViewProjection(androidx.xr.scenecore.impl.perception.Pose, androidx.xr.scenecore.impl.perception.Fov);
+ method public androidx.xr.scenecore.impl.perception.Fov getFov();
+ method public androidx.xr.scenecore.impl.perception.Pose getPose();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class ViewProjections {
+ ctor public ViewProjections(androidx.xr.scenecore.impl.perception.ViewProjection, androidx.xr.scenecore.impl.perception.ViewProjection);
+ method public androidx.xr.scenecore.impl.perception.ViewProjection getLeftEye();
+ method public androidx.xr.scenecore.impl.perception.ViewProjection getRightEye();
+ }
+
+}
+
+package androidx.xr.scenecore.impl.perception.exceptions {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class FailedToInitializeException extends java.lang.RuntimeException {
+ ctor public FailedToInitializeException(String);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class LibraryLoadingException extends java.lang.RuntimeException {
+ ctor public LibraryLoadingException(String);
+ }
+
+}
+
diff --git a/xr/scenecore/scenecore/build.gradle b/xr/scenecore/scenecore/build.gradle
new file mode 100644
index 0000000..af4f2d3
--- /dev/null
+++ b/xr/scenecore/scenecore/build.gradle
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+
+import androidx.build.LibraryType
+import androidx.build.KotlinTarget
+
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("com.google.protobuf")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ api(project(":xr:arcore:arcore"))
+ api(project(":xr:runtime:runtime"))
+
+ implementation("androidx.annotation:annotation:1.8.1")
+ implementation("androidx.savedstate:savedstate:1.2.1")
+ implementation("androidx.concurrent:concurrent-futures:1.0.0")
+ implementation("androidx.activity:activity:1.9.3")
+ implementation("com.google.errorprone:error_prone_annotations:2.30.0")
+ implementation("androidx.lifecycle:lifecycle-common:2.8.7")
+ implementation("androidx.lifecycle:lifecycle-runtime:2.8.7")
+ implementation(project(":xr:runtime:runtime-openxr"))
+ implementation("com.google.ar:impress:0.0.1")
+ implementation(libs.guavaListenableFuture)
+ implementation(libs.guavaAndroid)
+ implementation(libs.protobufLite)
+
+ testImplementation(libs.junit)
+ testImplementation(libs.kotlinTest)
+ testImplementation(libs.robolectric)
+ testImplementation(libs.testExtJunit)
+ testImplementation(libs.testExtTruth)
+ testImplementation(libs.testRunner)
+ testImplementation(libs.truth)
+ testImplementation(libs.mockitoCore)
+ testImplementation(libs.mockitoKotlin)
+ testImplementation(project(":xr:scenecore:scenecore-testing"))
+ testImplementation(project(":xr:runtime:runtime-testing"))
+
+ // For androidx.xr.extensions.
+ compileOnly(project(':xr:xr-stubs'))
+ testImplementation(project(':xr:xr-stubs'))
+}
+
+android {
+ defaultConfig {
+ consumerProguardFiles 'proguard-rules.pro'
+ // TODO: This should be lower, possibly 21.
+ // Address API calls that require higher versions.
+ minSdkVersion 30
+ }
+ sourceSets {
+ test {
+ assets {
+ srcDirs += ["src/test/java/androidx/xr/scenecore/impl/fake_assets"]
+ }
+ }
+ }
+ namespace "androidx.xr.scenecore"
+}
+
+androidx {
+ name = "XR SceneCore"
+ type = LibraryType.PUBLISHED_LIBRARY
+ inceptionYear = "2024"
+ description = "Scene building libraries for the androidx.xr namespace."
+
+ // TODO: b/379715750 - Remove this flag once the deprecated methods have been removed from the API.
+ failOnDeprecationWarnings = false
+ kotlinTarget = KotlinTarget.KOTLIN_1_9
+ // TODO: b/326456246
+ optOutJSpecify = true
+}
diff --git a/xr/scenecore/scenecore/proguard-rules.pro b/xr/scenecore/scenecore/proguard-rules.pro
new file mode 100644
index 0000000..4593d71
--- /dev/null
+++ b/xr/scenecore/scenecore/proguard-rules.pro
@@ -0,0 +1,14 @@
+# Keep all classes in the extensions and scenecore packages since they can be used externally
+-keep public class androidx.xr.extensions.** { *; }
+-keep public interface androidx.xr.extensions.** { *; }
+-keep @interface androidx.xr.extensions.**
+
+-keep class androidx.xr.scenecore.** { *; }
+-keep class androidx.xr.scenecore.**$* { *; }
+-keep class androidx.xr.scenecore.impl.** { *; }
+-keep class androidx.xr.scenecore.impl.**$* { *; }
+
+-keep class * extends androidx.xr.scenecore.** { *; }
+-keep class * extends androidx.xr.scenecore.**$* { *; }
+-keep class * extends androidx.xr.scenecore.impl.** { *; }
+-keep class * extends androidx.xr.scenecore.impl.**$* { *; }
diff --git a/xr/scenecore/scenecore/src/main/AndroidManifest.xml b/xr/scenecore/scenecore/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..4c0f76a
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<!--
+ Copyright 2024 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
+
+ http://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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <application>
+ <uses-library android:name="com.android.extensions.xr" android:required="false" />
+ </application>
+</manifest>
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/AndroidXrExtensions.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/AndroidXrExtensions.java
new file mode 100644
index 0000000..9d4141a
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/AndroidXrExtensions.java
@@ -0,0 +1,473 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions;
+
+import static java.util.Objects.requireNonNull;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.xr.extensions.asset.EnvironmentToken;
+import androidx.xr.extensions.asset.GltfModelToken;
+import androidx.xr.extensions.asset.SceneToken;
+import androidx.xr.extensions.asset.TokenConverter;
+import androidx.xr.extensions.media.MediaTypeConverter;
+import androidx.xr.extensions.media.XrSpatialAudioExtensions;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.extensions.node.NodeTransaction;
+import androidx.xr.extensions.node.NodeTypeConverter;
+import androidx.xr.extensions.node.ReformEvent;
+import androidx.xr.extensions.node.ReformOptions;
+import androidx.xr.extensions.node.Vec3;
+import androidx.xr.extensions.space.ActivityPanel;
+import androidx.xr.extensions.space.ActivityPanelLaunchParameters;
+import androidx.xr.extensions.space.Bounds;
+import androidx.xr.extensions.space.HitTestResult;
+import androidx.xr.extensions.space.SpaceTypeConverter;
+import androidx.xr.extensions.space.SpatialCapabilities;
+import androidx.xr.extensions.space.SpatialState;
+import androidx.xr.extensions.space.SpatialStateEvent;
+import androidx.xr.extensions.splitengine.SplitEngineBridge;
+import androidx.xr.extensions.splitengine.SplitEngineTypeConverter;
+import androidx.xr.extensions.subspace.Subspace;
+import androidx.xr.extensions.subspace.SubspaceTypeConverter;
+
+import java.io.InputStream;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+
+/**
+ * The main extensions class that creates or provides instances of various XR Extensions components.
+ *
+ * <p>This class wraps the com.android.extensions.xr.XrExtensions class.
+ */
+class AndroidXrExtensions implements XrExtensions {
+ @NonNull private final com.android.extensions.xr.XrExtensions mExtensions;
+
+ AndroidXrExtensions(@NonNull com.android.extensions.xr.XrExtensions extensions) {
+ requireNonNull(extensions);
+ mExtensions = extensions;
+ }
+
+ @Override
+ public int getApiVersion() {
+ return mExtensions.getApiVersion();
+ }
+
+ @Override
+ public @NonNull Node createNode() {
+ return NodeTypeConverter.toLibrary(mExtensions.createNode());
+ }
+
+ @Override
+ public @NonNull NodeTransaction createNodeTransaction() {
+ return NodeTypeConverter.toLibrary(mExtensions.createNodeTransaction());
+ }
+
+ @Override
+ public @NonNull Subspace createSubspace(
+ @NonNull SplitEngineBridge splitEngineBridge, int subspaceId) {
+ com.android.extensions.xr.splitengine.SplitEngineBridge bridge =
+ SplitEngineTypeConverter.toFramework(splitEngineBridge);
+
+ return SubspaceTypeConverter.toLibrary(mExtensions.createSubspace(bridge, subspaceId));
+ }
+
+ @Override
+ @Deprecated
+ public @NonNull CompletableFuture</* @Nullable */ GltfModelToken> loadGltfModel(
+ InputStream asset, int regionSizeBytes, int regionOffsetBytes, String url) {
+ return mExtensions
+ .loadGltfModel(asset, regionSizeBytes, regionOffsetBytes, url)
+ .thenApply(token -> TokenConverter.toLibrary(token));
+ }
+
+ @Override
+ @Deprecated
+ public @NonNull CompletableFuture</* @Nullable */ SceneViewerResult> displayGltfModel(
+ Activity activity, GltfModelToken gltfModel) {
+ return mExtensions
+ .displayGltfModel(activity, TokenConverter.toFramework(gltfModel))
+ .thenApply(result -> (result == null) ? null : new SceneViewerResult());
+ }
+
+ @Override
+ @Deprecated
+ public @NonNull CompletableFuture</* @Nullable */ EnvironmentToken> loadEnvironment(
+ InputStream asset, int regionSizeBytes, int regionOffsetBytes, String url) {
+ // This method has been deprecated on the platform side. Hard code width and height to 256.
+ return loadEnvironment(
+ asset,
+ regionSizeBytes,
+ regionOffsetBytes,
+ url,
+ /*default texture width*/ 256, /*default texture height*/
+ 256);
+ }
+
+ @Override
+ @Deprecated
+ public @NonNull CompletableFuture</* @Nullable */ EnvironmentToken> loadEnvironment(
+ InputStream asset,
+ int regionSizeBytes,
+ int regionOffsetBytes,
+ String url,
+ int textureWidth,
+ int textureHeight) {
+ return mExtensions
+ .loadEnvironment(
+ asset, regionSizeBytes, regionOffsetBytes, url, textureWidth, textureHeight)
+ .thenApply(token -> TokenConverter.toLibrary(token));
+ }
+
+ @Override
+ @Deprecated
+ public @NonNull CompletableFuture</* @Nullable */ SceneToken> loadImpressScene(
+ InputStream asset, int regionSizeBytes, int regionOffsetBytes) {
+ return mExtensions
+ .loadImpressScene(asset, regionSizeBytes, regionOffsetBytes)
+ .thenApply(token -> TokenConverter.toLibrary(token));
+ }
+
+ @Override
+ public @NonNull SplitEngineBridge createSplitEngineBridge() {
+ return SplitEngineTypeConverter.toLibrary(mExtensions.createSplitEngineBridge());
+ }
+
+ @Override
+ public @NonNull XrSpatialAudioExtensions getXrSpatialAudioExtensions() {
+ return MediaTypeConverter.toLibrary(mExtensions.getXrSpatialAudioExtensions());
+ }
+
+ @Override
+ @Deprecated
+ public void attachSpatialScene(
+ @NonNull Activity activity, @NonNull Node sceneNode, @NonNull Node windowNode) {
+ mExtensions.attachSpatialScene(
+ activity,
+ NodeTypeConverter.toFramework(sceneNode),
+ NodeTypeConverter.toFramework(windowNode));
+ }
+
+ @Override
+ public void attachSpatialScene(
+ @NonNull Activity activity,
+ @NonNull Node sceneNode,
+ @NonNull Node windowNode,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor) {
+ mExtensions.attachSpatialScene(
+ activity,
+ NodeTypeConverter.toFramework(sceneNode),
+ NodeTypeConverter.toFramework(windowNode),
+ /* callback= */ (result) -> {
+ callback.accept(new XrExtensionResultImpl(result));
+ },
+ executor);
+ }
+
+ @Override
+ @Deprecated
+ public void detachSpatialScene(@NonNull Activity activity) {
+ mExtensions.detachSpatialScene(activity);
+ }
+
+ @Override
+ public void detachSpatialScene(
+ @NonNull Activity activity,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor) {
+ mExtensions.detachSpatialScene(
+ activity,
+ /* callback= */ (result) -> {
+ callback.accept(new XrExtensionResultImpl(result));
+ },
+ executor);
+ }
+
+ @Override
+ @Deprecated
+ public void setMainWindowSize(@NonNull Activity activity, int width, int height) {
+ mExtensions.setMainWindowSize(activity, width, height);
+ }
+
+ @Override
+ public void setMainWindowSize(
+ @NonNull Activity activity,
+ int width,
+ int height,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor) {
+ mExtensions.setMainWindowSize(
+ activity,
+ width,
+ height,
+ /* callback= */ (result) -> {
+ callback.accept(new XrExtensionResultImpl(result));
+ },
+ executor);
+ }
+
+ @Override
+ @Deprecated
+ public void setMainWindowCurvatureRadius(@NonNull Activity activity, float curvatureRadius) {
+ mExtensions.setMainWindowCurvatureRadius(activity, curvatureRadius);
+ }
+
+ @Override
+ @Deprecated
+ public void attachSpatialEnvironment(
+ @NonNull Activity activity, @NonNull Node environmentNode) {
+ mExtensions.attachSpatialEnvironment(
+ activity, NodeTypeConverter.toFramework(environmentNode));
+ }
+
+ @Override
+ public void attachSpatialEnvironment(
+ @NonNull Activity activity,
+ @NonNull Node environmentNode,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor) {
+ mExtensions.attachSpatialEnvironment(
+ activity,
+ NodeTypeConverter.toFramework(environmentNode),
+ /* callback= */ (result) -> {
+ callback.accept(new XrExtensionResultImpl(result));
+ },
+ executor);
+ }
+
+ @Override
+ @Deprecated
+ public void detachSpatialEnvironment(@NonNull Activity activity) {
+ mExtensions.detachSpatialEnvironment(activity);
+ }
+
+ @Override
+ public void detachSpatialEnvironment(
+ @NonNull Activity activity,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor) {
+ mExtensions.detachSpatialEnvironment(
+ activity,
+ /* callback= */ (result) -> {
+ callback.accept(new XrExtensionResultImpl(result));
+ },
+ executor);
+ }
+
+ @Override
+ @Deprecated
+ public void setSpatialStateCallback(
+ @NonNull Activity activity,
+ @NonNull Consumer<SpatialStateEvent> callback,
+ @NonNull Executor executor) {
+ mExtensions.setSpatialStateCallbackDeprecated(
+ activity,
+ /* callback= */ (event) -> {
+ callback.accept(SpaceTypeConverter.toLibrary(event));
+ },
+ executor);
+ }
+
+ @Override
+ public void registerSpatialStateCallback(
+ @NonNull Activity activity,
+ @NonNull Consumer<SpatialState> callback,
+ @NonNull Executor executor) {
+ mExtensions.setSpatialStateCallback(
+ activity,
+ /* callback= */ (state) -> {
+ callback.accept(SpaceTypeConverter.toLibrary(state));
+ },
+ executor);
+ }
+
+ @Override
+ public void clearSpatialStateCallback(@NonNull Activity activity) {
+ mExtensions.clearSpatialStateCallback(activity);
+ }
+
+ @Override
+ public @NonNull ActivityPanel createActivityPanel(
+ @NonNull Activity host, @NonNull ActivityPanelLaunchParameters launchParameters) {
+ return SpaceTypeConverter.toLibrary(
+ mExtensions.createActivityPanel(
+ host, SpaceTypeConverter.toFramework(launchParameters)));
+ }
+
+ @Override
+ @Deprecated
+ public boolean canEmbedActivityPanel(@NonNull Activity activity) {
+ // TODO(coderleon): update the doc when we support spatial task fragment.
+ return mExtensions.canEmbedActivityPanel(activity);
+ }
+
+ @Override
+ @Deprecated
+ public boolean requestFullSpaceMode(@NonNull Activity activity) {
+ return mExtensions.requestFullSpaceMode(activity);
+ }
+
+ @Override
+ @Deprecated
+ public boolean requestHomeSpaceMode(@NonNull Activity activity) {
+ return mExtensions.requestHomeSpaceMode(activity);
+ }
+
+ @Override
+ public void requestFullSpaceMode(
+ @NonNull Activity activity,
+ boolean requestEnter,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor) {
+ mExtensions.requestFullSpaceMode(
+ activity,
+ requestEnter,
+ /* callback= */ (result) -> {
+ callback.accept(new XrExtensionResultImpl(result));
+ },
+ executor);
+ }
+
+ @Override
+ public @NonNull Bundle setFullSpaceMode(@NonNull Bundle bundle) {
+ return mExtensions.setFullSpaceStartMode(bundle);
+ }
+
+ @Override
+ public @NonNull Bundle setFullSpaceModeWithEnvironmentInherited(@NonNull Bundle bundle) {
+ return mExtensions.setFullSpaceStartModeWithEnvironmentInherited(bundle);
+ }
+
+ @Override
+ @Deprecated
+ public @NonNull Bundle setMainPanelCurvatureRadius(
+ @NonNull Bundle bundle, float panelCurvatureRadius) {
+ return mExtensions.setMainPanelCurvatureRadius(bundle, panelCurvatureRadius);
+ }
+
+ @Override
+ public @NonNull Config getConfig() {
+ return new ConfigImpl(mExtensions.getConfig());
+ }
+
+ @Override
+ public void hitTest(
+ @NonNull Activity activity,
+ @NonNull Vec3 origin,
+ @NonNull Vec3 direction,
+ @NonNull Consumer<HitTestResult> callback,
+ @NonNull Executor executor) {
+ mExtensions.hitTest(
+ activity,
+ NodeTypeConverter.toFramework(origin),
+ NodeTypeConverter.toFramework(direction),
+ /* callback= */ (result) -> {
+ callback.accept(SpaceTypeConverter.toLibrary(result));
+ },
+ executor);
+ }
+
+ @Override
+ public int getOpenXrWorldSpaceType() {
+ return mExtensions.getOpenXrWorldReferenceSpaceType();
+ }
+
+ @Override
+ public @NonNull ReformOptions createReformOptions(
+ @NonNull Consumer<ReformEvent> callback, @NonNull Executor executor) {
+ return NodeTypeConverter.toLibrary(
+ mExtensions.createReformOptions(
+ /* callback= */ (event) ->
+ callback.accept(NodeTypeConverter.toLibrary(event)),
+ executor));
+ }
+
+ @Override
+ public void addFindableView(@NonNull View view, @NonNull ViewGroup group) {
+ mExtensions.addFindableView(view, group);
+ }
+
+ @Override
+ public void removeFindableView(@NonNull View view, @NonNull ViewGroup group) {
+ mExtensions.removeFindableView(view, group);
+ }
+
+ @Override
+ public @Nullable Node getSurfaceTrackingNode(@NonNull View view) {
+ return NodeTypeConverter.toLibrary(mExtensions.getSurfaceTrackingNode(view));
+ }
+
+ @Override
+ @Deprecated
+ public void setPreferredAspectRatio(@NonNull Activity activity, float preferredRatio) {
+ mExtensions.setPreferredAspectRatio(activity, preferredRatio);
+ }
+
+ @Override
+ public void setPreferredAspectRatio(
+ @NonNull Activity activity,
+ float preferredRatio,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor) {
+ mExtensions.setPreferredAspectRatio(
+ activity,
+ preferredRatio,
+ /* callback= */ (result) -> {
+ callback.accept(new XrExtensionResultImpl(result));
+ },
+ executor);
+ }
+
+ @Override
+ @Deprecated
+ public void getSpatialCapabilities(
+ @NonNull Activity activity,
+ @NonNull Consumer<SpatialCapabilities> callback,
+ @NonNull Executor executor) {
+ mExtensions.getSpatialCapabilities(
+ activity,
+ /* callback= */ (capabilities) -> {
+ callback.accept(SpaceTypeConverter.toLibrary(capabilities));
+ },
+ executor);
+ }
+
+ @Override
+ @Deprecated
+ public void getBounds(
+ @NonNull Activity activity,
+ @NonNull Consumer<Bounds> callback,
+ @NonNull Executor executor) {
+ mExtensions.getBounds(
+ activity,
+ /* callback= */ (bounds) -> {
+ callback.accept(SpaceTypeConverter.toLibrary(bounds));
+ },
+ executor);
+ }
+
+ @Override
+ public @NonNull SpatialState getSpatialState(@NonNull Activity activity) {
+ return SpaceTypeConverter.toLibrary(mExtensions.getSpatialState(activity));
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/Config.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/Config.java
new file mode 100644
index 0000000..2ab5571
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/Config.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions;
+
+import androidx.annotation.RestrictTo;
+
+/** XR configuration information. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface Config {
+ /**
+ * Returns the default pixelsPerMeter value for 2D surfaces. See
+ * NodeTransaction.setPixelResolution() for the meaning of pixelsPerMeter.
+ *
+ * @param density The logical density of the display.
+ * @return The default pixelsPerMeter value.
+ */
+ float defaultPixelsPerMeter(float density);
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/ConfigImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/ConfigImpl.java
new file mode 100644
index 0000000..ad32052
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/ConfigImpl.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.NonNull;
+
+class ConfigImpl implements Config {
+ @NonNull private final com.android.extensions.xr.Config mConfig;
+
+ ConfigImpl(@NonNull com.android.extensions.xr.Config config) {
+ requireNonNull(config);
+
+ this.mConfig = config;
+ }
+
+ @Override
+ public float defaultPixelsPerMeter(float density) {
+ return mConfig.defaultPixelsPerMeter(density);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/ExperimentalExtensionApi.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/ExperimentalExtensionApi.java
new file mode 100644
index 0000000..3d64195
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/ExperimentalExtensionApi.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import androidx.annotation.RequiresOptIn;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation denotes an experimental Extensions API.
+ *
+ * <p>There are no guarantees on stability of these APIs; they may change over time. By opting-in,
+ * users of these APIs assume the risk of breakage as these changes occur.
+ */
+@Retention(CLASS)
+@Target({TYPE, METHOD, CONSTRUCTOR, FIELD})
+@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public @interface ExperimentalExtensionApi {}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/IBinderWrapper.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/IBinderWrapper.java
new file mode 100644
index 0000000..f6012ce
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/IBinderWrapper.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions;
+
+import android.os.IBinder;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import java.util.Objects;
+
+/** A wrapper class for {@link IBinder}. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public abstract class IBinderWrapper {
+
+ private final IBinder mToken;
+
+ public IBinderWrapper(@NonNull IBinder token) {
+ mToken = token;
+ }
+
+ @NonNull
+ protected IBinder getRawToken() {
+ return mToken;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof IBinderWrapper)) return false;
+ IBinderWrapper token = (IBinderWrapper) o;
+ return Objects.equals(mToken, token.mToken);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mToken);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "Token{" + "mToken=" + mToken + '}';
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/XrExtensionResult.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/XrExtensionResult.java
new file mode 100644
index 0000000..00a80a6
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/XrExtensionResult.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions;
+
+import static androidx.xr.extensions.XrExtensions.IMAGE_TOO_OLD;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+
+/** Represents a result of an asynchronous XR Extension call. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface XrExtensionResult {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(
+ value = {
+ XR_RESULT_SUCCESS,
+ XR_RESULT_SUCCESS_NOT_VISIBLE,
+ XR_RESULT_IGNORED_ALREADY_APPLIED,
+ XR_RESULT_ERROR_INVALID_STATE,
+ XR_RESULT_ERROR_NOT_ALLOWED,
+ XR_RESULT_ERROR_IGNORED,
+ XR_RESULT_ERROR_SYSTEM,
+ })
+ @Retention(SOURCE)
+ public @interface ResultType {}
+
+ /**
+ * The asynchronous call has been accepted by the system service, and an immediate state change
+ * is expected.
+ */
+ int XR_RESULT_SUCCESS = 0;
+
+ /**
+ * The asynchronous call has been accepted by the system service, but the caller activity's
+ * spatial state won't be changed until other condition(s) are met.
+ */
+ int XR_RESULT_SUCCESS_NOT_VISIBLE = 1;
+
+ /**
+ * The asynchronous call has been ignored by the system service because the caller activity is
+ * already in the requested state.
+ */
+ int XR_RESULT_IGNORED_ALREADY_APPLIED = 2;
+
+ /**
+ * @deprecated Renamed. Use XR_RESULT_IGNORED_ALREADY_APPLIED.
+ */
+ @Deprecated int XR_RESULT_ERROR_INVALID_STATE = 2;
+
+ /**
+ * The asynchronous call has been rejected by the system service because the caller activity
+ * does not have the required capability.
+ */
+ int XR_RESULT_ERROR_NOT_ALLOWED = 3;
+
+ /**
+ * @deprecated Renamed. Use XR_RESULT_ERROR_NOT_ALLOWED.
+ */
+ @Deprecated int XR_RESULT_ERROR_IGNORED = 3;
+
+ /**
+ * The asynchronous call cannot be sent to the system service, or the service cannot properly
+ * handle the request. This is not a recoverable error for the client. For example, this error
+ * is sent to the client when an asynchronous call attempt has failed with a RemoteException.
+ */
+ int XR_RESULT_ERROR_SYSTEM = 4;
+
+ /** Returns the result. */
+ default @ResultType int getResult() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/XrExtensionResultImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/XrExtensionResultImpl.java
new file mode 100644
index 0000000..59ae0bb
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/XrExtensionResultImpl.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions;
+
+import android.util.Log;
+
+class XrExtensionResultImpl implements XrExtensionResult {
+ private static final String TAG = "XrExtensionResultImpl";
+ private final @ResultType int result;
+
+ XrExtensionResultImpl(com.android.extensions.xr.XrExtensionResult result) {
+ switch (result.getResult()) {
+ case com.android.extensions.xr.XrExtensionResult.XR_RESULT_SUCCESS:
+ this.result = XrExtensionResult.XR_RESULT_SUCCESS;
+ break;
+ case com.android.extensions.xr.XrExtensionResult.XR_RESULT_SUCCESS_NOT_VISIBLE:
+ this.result = XrExtensionResult.XR_RESULT_SUCCESS_NOT_VISIBLE;
+ break;
+ case com.android.extensions.xr.XrExtensionResult.XR_RESULT_IGNORED_ALREADY_APPLIED:
+ this.result = XrExtensionResult.XR_RESULT_IGNORED_ALREADY_APPLIED;
+ break;
+ case com.android.extensions.xr.XrExtensionResult.XR_RESULT_ERROR_NOT_ALLOWED:
+ this.result = XrExtensionResult.XR_RESULT_ERROR_NOT_ALLOWED;
+ break;
+ case com.android.extensions.xr.XrExtensionResult.XR_RESULT_ERROR_SYSTEM:
+ this.result = XrExtensionResult.XR_RESULT_ERROR_SYSTEM;
+ break;
+ default:
+ // This path should never be taken.
+ Log.wtf(TAG, "Unknown result: " + result);
+ this.result = XrExtensionResult.XR_RESULT_ERROR_SYSTEM;
+ break;
+ }
+ }
+
+ @Override
+ public @ResultType int getResult() {
+ return result;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/XrExtensions.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/XrExtensions.java
new file mode 100644
index 0000000..1417ff4
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/XrExtensions.java
@@ -0,0 +1,825 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.xr.extensions.asset.EnvironmentToken;
+import androidx.xr.extensions.asset.GltfModelToken;
+import androidx.xr.extensions.asset.SceneToken;
+import androidx.xr.extensions.media.XrSpatialAudioExtensions;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.extensions.node.NodeTransaction;
+import androidx.xr.extensions.node.ReformEvent;
+import androidx.xr.extensions.node.ReformOptions;
+import androidx.xr.extensions.node.Vec3;
+import androidx.xr.extensions.space.ActivityPanel;
+import androidx.xr.extensions.space.ActivityPanelLaunchParameters;
+import androidx.xr.extensions.space.Bounds;
+import androidx.xr.extensions.space.HitTestResult;
+import androidx.xr.extensions.space.SpatialCapabilities;
+import androidx.xr.extensions.space.SpatialState;
+import androidx.xr.extensions.space.SpatialStateEvent;
+import androidx.xr.extensions.splitengine.SplitEngineBridge;
+import androidx.xr.extensions.subspace.Subspace;
+
+import java.io.InputStream;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+
+/**
+ * The main extensions class that creates or provides instances of various XR Extensions components.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface XrExtensions {
+ String IMAGE_TOO_OLD =
+ "This device's system image doesn't include the necessary "
+ + "implementation for this API. Please update to the latest system image. "
+ + "This API requires a corresponding implementation on the device to function "
+ + "correctly.";
+
+ /** Get the current version of the {@link XrExtensions} API. */
+ int getApiVersion();
+
+ /**
+ * Synchronously creates a node that can host a 2D panel or 3D subspace.
+ *
+ * @return A {@link Node}.
+ */
+ @NonNull
+ Node createNode();
+
+ /**
+ * Synchronously creates a new transaction that can be used to update multiple {@link Node}'s
+ * data and transformation in the 3D space.
+ *
+ * @return A {@link NodeTransaction} that can be used to queue the updates and submit to backend
+ * at once.
+ */
+ @NonNull
+ NodeTransaction createNodeTransaction();
+
+ /**
+ * Synchronously creates a subspace.
+ *
+ * @param splitEngineBridge The splitEngineBridge.
+ * @param subspaceId The unique identifier of the subspace.
+ * @return A {@link Subspace} that can be used to render 3D content in.
+ */
+ @NonNull
+ Subspace createSubspace(@NonNull SplitEngineBridge splitEngineBridge, int subspaceId);
+
+ /**
+ * Loads and caches the glTF model in the SpaceFlinger.
+ *
+ * @param asset The input stream data of the glTF model.
+ * @param regionSizeBytes The size of the memory region where the model is stored (in bytes).
+ * @param regionOffsetBytes The offset from the beginning of the memory region (in bytes).
+ * @param url The URL of the asset to be loaded. This string is only used for caching purposes.
+ * @return A {@link CompletableFuture} that either contains the {@link GltfModelToken}
+ * representing the loaded model or 'null' if the asset could not be loaded successfully.
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ @NonNull
+ CompletableFuture</* @Nullable */ GltfModelToken> loadGltfModel(
+ InputStream asset, int regionSizeBytes, int regionOffsetBytes, String url);
+
+ /**
+ * Views a 3D asset.
+ *
+ * @param activity The activity which relinquishes control in order to display the model..
+ * @param gltfModel The model to display.
+ * @return A {@link CompletableFuture} that notifies the caller when the session has completed.
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ @NonNull
+ CompletableFuture</* @Nullable */ SceneViewerResult> displayGltfModel(
+ Activity activity, GltfModelToken gltfModel);
+
+ /**
+ * Loads and caches the environment in the SpaceFlinger.
+ *
+ * @param asset The input stream data of the EXR or JPEG environment.
+ * @param regionSizeBytes The size of the memory region where the environment is stored (in
+ * bytes).
+ * @param regionOffsetBytes The offset from the beginning of the memory region (in bytes).
+ * @param url The URL of the asset to be loaded. This string is only used for caching purposes.
+ * @return A {@link CompletableFuture} that either contains the {@link EnvironmentToken}
+ * representing the loaded environment or 'null' if the asset could not be loaded
+ * successfully.
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ @NonNull
+ CompletableFuture</* @Nullable */ EnvironmentToken> loadEnvironment(
+ InputStream asset, int regionSizeBytes, int regionOffsetBytes, String url);
+
+ /**
+ * Loads and caches the environment in the SpaceFlinger.
+ *
+ * @param asset The input stream data of the EXR or JPEG environment.
+ * @param regionSizeBytes The size of the memory region where the environment is stored (in
+ * bytes).
+ * @param regionOffsetBytes The offset from the beginning of the memory region (in bytes).
+ * @param url The URL of the asset to be loaded.
+ * @param textureWidth The target width of the final texture which will be downsampled/upsampled
+ * from the original image.
+ * @param textureHeight The target height of the final texture which will be
+ * downsampled/upsampled from the original image.
+ * @return A {@link CompletableFuture} that either contains the {@link EnvironmentToken}
+ * representing the loaded environment or 'null' if the asset could not be loaded
+ * successfully.
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ @NonNull
+ CompletableFuture</* @Nullable */ EnvironmentToken> loadEnvironment(
+ InputStream asset,
+ int regionSizeBytes,
+ int regionOffsetBytes,
+ String url,
+ int textureWidth,
+ int textureHeight);
+
+ /**
+ * Loads and caches the Impress scene in the SpaceFlinger.
+ *
+ * @param asset The input stream data of the textproto Impress scene.
+ * @param regionSizeBytes The size of the memory region where the Impress scene is stored (in
+ * bytes).
+ * @param regionOffsetBytes The offset from the beginning of the memory region (in bytes).
+ * @return A {@link CompletableFuture} that either contains the {@link SceneToken} representing
+ * the loaded Impress scene or 'null' if the asset could not be loaded successfully.
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ @NonNull
+ CompletableFuture</* @Nullable */ SceneToken> loadImpressScene(
+ InputStream asset, int regionSizeBytes, int regionOffsetBytes);
+
+ /**
+ * Synchronously returns a {@link SplitEngineBridge}.
+ *
+ * @return A {@link SplitEngineBridge}.
+ */
+ @NonNull
+ SplitEngineBridge createSplitEngineBridge();
+
+ /**
+ * Synchronously returns the implementation of the {@link XrSpatialAudioExtensions} component.
+ *
+ * @return The {@link XrSpatialAudioExtensions}.
+ */
+ @NonNull
+ XrSpatialAudioExtensions getXrSpatialAudioExtensions();
+
+ /**
+ * Attaches the given {@code sceneNode} as the presentation for the given {@code activity} in
+ * the space, and asks the system to attach the 2D content of the {@code activity} into the
+ * given {@code windowNode}.
+ *
+ * <p>The {@code sceneNode} will only be visible if the {@code activity} is visible as in a
+ * lifecycle state between {@link Activity#onStart()} and {@link Activity#onStop()} and is
+ * SPATIAL_UI_CAPABLE too.
+ *
+ * <p>One activity can only attach one scene node. When a new scene node is attached for the
+ * same {@code activity}, the previous one will be detached.
+ *
+ * @param activity the owner activity of the {@code sceneNode}.
+ * @param sceneNode the node to show as the presentation of the {@code activity}.
+ * @param windowNode a leash node to allow the app to control the position and size of the
+ * activity's main window.
+ * @deprecated Use the new interface with a callback.
+ */
+ @Deprecated
+ void attachSpatialScene(
+ @NonNull Activity activity, @NonNull Node sceneNode, @NonNull Node windowNode);
+
+ /**
+ * Attaches the given {@code sceneNode} as the presentation for the given {@code activity} in
+ * the space, and asks the system to attach the 2D content of the {@code activity} into the
+ * given {@code windowNode}.
+ *
+ * <p>The {@code sceneNode} will only be visible if the {@code activity} is visible as in a
+ * lifecycle state between {@link Activity#onStart()} and {@link Activity#onStop()} and is
+ * SPATIAL_UI_CAPABLE too.
+ *
+ * <p>One activity can only attach one scene node. When a new scene node is attached for the
+ * same {@code activity}, the previous one will be detached.
+ *
+ * @param activity the owner activity of the {@code sceneNode}.
+ * @param sceneNode the node to show as the presentation of the {@code activity}.
+ * @param windowNode a leash node to allow the app to control the position and size of the
+ * activity's main window.
+ * @param callback the callback that will be called with the result. XrExtensionResult will
+ * indicate either of the following: XrExtensionResult.XR_RESULT_SUCCESS: The request has
+ * been accepted, and the client can expect that a spatial state callback with an updated
+ * SpatialState will run shortly. XrExtensionResult.XR_RESULT_SUCCESS_NOT_VISIBLE: The
+ * request has been accepted, but will not immediately change the spatial state. A spatial
+ * state callback with an updated SpatialState won't run until the activity becomes
+ * SPATIAL_UI_CAPABLE. XrExtensionResult.XR_RESULT_IGNORED_ALREADY_APPLIED: The request has
+ * been ignored because the activity is already in the requested state.
+ * XrExtensionResult.XR_RESULT_ERROR_NOT_ALLOWED: The request has been rejected because the
+ * activity does not have the required capability (e.g. called by an embedded guest
+ * activity.) XrExtensionResult.XR_RESULT_ERROR_SYSTEM: A unrecoverable service side error
+ * has happened.
+ * @param executor the executor the callback will be called on.
+ */
+ void attachSpatialScene(
+ @NonNull Activity activity,
+ @NonNull Node sceneNode,
+ @NonNull Node windowNode,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor);
+
+ /**
+ * Detaches the {@code sceneNode} that was previously attached for the {@code activity} via
+ * {@link #attachSpatialScene}.
+ *
+ * <p>When an {@link Activity} is destroyed, it must call this method to detach the scene node
+ * that was attached for itself.
+ *
+ * @param activity the owner activity of the {@code sceneNode}.
+ * @deprecated Use the new interface with a callback.
+ */
+ @Deprecated
+ void detachSpatialScene(@NonNull Activity activity);
+
+ /**
+ * Detaches the {@code sceneNode} that was previously attached for the {@code activity} via
+ * {@link #attachSpatialScene}.
+ *
+ * <p>When an {@link Activity} is destroyed, it must call this method to detach the scene node
+ * that was attached for itself.
+ *
+ * @param activity the owner activity of the {@code sceneNode}.
+ * @param callback the callback that will be called with the result. XrExtensionResult will
+ * indicate either of the following: XrExtensionResult.XR_RESULT_SUCCESS: The request has
+ * been accepted, and the client can expect that a spatial state callback with an updated
+ * SpatialState will run shortly. XrExtensionResult.XR_RESULT_SUCCESS_NOT_VISIBLE: The
+ * request has been accepted, but will not immediately change the spatial state. A spatial
+ * state callback with an updated SpatialState won't run until the activity becomes
+ * SPATIAL_UI_CAPABLE. XrExtensionResult.XR_RESULT_IGNORED_ALREADY_APPLIED: The request has
+ * been ignored because the activity is already in the requested state.
+ * XrExtensionResult.XR_RESULT_ERROR_SYSTEM: A unrecoverable service side error has
+ * happened.
+ * @param executor the executor the callback will be called on.
+ */
+ void detachSpatialScene(
+ @NonNull Activity activity,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor);
+
+ /**
+ * Resizes the main window of the given activity to the requested size.
+ *
+ * @param activity the activity whose main window should be resized.
+ * @param width the new main window width in pixels.
+ * @param height the new main window height in pixels.
+ * @deprecated Use the new interface with a callback.
+ */
+ @Deprecated
+ void setMainWindowSize(@NonNull Activity activity, int width, int height);
+
+ /**
+ * Resizes the main window of the given activity to the requested size.
+ *
+ * @param activity the activity whose main window should be resized.
+ * @param width the new main window width in pixels.
+ * @param height the new main window height in pixels.
+ * @param callback the callback that will be called with the result. XrExtensionResult will
+ * indicate either of the following: XrExtensionResult.XR_RESULT_SUCCESS: The request has
+ * been accepted, and the client can expect that a spatial state callback with an updated
+ * SpatialState will run shortly. XrExtensionResult.XR_RESULT_SUCCESS_NOT_VISIBLE: The
+ * request has been accepted, but will not immediately change the spatial state. A spatial
+ * state callback with an updated SpatialState won't run until the activity becomes
+ * SPATIAL_UI_CAPABLE. XrExtensionResult.XR_RESULT_IGNORED_ALREADY_APPLIED: The request has
+ * been ignored because the activity is already in the requested state.
+ * XrExtensionResult.XR_RESULT_ERROR_NOT_ALLOWED: The request has been rejected because the
+ * activity does not have the required capability (e.g. called by an embedded guest
+ * activity.) XrExtensionResult.XR_RESULT_ERROR_SYSTEM: A unrecoverable service side error
+ * has happened.
+ * @param executor the executor the callback will be called on.
+ */
+ void setMainWindowSize(
+ @NonNull Activity activity,
+ int width,
+ int height,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor);
+
+ /**
+ * Sets the main window of the given activity to the curvature radius. Note that it's allowed
+ * only for the activity in full space mode.
+ *
+ * @param activity the activity of the main window to which the curvature should be applied.
+ * @param curvatureRadius the panel curvature radius. It is measured in "radius * 1 /
+ * curvature". A value of 0.0f means that the panel will be flat.
+ * @deprecated Use Split Engine to create a curved panel.
+ */
+ @Deprecated
+ void setMainWindowCurvatureRadius(@NonNull Activity activity, float curvatureRadius);
+
+ /**
+ * Attaches an environment node for a given activity to make it visible.
+ *
+ * <p>SysUI will attach the environment node to the task node when the activity gains the
+ * APP_ENVIRONMENTS_CAPABLE capability.
+ *
+ * <p>This method can be called multiple times, SysUI will attach the new environment node and
+ * detach the old environment node if it exists.
+ *
+ * @param activity the activity that provides the environment node to attach.
+ * @param environmentNode the environment node provided by the activity to be attached.
+ * @deprecated Use the new interface with a callback.
+ */
+ @Deprecated
+ void attachSpatialEnvironment(@NonNull Activity activity, @NonNull Node environmentNode);
+
+ /**
+ * Attaches an environment node for a given activity to make it visible.
+ *
+ * <p>SysUI will attach the environment node to the task node when the activity gains the
+ * APP_ENVIRONMENTS_CAPABLE capability.
+ *
+ * <p>This method can be called multiple times, SysUI will attach the new environment node and
+ * detach the old environment node if it exists.
+ *
+ * <p>Note that once an environmentNode is attached and the caller gains
+ * APP_ENVIRONMENTS_CAPABLE capability, spatial callback's environment visibility status changes
+ * to APP_VISIBLE even if your application hasn't set a skybox or geometry to the environment
+ * node yet. For that reason, call this API only when your application wants to show a skybox or
+ * geometry. Otherwise, the APP_VISIBLE spatial state may lead to an unexpected behavior. For
+ * example, home environment's ambient audio (if any) may stop even if the user can still see
+ * the home environment.
+ *
+ * @param activity the activity that provides the environment node to attach.
+ * @param environmentNode the environment node provided by the activity to be attached.
+ * @param callback the callback that will be called with the result. XrExtensionResult will
+ * indicate either of the following: XrExtensionResult.XR_RESULT_SUCCESS: The request has
+ * been accepted, and the client can expect that a spatial state callback with an updated
+ * SpatialState will run shortly. XrExtensionResult.XR_RESULT_SUCCESS_NOT_VISIBLE: The
+ * request has been accepted, but will not immediately change the spatial state. A spatial
+ * state callback with an updated SpatialState won't run until the activity becomes
+ * APP_ENVIRONMENTS_CAPABLE. XrExtensionResult.XR_RESULT_IGNORED_ALREADY_APPLIED: The
+ * request has been ignored because the activity is already in the requested state.
+ * XrExtensionResult.XR_RESULT_ERROR_NOT_ALLOWED: The request has been rejected because the
+ * activity does not have the required capability (e.g. called by an embedded guest
+ * activity.) XrExtensionResult.XR_RESULT_ERROR_SYSTEM: A unrecoverable service side error
+ * has happened.
+ * @param executor the executor the callback will be called on.
+ */
+ void attachSpatialEnvironment(
+ @NonNull Activity activity,
+ @NonNull Node environmentNode,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor);
+
+ /**
+ * Detaches the environment node and its sub tree for a given activity to make it invisible.
+ *
+ * <p>This method will detach and cleanup the environment node and its subtree passed from the
+ * activity.
+ *
+ * @param activity the activity with which SysUI will detach and clean up the environment node
+ * tree.
+ * @deprecated Use the new interface with a callback.
+ */
+ @Deprecated
+ void detachSpatialEnvironment(@NonNull Activity activity);
+
+ /**
+ * Detaches the environment node and its sub tree for a given activity to make it invisible.
+ *
+ * <p>This method will detach and cleanup the environment node and its subtree passed from the
+ * activity.
+ *
+ * @param activity the activity with which SysUI will detach and clean up the environment node
+ * tree.
+ * @param callback the callback that will be called with the result. XrExtensionResult will
+ * indicate either of the following: XrExtensionResult.XR_RESULT_SUCCESS: The request has
+ * been accepted, and the client can expect that a spatial state callback with an updated
+ * SpatialState will run shortly. XrExtensionResult.XR_RESULT_SUCCESS_NOT_VISIBLE: The
+ * request has been accepted, but will not immediately change the spatial state. A spatial
+ * state callback with an updated SpatialState won't run until the activity becomes
+ * APP_ENVIRONMENTS_CAPABLE. XrExtensionResult.XR_RESULT_IGNORED_ALREADY_APPLIED: The
+ * request has been ignored because the activity is already in the requested state.
+ * XrExtensionResult.XR_RESULT_ERROR_SYSTEM: A unrecoverable service side error has
+ * happened.
+ * @param executor the executor the callback will be called on.
+ */
+ void detachSpatialEnvironment(
+ @NonNull Activity activity,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor);
+
+ /**
+ * Sets a callback to receive {@link SpatialStateEvent} for the given {@code activity}.
+ *
+ * <p>One activity can only set one callback. When a new callback is set for the same {@code
+ * activity}, the previous one will be cleared.
+ *
+ * <p>The callback will be triggered immediately with the current state when it is set, for each
+ * of the possible events.
+ *
+ * @param activity the activity for the {@code callback} to listen to.
+ * @param callback the callback to set.
+ * @param executor the executor that the callback will be called on.
+ * @see #clearSpatialStateCallback
+ * @deprecated Use registerSpatialStateCallback instead.
+ */
+ @Deprecated
+ void setSpatialStateCallback(
+ @NonNull Activity activity,
+ @NonNull Consumer<SpatialStateEvent> callback,
+ @NonNull Executor executor);
+
+ /**
+ * Synchronously registers a callback to receive {@link SpatialState} for the {@code activity}.
+ *
+ * <p>One activity can only set one callback. When a new callback is set for the same {@code
+ * activity}, the previous one will be cleared.
+ *
+ * <p>The {@code executor}'s execute() method will soon be called to run the callback with the
+ * current state when it is available, but it never happens directly from within this call.
+ *
+ * <p>This API throws IllegalArgumentException if it is called by an embedded (guest) activity.
+ *
+ * @param activity the activity for the {@code callback} to listen to.
+ * @param callback the callback to set.
+ * @param executor the executor that the callback will be called on.
+ * @see #clearSpatialStateCallback
+ */
+ void registerSpatialStateCallback(
+ @NonNull Activity activity,
+ @NonNull Consumer<SpatialState> callback,
+ @NonNull Executor executor);
+
+ /**
+ * Synchronously clears the {@link SpatialStateEvent} callback that was previously set to the
+ * {@code activity} via {@link #setSpatialStateCallback}.
+ *
+ * <p>When an {@link Activity} is destroyed, it must call this method to clear the callback that
+ * was set for itself.
+ *
+ * @param activity the activity for the {@code callback} to listen to.
+ */
+ void clearSpatialStateCallback(@NonNull Activity activity);
+
+ /**
+ * Synchronously creates an {@link ActivityPanel} to be embedded inside the given {@code host}
+ * activity.
+ *
+ * <p>Caller must make sure the {@code host} can embed {@link ActivityPanel}. See {@link
+ * getSpatialState()}. When embedding is possible, SpatialState's {@link SpatialCapabilities}
+ * has {@code SPATIAL_ACTIVITY_EMBEDDING_CAPABLE}.
+ *
+ * <p>For the {@link ActivityPanel} to be shown in the scene, caller needs to attach the {@link
+ * ActivityPanel#getNode()} to the scene node attached through {@link #attachSpatialScene}.
+ *
+ * <p>This API throws IllegalArgumentException if it is called by an embedded (guest) activity.
+ *
+ * @param host the host activity to embed the {@link ActivityPanel}.
+ * @param launchParameters the parameters to define the initial state of the {@link
+ * ActivityPanel}.
+ * @return the {@link ActivityPanel} created.
+ * @throws IllegalStateException if the {@code host} is not allowed to embed {@link
+ * ActivityPanel}.
+ */
+ @NonNull
+ ActivityPanel createActivityPanel(
+ @NonNull Activity host, @NonNull ActivityPanelLaunchParameters launchParameters);
+
+ /**
+ * Synchronously checks if an activity can be the host to embed an {@link ActivityPanel}.
+ *
+ * <p>Activity inside an {@link ActivityPanel} cannot be the host.
+ *
+ * @param activity the activity to check.
+ * @see #createActivityPanel
+ * @return true if the embedding is allowed.
+ * @deprecated Use {@link getSpatialState()} instead. When embedding is possible, SpatialState's
+ * {@link SpatialCapabilities} has {@code SPATIAL_ACTIVITY_EMBEDDING_CAPABLE}.
+ */
+ @Deprecated
+ boolean canEmbedActivityPanel(@NonNull Activity activity);
+
+ /**
+ * Requests to put an activity in full space mode when it has focus.
+ *
+ * @param activity the activity that requires to enter full space mode.
+ * @return true when the request was sent (when the activity has focus).
+ * @deprecated Use requestFullSpaceMode with 3 arguments.
+ */
+ @Deprecated
+ boolean requestFullSpaceMode(@NonNull Activity activity);
+
+ /**
+ * Requests to put an activity in home space mode when it has focus.
+ *
+ * @param activity the activity that requires to enter home space mode.
+ * @return true when the request was sent (when the activity has focus).
+ * @deprecated Use requestFullSpaceMode with 3 arguments.
+ */
+ @Deprecated
+ boolean requestHomeSpaceMode(@NonNull Activity activity);
+
+ /**
+ * Requests to put an activity in a different mode when it has focus.
+ *
+ * @param activity the activity that requires to enter full space mode.
+ * @param requestEnter when true, activity is put in full space mode. Home space mode otherwise.
+ * @param callback the callback that will be called with the result. XrExtensionResult will
+ * indicate either of the following: XrExtensionResult.XR_RESULT_SUCCESS: The request has
+ * been accepted, and the client can expect that a spatial state callback with an updated
+ * SpatialState will run shortly. XrExtensionResult.XR_RESULT_IGNORED_ALREADY_APPLIED: The
+ * request has been ignored because the activity is already in the requested mode.
+ * XrExtensionResult.XR_RESULT_ERROR_NOT_ALLOWED: The request has been rejected because the
+ * activity does not have the required capability (e.g. not the top activity in a top task
+ * in the desktop, called by an embedded guest activity.)
+ * XrExtensionResult.XR_RESULT_ERROR_SYSTEM: A unrecoverable service side error has
+ * happened.
+ * @param executor the executor the callback will be called on.
+ */
+ void requestFullSpaceMode(
+ @NonNull Activity activity,
+ boolean requestEnter,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor);
+
+ /**
+ * Synchronously sets the full space mode flag to the given {@link Bundle}.
+ *
+ * <p>The {@link Bundle} then could be used to launch an {@link Activity} with requesting to
+ * enter full space mode through {@link Activity#startActivity}. If there's a bundle used for
+ * customizing how the {@link Activity} should be started by {@link ActivityOptions.toBundle} or
+ * {@link androidx.core.app.ActivityOptionsCompat.toBundle}, it's suggested to use the bundle to
+ * call this method.
+ *
+ * <p>The flag will be ignored when no {@link Intent.FLAG_ACTIVITY_NEW_TASK} is set in the
+ * bundle, or it is not started from a focused Activity context.
+ *
+ * <p>This flag is also ignored when the {@link android.window.PROPERTY_XR_ACTIVITY_START_MODE}
+ * property is set to a value other than XR_ACTIVITY_START_MODE_UNDEFINED in the
+ * AndroidManifest.xml file for the activity being launched.
+ *
+ * @param bundle the input bundle to set with the full space mode flag.
+ * @return the input {@code bundle} with the full space mode flag set.
+ */
+ @NonNull
+ Bundle setFullSpaceMode(@NonNull Bundle bundle);
+
+ /**
+ * Synchronously sets the inherit full space mode environvment flag to the given {@link Bundle}.
+ *
+ * <p>The {@link Bundle} then could be used to launch an {@link Activity} with requesting to
+ * enter full space mode while inherit the existing environment through {@link
+ * Activity#startActivity}. If there's a bundle used for customizing how the {@link Activity}
+ * should be started by {@link ActivityOptions.toBundle} or {@link
+ * androidx.core.app.ActivityOptionsCompat.toBundle}, it's suggested to use the bundle to call
+ * this method.
+ *
+ * <p>When launched, the activity will be in full space mode and also inherits the environment
+ * from the launching activity. If the inherited environment needs to be animated, the launching
+ * activity has to continue updating the environment even after the activity is put into the
+ * stopped state.
+ *
+ * <p>The flag will be ignored when no {@link Intent.FLAG_ACTIVITY_NEW_TASK} is set in the
+ * intent, or it is not started from a focused Activity context.
+ *
+ * <p>The flag will also be ignored when there is no environment to inherit or the activity has
+ * its own environment set already.
+ *
+ * <p>This flag is ignored too when the {@link android.window.PROPERTY_XR_ACTIVITY_START_MODE}
+ * property is set to a value other than XR_ACTIVITY_START_MODE_UNDEFINED in the
+ * AndroidManifest.xml file for the activity being launched.
+ *
+ * <p>For security reasons, Z testing for the new activity is disabled, and the activity is
+ * always drawn on top of the inherited environment. Because Z testing is disabled, the activity
+ * should not spatialize itself, and should not curve its panel too much either.
+ *
+ * @param bundle the input bundle to set with the inherit full space mode environment flag.
+ * @return the input {@code bundle} with the inherit full space mode flag set.
+ */
+ @NonNull
+ Bundle setFullSpaceModeWithEnvironmentInherited(@NonNull Bundle bundle);
+
+ /**
+ * Sets panel curvature radius to the given {@link Bundle}.
+ *
+ * <p>The {@link Bundle} then could be used to launch an {@link Activity} with requesting to a
+ * custom curvature radius for the main panel through {@link Activity#startActivity}. If there's
+ * a bundle used for customizing how the {@link Activity} should be started by {@link
+ * ActivityOptions.toBundle} or {@link androidx.core.app.ActivityOptionsCompat.toBundle}, it's
+ * suggested to use the bundle to call this method.
+ *
+ * <p>The curvature radius must be used together with {@link
+ * #setFullSpaceModeWithEnvironmentInherited(Bundle)}. Otherwise, it will be ignored.
+ *
+ * @param bundle the input bundle to set with the inherit full space mode environment flag.
+ * @param panelCurvatureRadius the panel curvature radius. It is measured in "radius * 1 /
+ * curvature". A value of 0.0f means the panel is flat.
+ * @return the input {@code bundle} with the inherit full space mode flag set.
+ * @deprecated Use Split Engine to create a curved panel.
+ */
+ @Deprecated
+ @NonNull
+ Bundle setMainPanelCurvatureRadius(@NonNull Bundle bundle, float panelCurvatureRadius);
+
+ /**
+ * Synchronously returns system config information.
+ *
+ * @return A {@link Config} object.
+ */
+ @NonNull
+ Config getConfig();
+
+ /**
+ * Hit-tests a ray against the virtual scene. If the ray hits an object in the scene,
+ * information about the hit will be passed to the callback. If nothing is hit, the hit distance
+ * will be infinite. Note that attachSpatialScene() must be called before calling this method.
+ * Otherwise, an IllegalArgumentException is thrown.
+ *
+ * @param activity the requesting activity.
+ * @param origin the origin of the ray to test, in the activity's task coordinates.
+ * @param direction the direction of the ray to test, in the activity's task coordinates.
+ * @param callback the callback that will be called with the hit test result.
+ * @param executor the executor the callback will be called on.
+ */
+ void hitTest(
+ @NonNull Activity activity,
+ @NonNull Vec3 origin,
+ @NonNull Vec3 direction,
+ @NonNull Consumer<HitTestResult> callback,
+ @NonNull Executor executor);
+
+ /**
+ * Synchronously returns the OpenXR reference space type.
+ *
+ * @return the OpenXR reference space type used as world space for the shared scene.
+ */
+ int getOpenXrWorldSpaceType();
+
+ /**
+ * Synchronously creates a new ReformOptions instance.
+ *
+ * @param callback the callback that will be called with reform events.
+ * @param executor the executor the callback will be called on.
+ * @return the new builder instance.
+ */
+ @NonNull
+ ReformOptions createReformOptions(
+ @NonNull Consumer<ReformEvent> callback, @NonNull Executor executor);
+
+ /**
+ * Synchronously makes a View findable via findViewById().
+ *
+ * <p>This is done without it being a child of the given group.
+ *
+ * @param view the view to add as findable.
+ * @param group a group that is part of the hierarchy that findViewById() will be called on.
+ */
+ void addFindableView(@NonNull View view, @NonNull ViewGroup group);
+
+ /**
+ * Synchronously removes a findable view from the given group.
+ *
+ * @param view the view to remove as findable.
+ * @param group the group to remove the findable view from.
+ */
+ void removeFindableView(@NonNull View view, @NonNull ViewGroup group);
+
+ /**
+ * Returns the surface tracking node for a view, if there is one.
+ *
+ * <p>The surface tracking node is centered on the Surface that the view is attached to, and is
+ * sized to match the surface's size. Note that the view's position in the surface can be
+ * retrieved via View.getLocationInSurface().
+ *
+ * @param view the view.
+ * @return the surface tracking node, or null if no such node exists.
+ */
+ @Nullable
+ Node getSurfaceTrackingNode(@NonNull View view);
+
+ /**
+ * Sets a preferred main panel aspect ratio for home space mode.
+ *
+ * <p>The ratio is only applied to the activity. If the activity launches another activity in
+ * the same task, the ratio is not applied to the new activity. Also, while the activity is in
+ * full space mode, the preference is temporarily removed.
+ *
+ * @param activity the activity to set the preference.
+ * @param preferredRatio the aspect ratio determined by taking the panel's width over its
+ * height. A value <= 0.0f means there are no preferences.
+ * @deprecated Use the new interface with a callback.
+ */
+ @Deprecated
+ void setPreferredAspectRatio(@NonNull Activity activity, float preferredRatio);
+
+ /**
+ * Sets a preferred main panel aspect ratio for an activity that is not SPATIAL_UI_CAPABLE.
+ *
+ * <p>The ratio is only applied to the activity. If the activity launches another activity in
+ * the same task, the ratio is not applied to the new activity. Also, while the activity is
+ * SPATIAL_UI_CAPABLE, the preference is temporarily removed. While the activity is
+ * SPATIAL_UI_CAPABLE, use ReformOptions API instead.
+ *
+ * @param activity the activity to set the preference.
+ * @param preferredRatio the aspect ratio determined by taking the panel's width over its
+ * height. A value <= 0.0f means there are no preferences.
+ * @param callback the callback that will be called with the result. XrExtensionResult will
+ * indicate either of the following: XrExtensionResult.XR_RESULT_SUCCESS: The request has
+ * been accepted, and the client can expect that a spatial state callback with an updated
+ * SpatialState will run shortly. XrExtensionResult.XR_RESULT_SUCCESS_NOT_VISIBLE: The
+ * request has been accepted, but will not immediately change the spatial state. A spatial
+ * state callback with an updated SpatialState won't run until the activity loses the
+ * SPATIAL_UI_CAPABLE capability. XrExtensionResult.XR_RESULT_IGNORED_ALREADY_APPLIED: The
+ * request has been ignored because the activity is already in the requested state.
+ * XrExtensionResult.XR_RESULT_ERROR_NOT_ALLOWED: The request has been rejected because the
+ * activity does not have the required capability (e.g. called by an embedded guest
+ * activity.) XrExtensionResult.XR_RESULT_ERROR_SYSTEM: A unrecoverable service side error
+ * has happened.
+ * @param executor the executor the callback will be called on.
+ */
+ void setPreferredAspectRatio(
+ @NonNull Activity activity,
+ float preferredRatio,
+ @NonNull Consumer<XrExtensionResult> callback,
+ @NonNull Executor executor);
+
+ /**
+ * Gets the spatial capabilities of the activity.
+ *
+ * @param activity the activity to get the capabilities.
+ * @param callback the callback to run. If the activity is not found in SysUI, the callback runs
+ * with a null SpatialCapabilities.
+ * @param executor the executor that the callback will be called on.
+ * @deprecated Use getSpatialState synchronous getter.
+ */
+ @Deprecated
+ void getSpatialCapabilities(
+ @NonNull Activity activity,
+ @NonNull Consumer<SpatialCapabilities> callback,
+ @NonNull Executor executor);
+
+ /**
+ * Gets the bounds of the activity.
+ *
+ * @param activity the activity to get the bounds.
+ * @param callback the callback to run. If the activity is not found in SysUI, the callback runs
+ * with a null Bounds.
+ * @param executor the executor that the callback will be called on.
+ * @deprecated Use getSpatialState synchronous getter.
+ */
+ @Deprecated
+ void getBounds(
+ @NonNull Activity activity,
+ @NonNull Consumer<Bounds> callback,
+ @NonNull Executor executor);
+
+ /**
+ * Synchronously gets the spatial state of the activity.
+ *
+ * <p>Do not call the API from the Binder thread. That may cause a deadlock.
+ *
+ * <p>This API throws IllegalArgumentException if it is called by an embedded (guest) activity,
+ * and also throws RuntimeException if the calling thread is interrupted.
+ *
+ * @param activity the activity to get the capabilities.
+ * @return the state of the activity.
+ */
+ @NonNull
+ SpatialState getSpatialState(@NonNull Activity activity);
+
+ /**
+ * The result of a displayGltfModel request.
+ *
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ class SceneViewerResult {}
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/XrExtensionsProvider.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/XrExtensionsProvider.java
new file mode 100644
index 0000000..d2580f1
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/XrExtensionsProvider.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+/** Provides the OEM implementation of {@link XrExtensions}. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class XrExtensionsProvider {
+ private XrExtensionsProvider() {}
+
+ /**
+ * Returns the OEM implementation of {@link XrExtensions}.
+ *
+ * @return The OEM implementation of {@link XrExtensions} or throws an exception if no
+ * implementation is found.
+ */
+ @Nullable
+ public static XrExtensions getXrExtensions() {
+ try {
+ return XrExtensionsInstance.getInstance();
+ } catch (NoClassDefFoundError e) {
+ return null;
+ }
+ }
+
+ private static class XrExtensionsInstance {
+ private XrExtensionsInstance() {}
+
+ private static class XrExtensionsHolder {
+ public static final XrExtensions INSTANCE =
+ new AndroidXrExtensions(new com.android.extensions.xr.XrExtensions());
+ }
+
+ public static XrExtensions getInstance() {
+ return XrExtensionsHolder.INSTANCE;
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/AssetToken.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/AssetToken.java
new file mode 100644
index 0000000..20546b1
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/AssetToken.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.asset;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Token of a spatial asset cached in the SpaceFlinger.
+ *
+ * <p>A Node can reference an {@link AssetToken} such that the SpaceFlinger will render the asset
+ * inside it.
+ *
+ * <p>Note that the app needs to keep track of the {@link AssetToken} so that it can continue using
+ * it, and eventually, free it when it is no longer needed.
+ *
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+@Deprecated
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface AssetToken {}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/AssetTokenImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/AssetTokenImpl.java
new file mode 100644
index 0000000..ed23553
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/AssetTokenImpl.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.asset;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.NonNull;
+
+@Deprecated
+class AssetTokenImpl implements AssetToken {
+ @NonNull final com.android.extensions.xr.asset.AssetToken mToken;
+
+ AssetTokenImpl(@NonNull com.android.extensions.xr.asset.AssetToken token) {
+ requireNonNull(token);
+ this.mToken = token;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/EnvironmentToken.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/EnvironmentToken.java
new file mode 100644
index 0000000..8efd689
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/EnvironmentToken.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.asset;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Token of an environment cached in the SpaceFlinger.
+ *
+ * <p>A Node can reference an {@link EnvironmentToken} such that the SpaceFlinger will render the
+ * asset inside it.
+ *
+ * <p>Note that the app needs to keep track of the {@link EnvironmentToken} so that it can continue
+ * using it, and eventually, free it when it is no longer needed.
+ *
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+@Deprecated
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface EnvironmentToken extends AssetToken {}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/EnvironmentTokenImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/EnvironmentTokenImpl.java
new file mode 100644
index 0000000..3d41c4d
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/EnvironmentTokenImpl.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.asset;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.NonNull;
+
+@Deprecated
+class EnvironmentTokenImpl implements EnvironmentToken {
+ @NonNull final com.android.extensions.xr.asset.EnvironmentToken mToken;
+
+ EnvironmentTokenImpl(@NonNull com.android.extensions.xr.asset.EnvironmentToken token) {
+ requireNonNull(token);
+ mToken = token;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/GltfAnimation.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/GltfAnimation.java
new file mode 100644
index 0000000..a472c68
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/GltfAnimation.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.asset;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Animation configuration to be played on a glTF model.
+ *
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+@Deprecated
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface GltfAnimation {
+ /** State of a glTF animation. */
+ enum State {
+ /** Will stop the glTF animation that is currently playing or looping. */
+ STOP,
+ /** Will restart the glTF animation if it's currently playing, looping, or is stopped. */
+ PLAY,
+ /**
+ * Will restart and loop the glTF animation if it's currently playing, looping, or is
+ * stopped.
+ */
+ LOOP
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/GltfModelToken.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/GltfModelToken.java
new file mode 100644
index 0000000..1f2081e
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/GltfModelToken.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.asset;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Token of a glTF model cached in the SpaceFlinger.
+ *
+ * <p>A Node can reference an {@link GltfModelToken} such that the SpaceFlinger will render the
+ * asset inside it.
+ *
+ * <p>Note that the app needs to keep track of the {@link GltfModelToken} so that it can continue
+ * using it, and eventually, free it when it is no longer needed.
+ *
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+@Deprecated
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface GltfModelToken extends AssetToken {}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/GltfModelTokenImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/GltfModelTokenImpl.java
new file mode 100644
index 0000000..83521ef
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/GltfModelTokenImpl.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.asset;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.NonNull;
+
+@Deprecated
+class GltfModelTokenImpl implements GltfModelToken {
+ @NonNull final com.android.extensions.xr.asset.GltfModelToken mToken;
+
+ GltfModelTokenImpl(@NonNull com.android.extensions.xr.asset.GltfModelToken token) {
+ requireNonNull(token);
+ mToken = token;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/SceneToken.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/SceneToken.java
new file mode 100644
index 0000000..b5da782
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/SceneToken.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.asset;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Token of a scene cached in the SpaceFlinger.
+ *
+ * <p>A Node can reference an {@link SceneToken} such that the SpaceFlinger will render the asset
+ * inside it.
+ *
+ * <p>Note that the app needs to keep track of the {@link SceneToken} so that it can continue using
+ * it, and eventually, free it when it is no longer needed.
+ *
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+@Deprecated
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface SceneToken extends AssetToken {}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/SceneTokenImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/SceneTokenImpl.java
new file mode 100644
index 0000000..9b37a6c
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/SceneTokenImpl.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.asset;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.NonNull;
+
+@Deprecated
+class SceneTokenImpl implements SceneToken {
+ @NonNull final com.android.extensions.xr.asset.SceneToken mToken;
+
+ SceneTokenImpl(@NonNull com.android.extensions.xr.asset.SceneToken token) {
+ requireNonNull(token);
+ mToken = token;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/TokenConverter.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/TokenConverter.java
new file mode 100644
index 0000000..e39a7e6
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/asset/TokenConverter.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.asset;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+/**
+ * This class is able to convert library versions of {@link AssetToken}s into platform types.
+ *
+ * @deprecated This will be removed once all clients are migrated.
+ */
+@Deprecated
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class TokenConverter {
+
+ private TokenConverter() {}
+
+ /**
+ * Converts a {@link GltfModelToken} to a framework type.
+ *
+ * @param token The {@link GltfModelToken} to convert.
+ * @return The framework type of the {@link GltfModelToken}.
+ * @deprecated This will be removed once all clients are migrated.
+ */
+ @Deprecated
+ @NonNull
+ public static com.android.extensions.xr.asset.GltfModelToken toFramework(
+ @NonNull GltfModelToken token) {
+ requireNonNull(token);
+
+ return ((GltfModelTokenImpl) token).mToken;
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.asset.GltfModelToken} to a library type.
+ *
+ * @param token The {@link com.android.extensions.xr.asset.GltfModelToken} to convert.
+ * @return The library type of the {@link GltfModelToken}.
+ */
+ @Nullable
+ public static GltfModelToken toLibrary(
+ @Nullable com.android.extensions.xr.asset.GltfModelToken token) {
+ if (token == null) {
+ return null;
+ }
+
+ return new GltfModelTokenImpl((com.android.extensions.xr.asset.GltfModelToken) token);
+ }
+
+ /**
+ * Converts a {@link EnvironmentToken} to a framework type.
+ *
+ * @param token The {@link EnvironmentToken} to convert.
+ * @return The framework type of the {@link com.android.extensions.xr.asset.EnvironmentToken}.
+ */
+ @NonNull
+ public static com.android.extensions.xr.asset.EnvironmentToken toFramework(
+ @NonNull EnvironmentToken token) {
+ requireNonNull(token);
+
+ return ((EnvironmentTokenImpl) token).mToken;
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.asset.EnvironmentToken} to a library type.
+ *
+ * @param token The {@link com.android.extensions.xr.asset.EnvironmentToken} to convert.
+ * @return The library type of the {@link EnvironmentToken}.
+ */
+ @Nullable
+ public static EnvironmentToken toLibrary(
+ @Nullable com.android.extensions.xr.asset.EnvironmentToken token) {
+ if (token == null) {
+ return null;
+ }
+
+ return new EnvironmentTokenImpl((com.android.extensions.xr.asset.EnvironmentToken) token);
+ }
+
+ /**
+ * Converts a {@link SceneToken} to a framework type.
+ *
+ * @param token The {@link SceneToken} to convert.
+ * @return The framework type of the {@link com.android.extensions.xr.asset.SceneToken}.
+ */
+ @NonNull
+ public static com.android.extensions.xr.asset.SceneToken toFramework(
+ @NonNull SceneToken token) {
+ requireNonNull(token);
+
+ return ((SceneTokenImpl) token).mToken;
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.asset.SceneToken} to a library type.
+ *
+ * @param token The {@link com.android.extensions.xr.asset.SceneToken} to convert.
+ * @return The library type of the {@link SceneToken}.
+ */
+ @Nullable
+ public static SceneToken toLibrary(@Nullable com.android.extensions.xr.asset.SceneToken token) {
+ if (token == null) {
+ return null;
+ }
+
+ return new SceneTokenImpl((com.android.extensions.xr.asset.SceneToken) token);
+ }
+
+ /**
+ * Converts a {@link GltfAnimation.State} to a framework type.
+ *
+ * @param token The {@link GltfAnimation.State} to convert.
+ * @return The framework type {@link com.android.extensions.xr.asset.GltfAnimation.State}.
+ */
+ public static com.android.extensions.xr.asset.GltfAnimation.State toFramework(
+ GltfAnimation.State token) {
+
+ switch (token) {
+ case STOP:
+ return com.android.extensions.xr.asset.GltfAnimation.State.STOP;
+ case PLAY:
+ return com.android.extensions.xr.asset.GltfAnimation.State.PLAY;
+ case LOOP:
+ return com.android.extensions.xr.asset.GltfAnimation.State.LOOP;
+ default:
+ throw new IllegalArgumentException("Should not happen");
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/environment/EnvironmentTypeConverter.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/environment/EnvironmentTypeConverter.java
new file mode 100644
index 0000000..147ec25
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/environment/EnvironmentTypeConverter.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.environment;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/** This class is able to convert library types into platform types. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class EnvironmentTypeConverter {
+
+ private EnvironmentTypeConverter() {}
+
+ /**
+ * Converts a {@link PassthroughVisibilityState} to a framework type.
+ *
+ * @param state The {@link PassthroughVisibilityState} to convert.
+ * @return The framework type of the {@link
+ * com.android.extensions.xr.environment.PassthroughVisibilityState}.
+ */
+ @NonNull
+ public static com.android.extensions.xr.environment.PassthroughVisibilityState toFramework(
+ @NonNull PassthroughVisibilityState state) {
+ requireNonNull(state);
+
+ return ((PassthroughVisibilityStateImpl) state).mState;
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.environment.PassthroughVisibilityState} to a
+ * library type.
+ *
+ * @param state The {@link com.android.extensions.xr.environment.PassthroughVisibilityState} to
+ * convert.
+ * @return The library type of the {@link PassthroughVisibilityState}.
+ */
+ @NonNull
+ public static PassthroughVisibilityState toLibrary(
+ @NonNull com.android.extensions.xr.environment.PassthroughVisibilityState state) {
+ requireNonNull(state);
+
+ return new PassthroughVisibilityStateImpl(state);
+ }
+
+ /**
+ * Converts a {@link EnvironmentVisibilityState} to a framework type.
+ *
+ * @param state The {@link EnvironmentVisibilityState} to convert.
+ * @return The framework type of the {@link
+ * com.android.extensions.xr.environment.EnvironmentVisibilityState}.
+ */
+ @NonNull
+ public static com.android.extensions.xr.environment.EnvironmentVisibilityState toFramework(
+ @NonNull EnvironmentVisibilityState state) {
+ requireNonNull(state);
+
+ return ((EnvironmentVisibilityStateImpl) state).mState;
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.environment.EnvironmentVisibilityState} to a
+ * library type.
+ *
+ * @param state The {@link com.android.extensions.xr.environment.EnvironmentVisibilityState} to
+ * convert.
+ * @return The library type of the {@link EnvironmentVisibilityState}.
+ */
+ @NonNull
+ public static EnvironmentVisibilityState toLibrary(
+ @NonNull com.android.extensions.xr.environment.EnvironmentVisibilityState state) {
+ requireNonNull(state);
+
+ return new EnvironmentVisibilityStateImpl(state);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/environment/EnvironmentVisibilityState.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/environment/EnvironmentVisibilityState.java
new file mode 100644
index 0000000..6a33d0b
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/environment/EnvironmentVisibilityState.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.environment;
+
+import static androidx.xr.extensions.XrExtensions.IMAGE_TOO_OLD;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+
+/** Visibility states of an environment. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface EnvironmentVisibilityState {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(
+ value = {
+ INVISIBLE,
+ HOME_VISIBLE,
+ APP_VISIBLE,
+ })
+ @Retention(SOURCE)
+ @interface State {}
+
+ /*
+ * No environment is shown. This could mean Passthrough is on with 100% opacity or the
+ * home environment app has crashed.
+ */
+ int INVISIBLE = 0;
+
+ /** Home environment is shown. Passthrough might be on but its opacity is less than 100%. */
+ int HOME_VISIBLE = 1;
+
+ /** App environment is shown. Passthrough might be on but its opacity is less than 100%. */
+ int APP_VISIBLE = 2;
+
+ /** Returns the current environment visibility state */
+ default @EnvironmentVisibilityState.State int getCurrentState() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/environment/EnvironmentVisibilityStateImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/environment/EnvironmentVisibilityStateImpl.java
new file mode 100644
index 0000000..ff90ee2
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/environment/EnvironmentVisibilityStateImpl.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.environment;
+
+import androidx.annotation.NonNull;
+
+class EnvironmentVisibilityStateImpl implements EnvironmentVisibilityState {
+ @NonNull final com.android.extensions.xr.environment.EnvironmentVisibilityState mState;
+
+ EnvironmentVisibilityStateImpl(
+ @NonNull com.android.extensions.xr.environment.EnvironmentVisibilityState state) {
+ mState = state;
+ }
+
+ @Override
+ public @EnvironmentVisibilityState.State int getCurrentState() {
+ return mState.getCurrentState();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || !(other instanceof EnvironmentVisibilityStateImpl)) {
+ return false;
+ }
+ EnvironmentVisibilityStateImpl impl = (EnvironmentVisibilityStateImpl) other;
+ return mState.equals(impl.mState);
+ }
+
+ @Override
+ public int hashCode() {
+ return mState.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return mState.toString();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/environment/PassthroughVisibilityState.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/environment/PassthroughVisibilityState.java
new file mode 100644
index 0000000..a1b6f98
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/environment/PassthroughVisibilityState.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.environment;
+
+import static androidx.xr.extensions.XrExtensions.IMAGE_TOO_OLD;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+
+/** Visibility states of passthrough. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface PassthroughVisibilityState {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(
+ value = {
+ DISABLED, HOME, APP, SYSTEM,
+ })
+ @Retention(SOURCE)
+ @interface State {}
+
+ /** Passthrough is not shown. */
+ int DISABLED = 0;
+
+ /** Home environment Passthrough is shown with greater than 0 opacity. */
+ int HOME = 1;
+
+ /** App set Passthrough is shown with greater than 0 opacity. */
+ int APP = 2;
+
+ /** System set Passthrough is shown with greater than 0 opacity. */
+ int SYSTEM = 3;
+
+ /** Returns the current passthrough visibility state */
+ default @PassthroughVisibilityState.State int getCurrentState() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /** Returns the current passthrough opacity */
+ default float getOpacity() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/environment/PassthroughVisibilityStateImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/environment/PassthroughVisibilityStateImpl.java
new file mode 100644
index 0000000..454bd51
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/environment/PassthroughVisibilityStateImpl.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.environment;
+
+import androidx.annotation.NonNull;
+
+class PassthroughVisibilityStateImpl implements PassthroughVisibilityState {
+ @NonNull final com.android.extensions.xr.environment.PassthroughVisibilityState mState;
+
+ PassthroughVisibilityStateImpl(
+ @NonNull com.android.extensions.xr.environment.PassthroughVisibilityState state) {
+ mState = state;
+ }
+
+ @Override
+ public @PassthroughVisibilityState.State int getCurrentState() {
+ return mState.getCurrentState();
+ }
+
+ @Override
+ public float getOpacity() {
+ return mState.getOpacity();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || !(other instanceof PassthroughVisibilityStateImpl)) {
+ return false;
+ }
+ PassthroughVisibilityStateImpl impl = (PassthroughVisibilityStateImpl) other;
+ return mState.equals(impl.mState);
+ }
+
+ @Override
+ public int hashCode() {
+ return mState.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return mState.toString();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/function/Consumer.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/function/Consumer.java
new file mode 100644
index 0000000..2507c9e
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/function/Consumer.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Represents a function that accepts an argument and produces no result. It is used internally to
+ * avoid using Java 8 functional interface that leads to desugaring and Proguard shrinking.
+ *
+ * @param <T> the type of the input of the function
+ * @see java.util.function.Consumer
+ */
+@FunctionalInterface
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface Consumer<T> {
+ /**
+ * Performs the operation on the given argument
+ *
+ * @param t the input argument
+ */
+ void accept(T t);
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/AudioManagerExtensions.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/AudioManagerExtensions.java
new file mode 100644
index 0000000..e2062f4
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/AudioManagerExtensions.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.media;
+
+import static androidx.xr.extensions.XrExtensions.IMAGE_TOO_OLD;
+
+import android.media.AudioManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/** Provides spatial audio extensions on the framework {@link AudioManagerExtensions} class. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface AudioManagerExtensions {
+
+ /**
+ * Play a spatialized sound effect for sound sources that will be rendered in 3D space as a
+ * point source.
+ *
+ * @param audioManager The {@link AudioManager} to use to play the sound effect.
+ * @param effectType The type of sound effect.
+ * @param attributes attributes to specify sound source in 3D. {@link PointSourceAttributes}.
+ */
+ default void playSoundEffectAsPointSource(
+ @NonNull AudioManager audioManager,
+ int effectType,
+ @NonNull PointSourceAttributes attributes) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/AudioManagerExtensionsImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/AudioManagerExtensionsImpl.java
new file mode 100644
index 0000000..6e2f5b1
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/AudioManagerExtensionsImpl.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.media;
+
+import static java.util.Objects.requireNonNull;
+
+import android.media.AudioManager;
+
+import androidx.annotation.NonNull;
+
+/** Wraps a {@link com.android.extensions.xr.media.AudioManagerExtensions}. */
+class AudioManagerExtensionsImpl implements AudioManagerExtensions {
+ @NonNull final com.android.extensions.xr.media.AudioManagerExtensions mAudioManager;
+
+ AudioManagerExtensionsImpl(
+ @NonNull com.android.extensions.xr.media.AudioManagerExtensions audioManager) {
+ requireNonNull(audioManager);
+ mAudioManager = audioManager;
+ }
+
+ @Override
+ public void playSoundEffectAsPointSource(
+ @NonNull AudioManager audioManager,
+ int effectType,
+ @NonNull PointSourceAttributes attributes) {
+ mAudioManager.playSoundEffectAsPointSource(
+ audioManager,
+ effectType,
+ PointSourceAttributesHelper.convertToFramework(attributes));
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/AudioTrackExtensions.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/AudioTrackExtensions.java
new file mode 100644
index 0000000..039d7da
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/AudioTrackExtensions.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.media;
+
+import static androidx.xr.extensions.XrExtensions.IMAGE_TOO_OLD;
+
+import android.media.AudioTrack;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+/** Provides spatial audio extensions on the framework {@link AudioTrack} class. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface AudioTrackExtensions {
+ /**
+ * Gets the {@link PointSourceAttributes} of the provided {@link AudioTrack}.
+ *
+ * @param track The {@link AudioTrack} from which to get the {@link PointSourceAttributes}.
+ * @return The {@link PointSourceAttributes} of the provided track, null if not set.
+ */
+ @Nullable
+ default PointSourceAttributes getPointSourceAttributes(@NonNull AudioTrack track) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Gets the {@link SoundFieldAttributes} of the provided {@link AudioTrack}.
+ *
+ * @param track The {@link AudioTrack} from which to get the {@link SoundFieldAttributes}.
+ * @return The {@link SoundFieldAttributes} of the provided track, null if not set.
+ */
+ @Nullable
+ default SoundFieldAttributes getSoundFieldAttributes(@NonNull AudioTrack track) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Gets the {@link SourceType} of the provided {@link AudioTrack}. This value is implicitly set
+ * depending one which type of attributes was used to configure the builder. Will return {@link
+ * SOURCE_TYPE_BYPASS} for tracks that didn't use spatial audio attributes.
+ *
+ * @param track The {@link AudioTrack} from which to get the {@link SourceType}.
+ * @return The {@link SourceType} of the provided track.
+ */
+ default @SpatializerExtensions.SourceType int getSpatialSourceType(@NonNull AudioTrack track) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Sets the {@link PointSourceAttributes} on the provided {@link AudioTrack.Builder}.
+ *
+ * @param builder The Builder on which to set the attributes.
+ * @param attributes The source attributes to be set.
+ * @return The same {AudioTrack.Builder} instance provided.
+ */
+ @NonNull
+ default AudioTrack.Builder setPointSourceAttributes(
+ @NonNull AudioTrack.Builder builder, @NonNull PointSourceAttributes attributes) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Sets the {@link SoundFieldAttributes} on the provided {@link AudioTrack.Builder}.
+ *
+ * @param builder The Builder on which to set the attributes.
+ * @param attributes The sound field attributes to be set.
+ * @return The same {AudioTrack.Builder} instance provided.
+ */
+ @NonNull
+ default AudioTrack.Builder setSoundFieldAttributes(
+ @NonNull AudioTrack.Builder builder, @NonNull SoundFieldAttributes attributes) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/AudioTrackExtensionsImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/AudioTrackExtensionsImpl.java
new file mode 100644
index 0000000..f3cb5705
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/AudioTrackExtensionsImpl.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.media;
+
+import static java.util.Objects.requireNonNull;
+
+import android.media.AudioTrack;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Wraps a {@link com.android.extensions.xr.media.AudioTrackExtensions}. */
+class AudioTrackExtensionsImpl implements AudioTrackExtensions {
+ @NonNull final com.android.extensions.xr.media.AudioTrackExtensions mAudioTrack;
+
+ AudioTrackExtensionsImpl(
+ @NonNull com.android.extensions.xr.media.AudioTrackExtensions audioTrack) {
+ requireNonNull(audioTrack);
+ mAudioTrack = audioTrack;
+ }
+
+ @Override
+ @Nullable
+ public PointSourceAttributes getPointSourceAttributes(@NonNull AudioTrack track) {
+ return PointSourceAttributesHelper.convertToExtensions(
+ mAudioTrack.getPointSourceAttributes(track));
+ }
+
+ @Override
+ @Nullable
+ public SoundFieldAttributes getSoundFieldAttributes(@NonNull AudioTrack track) {
+ return SoundFieldAttributesHelper.convertToExtensions(
+ mAudioTrack.getSoundFieldAttributes(track));
+ }
+
+ @Override
+ public @SpatializerExtensions.SourceType int getSpatialSourceType(@NonNull AudioTrack track) {
+ return mAudioTrack.getSpatialSourceType(track);
+ }
+
+ @Override
+ @NonNull
+ public AudioTrack.Builder setPointSourceAttributes(
+ @NonNull AudioTrack.Builder builder, @NonNull PointSourceAttributes attributes) {
+ return mAudioTrack.setPointSourceAttributes(
+ builder, PointSourceAttributesHelper.convertToFramework(attributes));
+ }
+
+ @Override
+ @NonNull
+ public AudioTrack.Builder setSoundFieldAttributes(
+ @NonNull AudioTrack.Builder builder, @NonNull SoundFieldAttributes attributes) {
+ return mAudioTrack.setSoundFieldAttributes(
+ builder, SoundFieldAttributesHelper.convertToFramework(attributes));
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/MediaPlayerExtensions.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/MediaPlayerExtensions.java
new file mode 100644
index 0000000..dd6d97a5
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/MediaPlayerExtensions.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.media;
+
+import static androidx.xr.extensions.XrExtensions.IMAGE_TOO_OLD;
+
+import android.media.MediaPlayer;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/** Provides spatial audio extensions on the framework {@link MediaPlayer} class. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface MediaPlayerExtensions {
+
+ /**
+ * @param mediaPlayer The {@link MediaPlayer} on which to set the attributes.
+ * @param attributes The source attributes to be set.
+ * @return The same {@link MediaPlayer} instance provided.
+ */
+ @NonNull
+ default MediaPlayer setPointSourceAttributes(
+ @NonNull MediaPlayer mediaPlayer, @NonNull PointSourceAttributes attributes) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Sets the {@link SoundFieldAttributes} on the provided {@link MediaPlayer}.
+ *
+ * @param mediaPlayer The {@link MediaPlayer} on which to set the attributes.
+ * @param attributes The source attributes to be set.
+ * @return The same {@link MediaPlayer} instance provided.
+ */
+ @NonNull
+ default MediaPlayer setSoundFieldAttributes(
+ @NonNull MediaPlayer mediaPlayer, @NonNull SoundFieldAttributes attributes) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/MediaPlayerExtensionsImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/MediaPlayerExtensionsImpl.java
new file mode 100644
index 0000000..d8e0478
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/MediaPlayerExtensionsImpl.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.media;
+
+import static java.util.Objects.requireNonNull;
+
+import android.media.MediaPlayer;
+
+import androidx.annotation.NonNull;
+
+/** Wraps a {@link com.android.extensions.xr.media.MediaPlayerExtensions}. */
+class MediaPlayerExtensionsImpl implements MediaPlayerExtensions {
+ @NonNull final com.android.extensions.xr.media.MediaPlayerExtensions mMediaPlayer;
+
+ MediaPlayerExtensionsImpl(
+ @NonNull com.android.extensions.xr.media.MediaPlayerExtensions mediaPlayer) {
+ requireNonNull(mediaPlayer);
+ mMediaPlayer = mediaPlayer;
+ }
+
+ @Override
+ @NonNull
+ public MediaPlayer setPointSourceAttributes(
+ MediaPlayer mediaPlayer, PointSourceAttributes attributes) {
+ return mMediaPlayer.setPointSourceAttributes(
+ mediaPlayer, PointSourceAttributesHelper.convertToFramework(attributes));
+ }
+
+ @Override
+ @NonNull
+ public MediaPlayer setSoundFieldAttributes(
+ MediaPlayer mediaPlayer, SoundFieldAttributes attributes) {
+ return mMediaPlayer.setSoundFieldAttributes(
+ mediaPlayer, SoundFieldAttributesHelper.convertToFramework(attributes));
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/MediaTypeConverter.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/MediaTypeConverter.java
new file mode 100644
index 0000000..ebaddd2
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/MediaTypeConverter.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.media;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/** This class is able to convert library types into platform types. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class MediaTypeConverter {
+
+ private MediaTypeConverter() {}
+
+ /**
+ * Converts a {@link com.android.extensions.xr.media.XrSpatialAudioExtensions} to a library
+ * type.
+ *
+ * @param extensions The {@link com.android.extensions.xr.media.XrSpatialAudioExtensions} to
+ * convert.
+ * @return The library type of the {@link XrSpatialAudioExtensions}.
+ */
+ @NonNull
+ public static XrSpatialAudioExtensions toLibrary(
+ @NonNull com.android.extensions.xr.media.XrSpatialAudioExtensions extensions) {
+ requireNonNull(extensions);
+
+ return new XrSpatialAudioExtensionsImpl(extensions);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/PointSourceAttributes.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/PointSourceAttributes.java
new file mode 100644
index 0000000..09579cd
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/PointSourceAttributes.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.media;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.xr.extensions.XrExtensionsProvider;
+import androidx.xr.extensions.node.Node;
+
+/** {@link PointSourceAttributes} is used to configure a sound be spatialized as a 3D point. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class PointSourceAttributes {
+ private Node mNode;
+
+ private PointSourceAttributes(@NonNull Node node) {
+ mNode = node;
+ }
+
+ /**
+ * The {@link Node} to which this sound source is attached. The sound source will use the 3D
+ * transform of the Node. The node returned from this method must be parented to a node in the
+ * scene.
+ *
+ * @return The {@link Node} to which the sound source is attached.
+ */
+ public @NonNull Node getNode() {
+ return mNode;
+ }
+
+ /** Builder class for {@link PointSourceAttributes} */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public static final class Builder {
+ private Node mNode;
+
+ public Builder() {}
+
+ /**
+ * @param node The {@link Node} to use to position the sound source.
+ * @return The Builder instance.
+ */
+ public @NonNull Builder setNode(@NonNull Node node) {
+ mNode = node;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link PointSourceAttributes} to be used. If no {@link Node} is provided,
+ * this will create a new {@link Node} that must be parented to a node in the current scene.
+ *
+ * @return A new {@link PointSourceAttributes} object.
+ */
+ @NonNull
+ public PointSourceAttributes build() throws UnsupportedOperationException {
+ if (mNode == null) {
+ mNode = XrExtensionsProvider.getXrExtensions().createNode();
+ }
+
+ return new PointSourceAttributes(mNode);
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/PointSourceAttributesHelper.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/PointSourceAttributesHelper.java
new file mode 100644
index 0000000..709ce10
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/PointSourceAttributesHelper.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.media;
+
+import androidx.xr.extensions.node.NodeTypeConverter;
+
+// TODO(bvanderlaan): Replace this with a MediaTypeConverter for consistency with other modules
+class PointSourceAttributesHelper {
+
+ private PointSourceAttributesHelper() {}
+
+ static com.android.extensions.xr.media.PointSourceAttributes convertToFramework(
+ PointSourceAttributes attributes) {
+
+ return new com.android.extensions.xr.media.PointSourceAttributes.Builder()
+ .setNode(NodeTypeConverter.toFramework(attributes.getNode()))
+ .build();
+ }
+
+ static PointSourceAttributes convertToExtensions(
+ com.android.extensions.xr.media.PointSourceAttributes fwkAttributes) {
+
+ return new PointSourceAttributes.Builder()
+ .setNode(NodeTypeConverter.toLibrary(fwkAttributes.getNode()))
+ .build();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/SoundFieldAttributes.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/SoundFieldAttributes.java
new file mode 100644
index 0000000..dff8cd57
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/SoundFieldAttributes.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.media;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/** {@link SoundFieldAttributes} is used to configure ambisonic sound sources. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SoundFieldAttributes {
+ private int mAmbisonicsOrder;
+
+ private SoundFieldAttributes(int ambisonicsOrder) {
+ mAmbisonicsOrder = ambisonicsOrder;
+ }
+
+ /**
+ * @return The {@link SpatializerExtensions.AmbisonicsOrder} of this sound source.
+ */
+ @SpatializerExtensions.AmbisonicsOrder
+ public int getAmbisonicsOrder() {
+ return mAmbisonicsOrder;
+ }
+
+ /** Builder class for {@link SoundFieldAttributes} */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public static final class Builder {
+ private int mAmbisonicsOrder = SpatializerExtensions.AMBISONICS_ORDER_FIRST_ORDER;
+
+ public Builder() {}
+
+ /**
+ * @param ambisonicsOrder Sets the {@link SpatializerExtensions.AmbisonicsOrder} of this
+ * sound source.
+ * @return The Builder instance.
+ */
+ public @NonNull Builder setAmbisonicsOrder(
+ @SpatializerExtensions.AmbisonicsOrder int ambisonicsOrder) {
+ mAmbisonicsOrder = ambisonicsOrder;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link PointSourceAttributes} to be used. If no {@link Node} is provided,
+ * this will create a new {@link Node} that must be parented to a node in the current scene.
+ *
+ * @return A new {@link PointSourceAttributes} object.
+ */
+ @NonNull
+ public SoundFieldAttributes build() throws UnsupportedOperationException {
+ return new SoundFieldAttributes(mAmbisonicsOrder);
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/SoundFieldAttributesHelper.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/SoundFieldAttributesHelper.java
new file mode 100644
index 0000000..ff9cb0b
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/SoundFieldAttributesHelper.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.media;
+
+// TODO(bvanderlaan): Replace this with a MediaTypeConverter for consistency with other modules
+class SoundFieldAttributesHelper {
+
+ private SoundFieldAttributesHelper() {}
+
+ static com.android.extensions.xr.media.SoundFieldAttributes convertToFramework(
+ SoundFieldAttributes attributes) {
+
+ com.android.extensions.xr.media.SoundFieldAttributes.Builder builder =
+ new com.android.extensions.xr.media.SoundFieldAttributes.Builder();
+ builder.setAmbisonicsOrder(attributes.getAmbisonicsOrder());
+ return builder.build();
+ }
+
+ static SoundFieldAttributes convertToExtensions(
+ com.android.extensions.xr.media.SoundFieldAttributes fwkAttributes) {
+
+ SoundFieldAttributes.Builder builder = new SoundFieldAttributes.Builder();
+ builder.setAmbisonicsOrder(fwkAttributes.getAmbisonicsOrder());
+ return builder.build();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/SoundPoolExtensions.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/SoundPoolExtensions.java
new file mode 100644
index 0000000..90d60f8
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/SoundPoolExtensions.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.media;
+
+import static androidx.xr.extensions.XrExtensions.IMAGE_TOO_OLD;
+
+import android.media.SoundPool;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/** Provides spatial audio extensions on the framework {@link SoundPool} class. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface SoundPoolExtensions {
+ /**
+ * Plays a spatialized sound effect emitted relative {@link Node} in the {@link
+ * PointSourceAttributes}.
+ *
+ * @param soundPool The {@link SoundPool} to use to the play the sound.
+ * @param soundID a soundId returned by the load() function.
+ * @param attributes attributes to specify sound source. {@link PointSourceAttributes}
+ * @param volume volume value (range = 0.0 to 1.0)
+ * @param priority stream priority (0 = lowest priority)
+ * @param loop loop mode (0 = no loop, -1 = loop forever)
+ * @param rate playback rate (1.0 = normal playback, range 0.5 to 2.0)
+ * @return non-zero streamID if successful, zero if failed
+ */
+ default int playAsPointSource(
+ @NonNull SoundPool soundPool,
+ int soundID,
+ @NonNull PointSourceAttributes attributes,
+ float volume,
+ int priority,
+ int loop,
+ float rate) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Plays a spatialized sound effect as a sound field.
+ *
+ * @param soundPool The {@link SoundPool} to use to the play the sound.
+ * @param soundID a soundId returned by the load() function.
+ * @param attributes attributes to specify sound source. {@link SoundFieldAttributes}
+ * @param volume volume value (range = 0.0 to 1.0)
+ * @param priority stream priority (0 = lowest priority)
+ * @param loop loop mode (0 = no loop, -1 = loop forever)
+ * @param rate playback rate (1.0 = normal playback, range 0.5 to 2.0)
+ * @return non-zero streamID if successful, zero if failed
+ */
+ default int playAsSoundField(
+ @NonNull SoundPool soundPool,
+ int soundID,
+ @NonNull SoundFieldAttributes attributes,
+ float volume,
+ int priority,
+ int loop,
+ float rate) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * @param soundPool The {@link SoundPool} to use to get its SourceType.
+ * @param streamID a streamID returned by the play(), playAsPointSource(), or
+ * playAsSoundField().
+ * @return The {@link SpatializerExtensions.SourceType} for the given streamID.
+ */
+ default @SpatializerExtensions.SourceType int getSpatialSourceType(
+ @NonNull SoundPool soundPool, int streamID) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/SoundPoolExtensionsImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/SoundPoolExtensionsImpl.java
new file mode 100644
index 0000000..2554ca4
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/SoundPoolExtensionsImpl.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.media;
+
+import static java.util.Objects.requireNonNull;
+
+import android.media.SoundPool;
+
+import androidx.annotation.NonNull;
+
+/** Wraps a {@link com.android.extensions.xr.media.SoundPoolExtensions}. */
+class SoundPoolExtensionsImpl implements SoundPoolExtensions {
+ @NonNull final com.android.extensions.xr.media.SoundPoolExtensions mSoundPool;
+
+ SoundPoolExtensionsImpl(
+ @NonNull com.android.extensions.xr.media.SoundPoolExtensions soundPool) {
+ requireNonNull(soundPool);
+ mSoundPool = soundPool;
+ }
+
+ @Override
+ public int playAsPointSource(
+ @NonNull SoundPool soundPool,
+ int soundID,
+ @NonNull PointSourceAttributes attributes,
+ float volume,
+ int priority,
+ int loop,
+ float rate) {
+ com.android.extensions.xr.media.PointSourceAttributes internal =
+ PointSourceAttributesHelper.convertToFramework(attributes);
+
+ return mSoundPool.playAsPointSource(
+ soundPool, soundID, internal, volume, priority, loop, rate);
+ }
+
+ @Override
+ public int playAsSoundField(
+ @NonNull SoundPool soundPool,
+ int soundID,
+ @NonNull SoundFieldAttributes attributes,
+ float volume,
+ int priority,
+ int loop,
+ float rate) {
+ com.android.extensions.xr.media.SoundFieldAttributes internal =
+ SoundFieldAttributesHelper.convertToFramework(attributes);
+
+ return mSoundPool.playAsSoundField(
+ soundPool, soundID, internal, volume, priority, loop, rate);
+ }
+
+ @Override
+ @SpatializerExtensions.SourceType
+ public int getSpatialSourceType(@NonNull SoundPool soundPool, int streamID) {
+ return mSoundPool.getSpatialSourceType(soundPool, streamID);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/SpatializerExtensions.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/SpatializerExtensions.java
new file mode 100644
index 0000000..dbd1880
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/SpatializerExtensions.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.media;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+
+/** Extensions of the existing {@link Spatializer} class. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface SpatializerExtensions {
+ /** Used to set the Ambisonics order of a {@link SoundFieldAttributes} */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(
+ value = {
+ AMBISONICS_ORDER_FIRST_ORDER,
+ AMBISONICS_ORDER_SECOND_ORDER,
+ AMBISONICS_ORDER_THIRD_ORDER,
+ })
+ @Retention(SOURCE)
+ @interface AmbisonicsOrder {}
+
+ /** Specifies spatial rendering using First Order Ambisonics */
+ int AMBISONICS_ORDER_FIRST_ORDER = 0;
+
+ /** Specifies spatial rendering using Second Order Ambisonics */
+ int AMBISONICS_ORDER_SECOND_ORDER = 1;
+
+ /** Specifies spatial rendering using Third Order Ambisonics */
+ int AMBISONICS_ORDER_THIRD_ORDER = 2;
+
+ /** Represents the type of spatialization for an audio source */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(
+ value = {
+ SOURCE_TYPE_BYPASS,
+ SOURCE_TYPE_POINT_SOURCE,
+ SOURCE_TYPE_SOUND_FIELD,
+ })
+ @Retention(SOURCE)
+ @interface SourceType {}
+
+ /** The sound source has not been spatialized with the Spatial Audio SDK. */
+ int SOURCE_TYPE_BYPASS = 0;
+
+ /** The sound source has been spatialized as a 3D point source. */
+ int SOURCE_TYPE_POINT_SOURCE = 1;
+
+ /** The sound source is an ambisonics sound field. */
+ int SOURCE_TYPE_SOUND_FIELD = 2;
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/XrSpatialAudioExtensions.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/XrSpatialAudioExtensions.java
new file mode 100644
index 0000000..3c9f0e8
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/XrSpatialAudioExtensions.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.media;
+
+import static androidx.xr.extensions.XrExtensions.IMAGE_TOO_OLD;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/** Provides new functionality of existing framework APIs needed to Spatialize audio sources. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface XrSpatialAudioExtensions {
+ /**
+ * @return {@link SoundPoolExtensions} instance to control spatial audio from a {@link
+ * SoundPool}.
+ */
+ default @NonNull SoundPoolExtensions getSoundPoolExtensions() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * @return {@link AudioTrackExtensions} instance to control spatial audio from an {@link
+ * AudioTrack}.
+ */
+ default @NonNull AudioTrackExtensions getAudioTrackExtensions() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * @return {@link AudioManagerExtensions} instance to control spatial audio from an {@link
+ * AudioManager}.
+ */
+ default @NonNull AudioManagerExtensions getAudioManagerExtensions() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * @return {@link MediaPlayerExtensions} instance to control spatial audio from a {@link
+ * MediaPlayer}.
+ */
+ default @NonNull MediaPlayerExtensions getMediaPlayerExtensions() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/XrSpatialAudioExtensionsImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/XrSpatialAudioExtensionsImpl.java
new file mode 100644
index 0000000..9791bc0
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/media/XrSpatialAudioExtensionsImpl.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.media;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.NonNull;
+
+/** Provides new functionality of existing framework APIs needed to spatialize audio sources. */
+class XrSpatialAudioExtensionsImpl implements XrSpatialAudioExtensions {
+ @NonNull final com.android.extensions.xr.media.XrSpatialAudioExtensions mExtensions;
+
+ @NonNull private final SoundPoolExtensionsImpl mSoundPoolExtensions;
+ @NonNull private final AudioTrackExtensionsImpl mAudioTrackExtensions;
+ @NonNull private final AudioManagerExtensionsImpl mAudioManagerExtensions;
+ @NonNull private final MediaPlayerExtensionsImpl mMediaPlayerExtensions;
+
+ XrSpatialAudioExtensionsImpl(
+ @NonNull com.android.extensions.xr.media.XrSpatialAudioExtensions extensions) {
+ requireNonNull(extensions);
+ mExtensions = extensions;
+
+ mSoundPoolExtensions = new SoundPoolExtensionsImpl(mExtensions.getSoundPoolExtensions());
+ mAudioTrackExtensions = new AudioTrackExtensionsImpl(mExtensions.getAudioTrackExtensions());
+ mAudioManagerExtensions =
+ new AudioManagerExtensionsImpl(mExtensions.getAudioManagerExtensions());
+ mMediaPlayerExtensions =
+ new MediaPlayerExtensionsImpl(mExtensions.getMediaPlayerExtensions());
+ }
+
+ @Override
+ @NonNull
+ public SoundPoolExtensions getSoundPoolExtensions() {
+ return mSoundPoolExtensions;
+ }
+
+ @Override
+ @NonNull
+ public AudioTrackExtensions getAudioTrackExtensions() {
+ return mAudioTrackExtensions;
+ }
+
+ @Override
+ @NonNull
+ public AudioManagerExtensions getAudioManagerExtensions() {
+ return mAudioManagerExtensions;
+ }
+
+ @Override
+ @NonNull
+ public MediaPlayerExtensions getMediaPlayerExtensions() {
+ return mMediaPlayerExtensions;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/InputEvent.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/InputEvent.java
new file mode 100644
index 0000000..64fa841
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/InputEvent.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.node;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+
+/** A single 6DOF pointer event. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface InputEvent {
+ // clang-format off
+ /** The type of the source of this event. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(
+ value = {
+ SOURCE_UNKNOWN,
+ SOURCE_HEAD,
+ SOURCE_CONTROLLER,
+ SOURCE_HANDS,
+ SOURCE_MOUSE,
+ SOURCE_GAZE_AND_GESTURE,
+ })
+ @Retention(SOURCE)
+ @interface Source {}
+
+ // clang-format on
+
+ // Unknown source.
+ int SOURCE_UNKNOWN = 0;
+
+ /**
+ * Event is based on the user's head. Ray origin is at average between eyes, pushed out to the
+ * near clipping plane for both eyes and points in direction head is facing. Action state is
+ * based on volume up button being depressed.
+ *
+ * <p>Events from this device type are considered sensitive and hover events are never sent.
+ */
+ int SOURCE_HEAD = 1;
+
+ /**
+ * Event is based on (one of) the user's controller(s). Ray origin and direction are for a
+ * controller aim pose as defined by OpenXR.
+ * (https://registry.khronos.org/OpenXR/specs/1.1/html/xrspec.html#semantic-paths-standard-pose-identifiers)
+ * Action state is based on the primary button on the controller, usually the bottom-most face
+ * button.
+ */
+ int SOURCE_CONTROLLER = 2;
+
+ /**
+ * Event is based on one of the user's hands. Ray is a hand aim pose, with origin between thumb
+ * and forefinger and points in direction based on hand orientation. Action state is based on a
+ * pinch gesture.
+ */
+ int SOURCE_HANDS = 3;
+
+ /**
+ * Event is based on a 2D mouse pointing device. Ray origin behaves the same as for
+ * DEVICE_TYPE_HEAD and points in direction based on mouse movement. During a drag, the ray
+ * origin moves approximating hand motion. The scrollwheel moves the ray away from / towards the
+ * user. Action state is based on the primary mouse button.
+ */
+ int SOURCE_MOUSE = 4;
+
+ /**
+ * Event is based on a mix of the head, eyes, and hands. Ray origin is at average between eyes
+ * and points in direction based on a mix of eye gaze direction and hand motion. During a
+ * two-handed zoom/rotate gesture, left/right pointer events will be issued; otherwise, default
+ * events are issued based on the gaze ray. Action state is based on if the user has done a
+ * pinch gesture or not.
+ *
+ * <p>Events from this device type are considered sensitive and hover events are never sent.
+ */
+ int SOURCE_GAZE_AND_GESTURE = 5;
+
+ /** Returns the source of this event. */
+ @Source
+ int getSource();
+
+ /** The type of the individual pointer. */
+ // clang-format off
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(
+ value = {
+ POINTER_TYPE_DEFAULT,
+ POINTER_TYPE_LEFT,
+ POINTER_TYPE_RIGHT,
+ })
+ @Retention(SOURCE)
+ @interface PointerType {}
+
+ // clang-format on
+
+ /**
+ * Default pointer type for the source (no handedness). Occurs for SOURCE_UNKNOWN, SOURCE_HEAD,
+ * SOURCE_MOUSE, and SOURCE_GAZE_AND_GESTURE.
+ */
+ int POINTER_TYPE_DEFAULT = 0;
+
+ /**
+ * Left hand / controller pointer.. Occurs for SOURCE_CONTROLLER, SOURCE_HANDS, and
+ * SOURCE_GAZE_AND_GESTURE.
+ */
+ int POINTER_TYPE_LEFT = 1;
+
+ /**
+ * Right hand / controller pointer.. Occurs for SOURCE_CONTROLLER, SOURCE_HANDS, and
+ * SOURCE_GAZE_AND_GESTURE.
+ */
+ int POINTER_TYPE_RIGHT = 2;
+
+ /** Returns the pointer type of this event. */
+ @PointerType
+ int getPointerType();
+
+ /** The time this event occurred, in the android.os.SystemClock#uptimeMillis time base. */
+ long getTimestamp();
+
+ /** The origin of the ray, in the receiver's task coordinate space. */
+ @NonNull
+ Vec3 getOrigin();
+
+ /**
+ * The direction the ray is pointing in, in the receiver's task coordinate space. Any point
+ * along the ray can be represented as origin + d * direction, where d is non-negative.
+ */
+ @NonNull
+ Vec3 getDirection();
+
+ /** Info about a single ray hit. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ interface HitInfo {
+ /**
+ * ID of the front-end Impress node within the subspace that was hit. Used by Split-Engine
+ * to create a handle to the node with the same entity ID. In case the node doesn't belong
+ * to a subspace the value will be 0, i.e.,
+ * utils::Entity::import(subspaceImpressNodeId).IsNull() == true.
+ *
+ * <p>ACTION_MOVE, ACTION_UP, and ACTION_CANCEL events will report the same node id as was
+ * hit during the initial ACTION_DOWN.
+ */
+ int getSubspaceImpressNodeId();
+
+ /**
+ * The CPM node that was hit.
+ *
+ * <p>ACTION_MOVE, ACTION_UP, and ACTION_CANCEL events will report the same node as was hit
+ * during the initial ACTION_DOWN.
+ */
+ @NonNull
+ Node getInputNode();
+
+ /**
+ * The ray hit position, in the receiver's task coordinate space.
+ *
+ * <p>All events may report the current ray's hit position. This can be null if there no
+ * longer is a collision between the ray and the input node (eg, during a drag event).
+ */
+ @Nullable
+ Vec3 getHitPosition();
+
+ /** The matrix transforming task node coordinates into the hit CPM node's coordinates. */
+ @NonNull
+ Mat4f getTransform();
+ }
+
+ /**
+ * Info about the first scene node (closest to the ray origin) that was hit by the input ray, if
+ * any. This will be null if no node was hit. Note that the hit node remains the same during an
+ * ongoing DOWN -> MOVE -> UP action, even if the pointer stops hitting the node during the
+ * action.
+ */
+ @Nullable
+ HitInfo getHitInfo();
+
+ /** Info about the second scene node from the same task that was hit, if any. */
+ @Nullable
+ HitInfo getSecondaryHitInfo();
+
+ /** Event dispatch flags. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(value = {DISPATCH_FLAG_NONE, DISPATCH_FLAG_CAPTURED_POINTER, DISPATCH_FLAG_2D})
+ @Retention(SOURCE)
+ @interface DispatchFlag {}
+
+ // Normal dispatch.
+ int DISPATCH_FLAG_NONE = 0;
+ // This event was dispatched to this receiver only because pointer capture was enabled.
+ int DISPATCH_FLAG_CAPTURED_POINTER = 1;
+ // This event was also dispatched as a 2D Android input event.
+ int DISPATCH_FLAG_2D = 2;
+
+ /** Returns the dispatch flags for this event. */
+ @DispatchFlag
+ int getDispatchFlags();
+
+ // clang-format off
+ /**
+ * Actions similar to Android's MotionEvent actions:
+ * https://developer.android.com/reference/android/view/MotionEvent for keeping track of a
+ * sequence of events on the same target, e.g., * HOVER_ENTER -> HOVER_MOVE -> HOVER_EXIT * DOWN
+ * -> MOVE -> UP
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(
+ value = {
+ ACTION_DOWN,
+ ACTION_UP,
+ ACTION_MOVE,
+ ACTION_CANCEL,
+ ACTION_HOVER_MOVE,
+ ACTION_HOVER_ENTER,
+ ACTION_HOVER_EXIT,
+ })
+ @Retention(SOURCE)
+ @interface Action {}
+
+ // clang-format on
+
+ /** The primary action button or gesture was just pressed / started. */
+ int ACTION_DOWN = 0;
+
+ /**
+ * The primary action button or gesture was just released / stopped. The hit info represents the
+ * node that was originally hit (ie, as provided in the ACTION_DOWN event).
+ */
+ int ACTION_UP = 1;
+
+ /**
+ * The primary action button or gesture was pressed/active in the previous event, and is still
+ * pressed/active. The hit info represents the node that was originally hit (ie, as provided in
+ * the ACTION_DOWN event). The hit position may be null if the pointer is no longer hitting that
+ * node.
+ */
+ int ACTION_MOVE = 2;
+
+ /**
+ * While the primary action button or gesture was held, the pointer was disabled. This happens
+ * if you are using controllers and the battery runs out, or if you are using a source that
+ * transitions to a new pointer type, eg SOURCE_GAZE_AND_GESTURE.
+ */
+ int ACTION_CANCEL = 3;
+
+ /**
+ * The primary action button or gesture is not pressed, and the pointer ray continued to hit the
+ * same node. The hit info represents the node that was hit (may be null if pointer capture is
+ * enabled).
+ *
+ * <p>Hover input events are never provided for sensitive source types.
+ */
+ int ACTION_HOVER_MOVE = 4;
+
+ /**
+ * The primary action button or gesture is not pressed, and the pointer ray started to hit a new
+ * node. The hit info represents the node that is being hit (may be null if pointer capture is
+ * enabled).
+ *
+ * <p>Hover input events are never provided for sensitive source types.
+ */
+ int ACTION_HOVER_ENTER = 5;
+
+ /**
+ * The primary action button or gesture is not pressed, and the pointer ray stopped hitting the
+ * node that it was previously hitting. The hit info represents the node that was being hit (may
+ * be null if pointer capture is enabled).
+ *
+ * <p>Hover input events are never provided for sensitive source types.
+ */
+ int ACTION_HOVER_EXIT = 6;
+
+ /** Returns the current action associated with this input event. */
+ @Action
+ int getAction();
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/InputEventImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/InputEventImpl.java
new file mode 100644
index 0000000..c6c109e
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/InputEventImpl.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.node;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+class InputEventImpl implements InputEvent {
+ @NonNull private final com.android.extensions.xr.node.InputEvent mEvent;
+
+ InputEventImpl(@NonNull com.android.extensions.xr.node.InputEvent event) {
+ mEvent = event;
+ }
+
+ @Override
+ public int getSource() {
+ return mEvent.getSource();
+ }
+
+ @Override
+ public int getPointerType() {
+ return mEvent.getPointerType();
+ }
+
+ @Override
+ public long getTimestamp() {
+ return mEvent.getTimestamp();
+ }
+
+ @Override
+ @NonNull
+ public Vec3 getOrigin() {
+ com.android.extensions.xr.node.Vec3 origin = mEvent.getOrigin();
+ return new Vec3(origin.x, origin.y, origin.z);
+ }
+
+ @Override
+ @NonNull
+ public Vec3 getDirection() {
+ com.android.extensions.xr.node.Vec3 direction = mEvent.getDirection();
+ return new Vec3(direction.x, direction.y, direction.z);
+ }
+
+ @Override
+ @Nullable
+ public HitInfo getHitInfo() {
+ com.android.extensions.xr.node.InputEvent.HitInfo info = mEvent.getHitInfo();
+ return (info == null) ? null : new HitInfo(info);
+ }
+
+ @Override
+ @Nullable
+ public HitInfo getSecondaryHitInfo() {
+ com.android.extensions.xr.node.InputEvent.HitInfo info = mEvent.getSecondaryHitInfo();
+ return (info == null) ? null : new HitInfo(info);
+ }
+
+ @Override
+ public int getDispatchFlags() {
+ return mEvent.getDispatchFlags();
+ }
+
+ @Override
+ public int getAction() {
+ return mEvent.getAction();
+ }
+
+ static class HitInfo implements InputEvent.HitInfo {
+ @NonNull private final com.android.extensions.xr.node.InputEvent.HitInfo info;
+
+ HitInfo(@NonNull com.android.extensions.xr.node.InputEvent.HitInfo info) {
+ this.info = info;
+ }
+
+ @Override
+ public int getSubspaceImpressNodeId() {
+ return info.getSubspaceImpressNodeId();
+ }
+
+ @Override
+ @NonNull
+ public Node getInputNode() {
+ return NodeTypeConverter.toLibrary(info.getInputNode());
+ }
+
+ @Override
+ @Nullable
+ public Vec3 getHitPosition() {
+ com.android.extensions.xr.node.Vec3 position = info.getHitPosition();
+ return (position == null) ? null : new Vec3(position.x, position.y, position.z);
+ }
+
+ @Override
+ @NonNull
+ public Mat4f getTransform() {
+ float[] transform = info.getTransform().getFlattenedMatrix();
+ return new Mat4f(transform);
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/Mat4f.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/Mat4f.java
new file mode 100644
index 0000000..fe551af
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/Mat4f.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.node;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/** 4x4 matrix. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class Mat4f {
+ public Mat4f(@NonNull float[] m) {
+ for (int i = 0; i < 4; i++) {
+ for (int j = 0; j < 4; j++) {
+ this.m[i][j] = m[i * 4 + j];
+ }
+ }
+ }
+
+ /**
+ * The matrix data.
+ *
+ * <p>The matrix is stored in column-major order, i.e. the first row is the first column.
+ */
+ @NonNull public float[][] m = new float[4][4];
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/Node.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/Node.java
new file mode 100644
index 0000000..932fd85
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/Node.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.node;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.os.Parcelable;
+import android.view.AttachedSurfaceControl;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.xr.extensions.Consumer;
+
+import java.io.Closeable;
+import java.lang.annotation.Retention;
+import java.util.concurrent.Executor;
+
+/**
+ * Handle to a node in the SpaceFlinger scene graph that can also host a 2D Panel or 3D subspace.
+ *
+ * <p>A Node by itself does not have any visual representation. It merely defines a local space in
+ * its parent space. However, a node can also host a single 2D panel or 3D subspace. Once an element
+ * is hosted, the node must be attached to the rest of scene graph hierarchy for the element become
+ * visible and appear on-screen.
+ *
+ * <p>Note that {@link Node} uses a right-hand coordinate system, i.e. +X points to the right, +Y
+ * up, and +Z points towards the camera.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface Node extends Parcelable {
+ /**
+ * Begins listening for 6DOF input events on this Node, and any descendant Nodes that do not
+ * have their own event listener set. The event listener is called on the provided Executor.
+ * Calling this method replaces any existing event listener for this node.
+ */
+ void listenForInput(@NonNull Consumer<InputEvent> listener, @NonNull Executor executor);
+
+ /** Removes the listener for 6DOF input events from this Node. */
+ void stopListeningForInput();
+
+ /**
+ * Sets the focus target for non-pointer input (eg, keyboard events) when this Node is clicked.
+ * The new target is the focusTarget's underlying View Root.
+ */
+ void setNonPointerFocusTarget(@NonNull AttachedSurfaceControl focusTarget);
+
+ /** Pointer capture states. */
+ // clang-format off
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(
+ value = {
+ POINTER_CAPTURE_STATE_PAUSED,
+ POINTER_CAPTURE_STATE_ACTIVE,
+ POINTER_CAPTURE_STATE_STOPPED,
+ })
+ @Retention(SOURCE)
+ public @interface PointerCaptureState {}
+
+ // clang-format on
+ int POINTER_CAPTURE_STATE_PAUSED = 0;
+ int POINTER_CAPTURE_STATE_ACTIVE = 1;
+ int POINTER_CAPTURE_STATE_STOPPED = 2;
+
+ /**
+ * Requests pointer capture. All XR input events that hit this node or any of its children are
+ * delivered as normal; any other input events that would otherwise be dispatched elsewhere will
+ * instead be delivered to the input queue of this node (without hit info).
+ *
+ * <p>The stateCallback is called immediately with the current state of this pointer capture.
+ * Whenever this node is visible and a descendant of a task that is not bounded (is in FSM or
+ * overlay space), pointer capture will be active; otherwise it will be paused.
+ *
+ * <p>If pointer capture is explicitly stopped by a new call to requestPointerCapture() on the
+ * same node, or by a call to stopPointerCapture(), POINTER_CAPTURE_STATE_STOPPED is passed (and
+ * the stateCallback will not be called subsequently; also, the app can be sure that no more
+ * captured pointer events will be delivered based on that request). This also occurs if the
+ * node is destroyed without explicitly stopping pointer capture, or if a new call to
+ * requestPointerCapture() is made on the same node without stopping the previous request.
+ *
+ * <p>If there are multiple pointer capture requests (eg from other apps) that could be active
+ * at the same time, the most recently requested one is activated; all other requests stay
+ * paused.
+ *
+ * <p>There can only be a single request per Node. If a new requestPointerCapture() call is made
+ * on the same node without stopping the previous pointer capture request, the previous request
+ * is automatically stopped.
+ *
+ * @param stateCallback a callback that will be called when pointer capture state changes.
+ * @param executor the executor the callback will be called on.
+ */
+ void requestPointerCapture(
+ @NonNull Consumer</* @PointerCaptureState */ Integer> stateCallback,
+ @NonNull Executor executor);
+
+ /**
+ * Disables previously-requested pointer capture on this node. The stateCallback callback will
+ * be called with POINTER_CAPTURE_STOPPED.
+ */
+ void stopPointerCapture();
+
+ /**
+ * Subscribes to the transform of this node, relative to the OpenXR reference space used as
+ * world space for the shared scene. See {@code XrExtensions.getOpenXrWorldSpaceType()}. The
+ * provided matrix transforms a point in this node's local coordinate system into a point in
+ * world space coordinates. For example, {@code NodeTransform.getTransform()} * (0, 0, 0, 1) is
+ * the position of this node in world space. The first non-null transform will be returned
+ * immediately after the subscription set-up is complete. Note that the returned closeable must
+ * be closed by calling {@code close()} to prevent wasting system resources associated with the
+ * subscription.
+ *
+ * @param transformCallback a callback that will be called when this node's transform changes.
+ * @param executor the executor the callback will be called on.
+ * @return a Closeable that must be used to cancel the subscription by calling {@code close()}.
+ */
+ @NonNull
+ Closeable subscribeToTransform(
+ @NonNull Consumer<NodeTransform> transformCallback, @NonNull Executor executor);
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/NodeImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/NodeImpl.java
new file mode 100644
index 0000000..6829768
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/NodeImpl.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.node;
+
+import static java.util.Objects.requireNonNull;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.AttachedSurfaceControl;
+
+import androidx.annotation.NonNull;
+import androidx.xr.extensions.Consumer;
+
+import java.io.Closeable;
+import java.util.concurrent.Executor;
+
+class NodeImpl implements Node {
+ @NonNull final com.android.extensions.xr.node.Node mNode;
+
+ NodeImpl(@NonNull com.android.extensions.xr.node.Node node) {
+ requireNonNull(node);
+ mNode = node;
+ }
+
+ @Override
+ public void listenForInput(@NonNull Consumer<InputEvent> listener, @NonNull Executor executor) {
+ mNode.listenForInput((event) -> listener.accept(new InputEventImpl(event)), executor);
+ }
+
+ @Override
+ public void stopListeningForInput() {
+ mNode.stopListeningForInput();
+ }
+
+ @Override
+ public void setNonPointerFocusTarget(@NonNull AttachedSurfaceControl focusTarget) {
+ mNode.setNonPointerFocusTarget(focusTarget);
+ }
+
+ @Override
+ public void requestPointerCapture(
+ @NonNull Consumer</* @PointerCaptureState */ Integer> stateCallback,
+ @NonNull Executor executor) {
+ mNode.requestPointerCapture((state) -> stateCallback.accept(state), executor);
+ }
+
+ @Override
+ public void stopPointerCapture() {
+ mNode.stopPointerCapture();
+ }
+
+ @Override
+ @NonNull
+ public Closeable subscribeToTransform(
+ @NonNull Consumer<NodeTransform> transformCallback, @NonNull Executor executor) {
+ return mNode.subscribeToTransform(
+ (transform) -> {
+ transformCallback.accept(new NodeTransformImpl(transform));
+ },
+ executor);
+ }
+
+ @Override
+ public int describeContents() {
+ return mNode.describeContents();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeParcelable(mNode, flags);
+ }
+
+ @Override
+ public String toString() {
+ return mNode.toString();
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) return true;
+ if (object == null) return false;
+
+ if (object instanceof NodeImpl) {
+ return this.mNode.equals(((NodeImpl) object).mNode);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mNode.hashCode();
+ }
+
+ public static final Parcelable.Creator<NodeImpl> CREATOR =
+ new Parcelable.Creator<NodeImpl>() {
+ @Override
+ public NodeImpl createFromParcel(Parcel in) {
+ return new NodeImpl(
+ in.readParcelable(
+ com.android.extensions.xr.node.Node.class.getClassLoader()));
+ }
+
+ @Override
+ public NodeImpl[] newArray(int size) {
+ return new NodeImpl[size];
+ }
+ };
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/NodeTransaction.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/NodeTransaction.java
new file mode 100644
index 0000000..f72074c
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/NodeTransaction.java
@@ -0,0 +1,492 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.node;
+
+import static androidx.xr.extensions.XrExtensions.IMAGE_TOO_OLD;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost.SurfacePackage;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.xr.extensions.asset.EnvironmentToken;
+import androidx.xr.extensions.asset.GltfAnimation;
+import androidx.xr.extensions.asset.GltfModelToken;
+import androidx.xr.extensions.asset.SceneToken;
+import androidx.xr.extensions.passthrough.PassthroughState;
+import androidx.xr.extensions.subspace.Subspace;
+
+import java.io.Closeable;
+import java.lang.annotation.Retention;
+
+/**
+ * An atomic set of changes to apply to a set of {@link Node}s.
+ *
+ * <p>Note that {@link Node} uses a right-hand coordinate system, i.e. +X points to the right, +Y
+ * up, and +Z points towards the camera.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface NodeTransaction extends Closeable {
+ /**
+ * Sets a name for the node that is used to it in `adb dumpsys cpm` output log.
+ *
+ * <p>While the name does not have to be globally unique, it is recommended to set a unique name
+ * for each node for ease of debugging.
+ *
+ * @param node The node to be updated.
+ * @param name The debug name of the node.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction setName(@NonNull Node node, @NonNull String name) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Sets the parent of this node to the given node.
+ *
+ * <p>This method detaches the node from its current branch and moves into the new parent's
+ * hierarchy (if any). If parent parameter is `null`, the node will be orphaned and removed from
+ * the rendering tree until it is reattached to another node that is in the root hierarchy.
+ *
+ * @param node The node to be updated.
+ * @param parent The new parent of the node or `null` if the node is to be removed from the
+ * rendering tree.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction setParent(@NonNull Node node, @Nullable Node parent) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Sets the position of the node in the local coordinate space (parent space).
+ *
+ * @param node The node to be updated.
+ * @param x The 'x' distance in meters from parent's origin.
+ * @param y The 'y' distance in meters from parent's origin.
+ * @param z The 'z' distance in meters from parent's origin.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction setPosition(@NonNull Node node, float x, float y, float z) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Rotates the node by the quaternion specified by x, y, z, and w components in the local
+ * coordinate space.
+ *
+ * @param node The node to be updated.
+ * @param x The x component of the quaternion.
+ * @param y The y component of the quaternion.
+ * @param z The z component of the quaternion.
+ * @param w The w component of the quaternion.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction setOrientation(
+ @NonNull Node node, float x, float y, float z, float w) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Scales the node along the x, y, and z axis in the local coordinate space.
+ *
+ * <p>For 2D panels, this method scales the panel in the world, increasing its visual size
+ * without changing the buffer size. It will not trigger a relayout and will not affect its
+ * enclosing view's layout configuration.
+ *
+ * @param node The node to be updated.
+ * @param sx The scaling factor along the x-axis.
+ * @param sy The scaling factor along the y-axis.
+ * @param sz The scaling factor along the z-axis.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction setScale(@NonNull Node node, float sx, float sy, float sz) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Sets the opacity of the node's content to a value between [0..1].
+ *
+ * @param node The node to be updated.
+ * @param value The new opacity amount in range of [0..1].
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction setAlpha(@NonNull Node node, float value) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Changes the visibility of the node and its content.
+ *
+ * @param node The node to be updated.
+ * @param isVisible Whether the node is visible.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction setVisibility(@NonNull Node node, boolean isVisible) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Configures the node to host and control the given surface data.
+ *
+ * <p>Passing a 'null' for surfaceControl parameter will disassociate it from the node, so the
+ * same node can be used to host another surface or volume data.
+ *
+ * @param node The node to be updated.
+ * @param surfaceControl Handle to an on-screen surface managed by the system compositor, or
+ * 'null' to disassociate the currently hosted surface from the node.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction setSurfaceControl(
+ @Nullable Node node, @NonNull SurfaceControl surfaceControl) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Configures the node to host and control the given surface data.
+ *
+ * <p>This method is similar to {@link #setSurfaceControl(Node, SurfaceControl)} and is provided
+ * for convenience.
+ *
+ * @param node The node to be updated.
+ * @param surfacePackage The package that contains the {@link SurfaceControl}, or 'null' to
+ * disassociate the currently hosted surface from the node.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction setSurfacePackage(
+ @Nullable Node node, @NonNull SurfacePackage surfacePackage) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Crops the 2D buffer of the Surface hosted by this node to match the given bounds in pixels.
+ *
+ * <p>This method only applies to nodes that host a {@link SurfaceControl} set by {@link
+ * #setSurfaceControl}.
+ *
+ * @param surfaceControl The on-screen surface.
+ * @param widthPx The width of the surface in pixels.
+ * @param heightPx The height of the surface in pixels.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction setWindowBounds(
+ @NonNull SurfaceControl surfaceControl, int widthPx, int heightPx) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Crops the 2D buffer of the Surface hosted by this node to match the given bounds in pixels.
+ *
+ * <p>This method is similar to {@link #setWindowBounds(SurfaceControl, int, int)} and is
+ * provided for convenience.
+ *
+ * @param surfacePackage The package that contains the {@link SurfaceControl}.
+ * @param widthPx The width of the surface in pixels.
+ * @param heightPx The height of the surface in pixels.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ @NonNull
+ default NodeTransaction setWindowBounds(
+ @NonNull SurfacePackage surfacePackage, int widthPx, int heightPx) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Curves the XY plane of the node around the y-axis and towards the positive z-axis.
+ *
+ * <p>This method essentially curves the x-axis of the node, moving and rotating its children to
+ * align with the new x-axis shape. It will also curve the children's x-axes in a similar
+ * manner.
+ *
+ * <p>If this node is hosting a 2D panel, setting a curvature will bend the panel along the Y
+ * axis, projecting it onto a cylinder defined by the given radius.
+ *
+ * <p>To remove the curvature, set the radius to 0.
+ *
+ * @param node The node to be updated.
+ * @param curvature A positive value equivalent to 1/radius, where 'radius' represents the
+ * radial distance of the polar coordinate system that is used to curve the x-axis. Setting
+ * this value to 0 will straighten the axis and remove its curvature.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ * @deprecated Use Split Engine to create a curved panel.
+ */
+ @Deprecated
+ default @NonNull NodeTransaction setCurvature(@NonNull Node node, float curvature) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Sets the resolution of 2D surfaces under this node.
+ *
+ * <p>The sizes of 2D surfaces under this node will be set according to their 2D pixel
+ * dimensions and the pixelsPerMeter value. The pixelsPerMeter value is propagated to child
+ * nodes.
+ *
+ * @param node The node to be updated.
+ * @param pixelsPerMeter The number of pixels per meter to use when sizing 2D surfaces.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction setPixelResolution(@NonNull Node node, float pixelsPerMeter) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ // clang-format off
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(
+ flag = true,
+ value = {
+ X_POSITION_IN_PIXELS,
+ Y_POSITION_IN_PIXELS,
+ Z_POSITION_IN_PIXELS,
+ POSITION_FROM_PARENT_TOP_LEFT,
+ })
+ @Retention(SOURCE)
+ public @interface PixelPositionFlags {}
+
+ // clang-format on
+
+ int X_POSITION_IN_PIXELS = 0x01;
+ int Y_POSITION_IN_PIXELS = 0x02;
+ int Z_POSITION_IN_PIXELS = 0x04;
+ // POSITION_FROM_PARENT_TOP_LEFT makes it so the node's position is relative to the top left
+ // corner of the parent node, instead of the center. Only relevant if the parent node has a size
+ // (currently this is only true for surface tracking nodes).
+ int POSITION_FROM_PARENT_TOP_LEFT = 0x40;
+
+ /**
+ * Sets whether position is interpreted in meters or in pixels for each dimension.
+ *
+ * <p>The sizes of 2D surfaces under this node will be set according to their 2D pixel
+ * dimensions and the pixelsPerMeter value. The pixelsPerMeter value is propagated to child
+ * nodes.
+ *
+ * @param node The node to be updated.
+ * @param pixelPositionFlags Flags indicating which dimensins of the local position of the node
+ * should be interpreted as pixel values (as opposed to the default meters).
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction setPixelPositioning(
+ @NonNull Node node, @PixelPositionFlags int pixelPositionFlags) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Renders a previously loaded glTF model.
+ *
+ * <p>The token must belong to a previously loaded glTF model that is currently cached in the
+ * SpaceFlinger.
+ *
+ * @param node The node to be updated.
+ * @param gltfModelToken The token of a glTF model that was previously loaded.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ default @NonNull NodeTransaction setGltfModel(
+ @NonNull Node node, @NonNull GltfModelToken gltfModelToken) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Renders a previously loaded environment.
+ *
+ * <p>The token must belong to a previously loaded environment that is currently cached in the
+ * SpaceFlinger.
+ *
+ * @param node The node to be updated.
+ * @param environmentToken The token of an environment that was previously loaded.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ default @NonNull NodeTransaction setEnvironment(
+ @NonNull Node node, @NonNull EnvironmentToken environmentToken) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Renders a previously loaded Impress scene.
+ *
+ * <p>The token must belong to a previously loaded Impress scene that is currently cached in the
+ * SpaceFlinger.
+ *
+ * @param node The node to be updated.
+ * @param sceneToken The token of an Impress scene that was previously loaded.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ default @NonNull NodeTransaction setImpressScene(
+ @NonNull Node node, @NonNull SceneToken sceneToken) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Animates a previously loaded glTF model.
+ *
+ * @param node The node to be updated.
+ * @param gltfAnimationName The name of the glTF animation.
+ * @param gltfAnimationState The {@link GltfAnimation.State} state of the glTF animation.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ default @NonNull NodeTransaction setGltfAnimation(
+ @NonNull Node node,
+ @NonNull String gltfAnimationName,
+ @NonNull GltfAnimation.State gltfAnimationState) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Sets the transform of the node on a per-frame basis from a previously created anchor.
+ *
+ * <p>The client who created the anchor and provided the ID will always remain the owner of the
+ * anchor.
+ *
+ * <p>Modifying the transform of the node will only be applied if or when the anchor is no
+ * longer linked to the node, or if the anchor is no longer locatable.
+ *
+ * <p>A node can be unlinked from an anchor by setting the ID to null. Note that this does not
+ * destroy the actual anchor.
+ *
+ * @param node The node to be updated.
+ * @param anchorId The ID of a previously created anchor.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction setAnchorId(@NonNull Node node, @Nullable IBinder anchorId) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Sets a subspace to be used.
+ *
+ * @param node The node to be updated.
+ * @param subspace The previously created subspace to be associated with the node.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction setSubspace(@NonNull Node node, @NonNull Subspace subspace) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Updates the passthrough state.
+ *
+ * @param node The node to be updated.
+ * @param passthroughOpacity The opacity of the passthrough layer where 0.0 means no passthrough
+ * and 1.0 means full passthrough.
+ * @param passthroughMode The {@link PassthroughState.Mode} mode that the passthrough will use.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction setPassthroughState(
+ @NonNull Node node,
+ float passthroughOpacity,
+ @PassthroughState.Mode int passthroughMode) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Enables reform UX for a node.
+ *
+ * @param node The node to be updated.
+ * @param options Configuration options for the reform UX.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction enableReform(
+ @NonNull Node node, @NonNull ReformOptions options) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Updates the size of the reform UX.
+ *
+ * @param node The node to be updated.
+ * @param reformSize The new size in meters that should be used to lay out the reform UX.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction setReformSize(@NonNull Node node, @NonNull Vec3 reformSize) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Disables reform UX for a node.
+ *
+ * @param node The node to be updated.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction disableReform(@NonNull Node node) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Sets the corner radius for 2D surfaces under this node.
+ *
+ * <p>The corner radius is propagated to child nodes.
+ *
+ * @param node The node to be updated.
+ * @param cornerRadius The corner radius for 2D surfaces under this node, in meters.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction setCornerRadius(@NonNull Node node, float cornerRadius) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Removes the corner radius from this node.
+ *
+ * @param node The node to be updated.
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction removeCornerRadius(@NonNull Node node) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Merges the given transaction into this one so that they can be submitted together to the
+ * system. All of the changes in the other transaction are moved into this one; the other
+ * transaction is left in an empty state.
+ *
+ * @return The reference to this {@link NodeTransaction} object that is currently being updated.
+ */
+ default @NonNull NodeTransaction merge(@NonNull NodeTransaction other) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Submits the queued transactions to backend.
+ *
+ * <p>This method will clear the existing transaction state so the same transaction object can
+ * be used for the next set of updates.
+ */
+ default void apply() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Closes and releases the native transaction object without applying it.
+ *
+ * <p>Note that a closed transaction cannot be used again.
+ */
+ @Override
+ default void close() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/NodeTransactionImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/NodeTransactionImpl.java
new file mode 100644
index 0000000..6d72f74
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/NodeTransactionImpl.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.node;
+
+import static java.util.Objects.requireNonNull;
+
+import android.os.Build;
+import android.os.IBinder;
+import android.util.Log;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost.SurfacePackage;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.xr.extensions.asset.EnvironmentToken;
+import androidx.xr.extensions.asset.GltfAnimation;
+import androidx.xr.extensions.asset.GltfModelToken;
+import androidx.xr.extensions.asset.SceneToken;
+import androidx.xr.extensions.asset.TokenConverter;
+import androidx.xr.extensions.passthrough.PassthroughState;
+import androidx.xr.extensions.subspace.Subspace;
+import androidx.xr.extensions.subspace.SubspaceTypeConverter;
+
+class NodeTransactionImpl implements NodeTransaction {
+ @NonNull final com.android.extensions.xr.node.NodeTransaction transaction;
+
+ NodeTransactionImpl(@NonNull com.android.extensions.xr.node.NodeTransaction transaction) {
+ requireNonNull(transaction);
+ this.transaction = transaction;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setName(@NonNull Node node, @NonNull String name) {
+ transaction.setName(toFramework(node), name);
+ return this;
+ }
+
+ @Nullable
+ private com.android.extensions.xr.node.Node toFramework(@Nullable Node node) {
+ return NodeTypeConverter.toFramework(node);
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setParent(@NonNull Node node, @Nullable Node parent) {
+ transaction.setParent(toFramework(node), toFramework(parent));
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setPosition(@NonNull Node node, float x, float y, float z) {
+ transaction.setPosition(toFramework(node), x, y, z);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setOrientation(@NonNull Node node, float x, float y, float z, float w) {
+ transaction.setOrientation(toFramework(node), x, y, z, w);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setScale(@NonNull Node node, float sx, float sy, float sz) {
+ transaction.setScale(toFramework(node), sx, sy, sz);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setAlpha(@NonNull Node node, float value) {
+ transaction.setAlpha(toFramework(node), value);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setVisibility(@NonNull Node node, boolean isVisible) {
+ transaction.setVisibility(toFramework(node), isVisible);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setSurfaceControl(
+ @Nullable Node node, @NonNull SurfaceControl surfaceControl) {
+ transaction.setSurfaceControl(toFramework(node), surfaceControl);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setSurfacePackage(
+ @Nullable Node node, @NonNull SurfacePackage surfacePackage) {
+ // This method has been deprecated in the platform side.
+ if (Build.VERSION.SDK_INT >= 34) {
+ requireNonNull(surfacePackage);
+ return setSurfaceControl(node, surfacePackage.getSurfaceControl());
+ } else {
+ Log.e("NodeTransaction", "setSurfacePackage is not supported in SDK lower then 34");
+ return this;
+ }
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setWindowBounds(
+ @NonNull SurfaceControl surfaceControl, int widthPx, int heightPx) {
+ transaction.setWindowBounds(surfaceControl, widthPx, heightPx);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setWindowBounds(
+ @NonNull SurfacePackage surfacePackage, int widthPx, int heightPx) {
+ // This method has been deprecated in the platform side.
+ if (Build.VERSION.SDK_INT >= 34) {
+ requireNonNull(surfacePackage);
+ return setWindowBounds(surfacePackage.getSurfaceControl(), widthPx, heightPx);
+ } else {
+ Log.e("NodeTransaction", "setSurfacePackage is not supported in SDK lower then 34");
+ return this;
+ }
+ }
+
+ @Override
+ @NonNull
+ @Deprecated
+ public NodeTransaction setCurvature(@NonNull Node node, float radius) {
+ transaction.setCurvature(toFramework(node), radius);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setPixelResolution(@NonNull Node node, float pixelsPerMeter) {
+ transaction.setPixelResolution(toFramework(node), pixelsPerMeter);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setPixelPositioning(
+ @NonNull Node node, @PixelPositionFlags int pixelPositionFlags) {
+ transaction.setPixelPositioning(toFramework(node), pixelPositionFlags);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ @Deprecated
+ public NodeTransaction setGltfModel(
+ @NonNull Node node, @NonNull GltfModelToken gltfModelToken) {
+ transaction.setGltfModel(toFramework(node), TokenConverter.toFramework(gltfModelToken));
+ return this;
+ }
+
+ @Override
+ @NonNull
+ @Deprecated
+ public NodeTransaction setEnvironment(
+ @NonNull Node node, @NonNull EnvironmentToken environmentToken) {
+ transaction.setEnvironment(toFramework(node), TokenConverter.toFramework(environmentToken));
+ return this;
+ }
+
+ @Override
+ @NonNull
+ @Deprecated
+ public NodeTransaction setImpressScene(@NonNull Node node, @NonNull SceneToken sceneToken) {
+ transaction.setImpressScene(toFramework(node), TokenConverter.toFramework(sceneToken));
+ return this;
+ }
+
+ @Override
+ @NonNull
+ @Deprecated
+ public NodeTransaction setGltfAnimation(
+ @NonNull Node node,
+ @NonNull String gltfAnimationName,
+ @NonNull GltfAnimation.State gltfAnimationState) {
+ transaction.setGltfAnimation(
+ toFramework(node),
+ gltfAnimationName,
+ TokenConverter.toFramework(gltfAnimationState));
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setAnchorId(@NonNull Node node, @Nullable IBinder anchorId) {
+ transaction.setAnchorId(toFramework(node), anchorId);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setSubspace(@NonNull Node node, @NonNull Subspace subspace) {
+ transaction.setSubspace(toFramework(node), SubspaceTypeConverter.toFramework(subspace));
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setPassthroughState(
+ @NonNull Node node,
+ float passthroughOpacity,
+ @PassthroughState.Mode int passthroughMode) {
+ transaction.setPassthroughState(toFramework(node), passthroughOpacity, passthroughMode);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction enableReform(@NonNull Node node, @NonNull ReformOptions options) {
+ transaction.enableReform(toFramework(node), NodeTypeConverter.toFramework(options));
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setReformSize(@NonNull Node node, @NonNull Vec3 reformSize) {
+ transaction.setReformSize(
+ toFramework(node),
+ new com.android.extensions.xr.node.Vec3(reformSize.x, reformSize.y, reformSize.z));
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction disableReform(@NonNull Node node) {
+ transaction.disableReform(toFramework(node));
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction setCornerRadius(@NonNull Node node, float cornerRadius) {
+ transaction.setCornerRadius(toFramework(node), cornerRadius);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction removeCornerRadius(@NonNull Node node) {
+ transaction.removeCornerRadius(toFramework(node));
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public NodeTransaction merge(@NonNull NodeTransaction transaction) {
+ this.transaction.merge(((NodeTransactionImpl) transaction).transaction);
+ return this;
+ }
+
+ @Override
+ public void apply() {
+ transaction.apply();
+ }
+
+ @Override
+ public void close() {
+ transaction.close();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/NodeTransform.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/NodeTransform.java
new file mode 100644
index 0000000..22fb3fa
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/NodeTransform.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.node;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/** interface containing the Node transform */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface NodeTransform {
+ /**
+ * Get the transformation matrix associated with the node.
+ *
+ * <p>The provided matrix transforms a point in this node's local coordinate system into a point
+ * in world space coordinates. For example, {@code NodeTransform.getTransform()} * (0, 0, 0, 1)
+ * is the position of this node in world space. The first non-null transform will be returned
+ * immediately after the subscription set-up is complete.
+ *
+ * @return A transformation matrix {@link Mat4f} containing the current transformation matrix of
+ * this node.
+ */
+ @NonNull
+ Mat4f getTransform();
+
+ /**
+ * Get the timestamp at which the transformation matrix was recorded.
+ *
+ * <p>The time the record happened, in the android.os.SystemClock#uptimeNanos time base.
+ *
+ * @return A timestamp at which the transformation matrix was recorded.
+ */
+ long getTimestamp();
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/NodeTransformImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/NodeTransformImpl.java
new file mode 100644
index 0000000..37089a2
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/NodeTransformImpl.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.node;
+
+import androidx.annotation.NonNull;
+
+class NodeTransformImpl implements NodeTransform {
+ @NonNull private final com.android.extensions.xr.node.NodeTransform mTransform;
+
+ NodeTransformImpl(@NonNull com.android.extensions.xr.node.NodeTransform transform) {
+ mTransform = transform;
+ }
+
+ @Override
+ @NonNull
+ public Mat4f getTransform() {
+ return new Mat4f(mTransform.getTransform().getFlattenedMatrix());
+ }
+
+ @Override
+ public long getTimestamp() {
+ return mTransform.getTimestamp();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/NodeTypeConverter.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/NodeTypeConverter.java
new file mode 100644
index 0000000..952340e
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/NodeTypeConverter.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.node;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+/** This class is able to convert library types into platform types. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class NodeTypeConverter {
+
+ private NodeTypeConverter() {}
+
+ /**
+ * Converts a {@link Node} to a framework type.
+ *
+ * @param node The {@link Node} to convert.
+ * @return The framework type of the {@link com.android.extensions.xr.node.Node}.
+ */
+ @Nullable
+ public static com.android.extensions.xr.node.Node toFramework(@Nullable Node node) {
+ if (node == null) {
+ return null;
+ }
+
+ return ((NodeImpl) node).mNode;
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.node.Node} to a library type.
+ *
+ * @param node The {@link com.android.extensions.xr.node.Node} to convert.
+ * @return The library type of the {@link Node}.
+ */
+ @Nullable
+ public static Node toLibrary(@Nullable com.android.extensions.xr.node.Node node) {
+ if (node == null) {
+ return null;
+ }
+
+ return new NodeImpl(node);
+ }
+
+ /**
+ * Converts a {@link Vec3} to a framework type.
+ *
+ * @param vec The {@link Vec3} to convert.
+ * @return The framework type of the {@link com.android.extensions.xr.node.Vec3}.
+ */
+ @NonNull
+ public static com.android.extensions.xr.node.Vec3 toFramework(@NonNull Vec3 vec) {
+ requireNonNull(vec);
+ return new com.android.extensions.xr.node.Vec3(vec.x, vec.y, vec.z);
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.node.Vec3} to a library type.
+ *
+ * @param vec The {@link com.android.extensions.xr.node.Vec3} to convert.
+ * @return The library type of the {@link Vec3}.
+ */
+ @NonNull
+ public static Vec3 toLibrary(@NonNull com.android.extensions.xr.node.Vec3 vec) {
+ requireNonNull(vec);
+ return new Vec3(vec.x, vec.y, vec.z);
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.node.NodeTransaction} to a library type.
+ *
+ * @param transform The {@link com.android.extensions.xr.node.NodeTransaction} to convert.
+ * @return The framework type of the {@link NodeTransaction}.
+ */
+ @NonNull
+ public static NodeTransaction toLibrary(
+ @NonNull com.android.extensions.xr.node.NodeTransaction transform) {
+ requireNonNull(transform);
+ return new NodeTransactionImpl(transform);
+ }
+
+ /**
+ * Converts a {@link Quatf} to a framework type.
+ *
+ * @param value The {@link Quatf} to convert.
+ * @return The framework type of the {@link com.android.extensions.xr.node.Quatf}.
+ */
+ @NonNull
+ public static Quatf toLibrary(@NonNull com.android.extensions.xr.node.Quatf value) {
+ requireNonNull(value);
+ return new Quatf(value.x, value.y, value.z, value.w);
+ }
+
+ /**
+ * Converts a {@link ReformOptions} to a framework type.
+ *
+ * @param options The {@link ReformOptions} to convert.
+ * @return The framework type of the {@link com.android.extensions.xr.node.ReformOptions}.
+ */
+ @NonNull
+ public static com.android.extensions.xr.node.ReformOptions toFramework(
+ @NonNull ReformOptions options) {
+ requireNonNull(options);
+ return ((ReformOptionsImpl) options).mOptions;
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.node.ReformOptions} to a library type.
+ *
+ * @param options The {@link com.android.extensions.xr.node.ReformOptions} to convert.
+ * @return The library type of the {@link ReformOptions}.
+ */
+ @NonNull
+ public static ReformOptions toLibrary(
+ @NonNull com.android.extensions.xr.node.ReformOptions options) {
+ requireNonNull(options);
+ return new ReformOptionsImpl(options);
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.node.ReformEvent} to a library type.
+ *
+ * @param event The {@linkcom.android.extensions.xr.node.ReformEvent} to convert.
+ * @return The library type of the {@link ReformEvent}.
+ */
+ @NonNull
+ public static ReformEvent toLibrary(@NonNull com.android.extensions.xr.node.ReformEvent event) {
+ requireNonNull(event);
+ return new ReformOptionsImpl.ReformEventImpl(event);
+ }
+
+ /**
+ * Converts a {@link ReformEvent} to a framework type.
+ *
+ * @param event The {@link ReformEvent} to convert.
+ * @return The framework type of the {@link com.android.extensions.xr.node.ReformEvent}.
+ */
+ @NonNull
+ public static com.android.extensions.xr.node.ReformEvent toFramework(
+ @NonNull ReformEvent event) {
+ requireNonNull(event);
+ return ((ReformOptionsImpl.ReformEventImpl) event).mEvent;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/Quatf.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/Quatf.java
new file mode 100644
index 0000000..d215ada
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/Quatf.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.node;
+
+import androidx.annotation.RestrictTo;
+
+/** Quaternion. q = w + xi + yj + zk */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class Quatf {
+ public Quatf(float x, float y, float z, float w) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.w = w;
+ }
+
+ public float x;
+ public float y;
+ public float z;
+ public float w;
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/ReformEvent.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/ReformEvent.java
new file mode 100644
index 0000000..9491952
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/ReformEvent.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.node;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+
+/** A reform (move / resize) event. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface ReformEvent {
+ // clang-format off
+ /** The type of reform action this event is referring to. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(
+ value = {
+ REFORM_TYPE_UNKNOWN,
+ REFORM_TYPE_MOVE,
+ REFORM_TYPE_RESIZE,
+ })
+ @Retention(SOURCE)
+ @interface ReformType {}
+
+ // clang-format on
+
+ int REFORM_TYPE_UNKNOWN = 0;
+ int REFORM_TYPE_MOVE = 1;
+ int REFORM_TYPE_RESIZE = 2;
+
+ /** Returns the type of reform action this event is referring to. */
+ @ReformType
+ int getType();
+
+ // clang-format off
+ /** The state of the reform action. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(
+ value = {
+ REFORM_STATE_UNKNOWN,
+ REFORM_STATE_START,
+ REFORM_STATE_ONGOING,
+ REFORM_STATE_END,
+ })
+ @Retention(SOURCE)
+ @interface ReformState {}
+
+ // clang-format on
+
+ int REFORM_STATE_UNKNOWN = 0;
+ int REFORM_STATE_START = 1;
+ int REFORM_STATE_ONGOING = 2;
+ int REFORM_STATE_END = 3;
+
+ /** Returns the state of the reform action. */
+ @ReformState
+ int getState();
+
+ /** An identifier for this reform action. */
+ int getId();
+
+ /** The initial ray origin and direction, in task space. */
+ @NonNull
+ Vec3 getInitialRayOrigin();
+
+ /** The initial ray direction, in task space. */
+ @NonNull
+ Vec3 getInitialRayDirection();
+
+ /** The current ray origin and direction, in task space. */
+ @NonNull
+ Vec3 getCurrentRayOrigin();
+
+ /** The current ray direction, in task space. */
+ @NonNull
+ Vec3 getCurrentRayDirection();
+
+ /**
+ * For a move event, the proposed pose of the node, in task space (or relative to the parent
+ * node, if FLAG_POSE_RELATIVE_TO_PARENT was specified in the ReformOptions).
+ */
+ @NonNull
+ Vec3 getProposedPosition();
+
+ /**
+ * For a move event, the proposed orientation of the node, in task space (or relative to the
+ * parent node, if FLAG_POSE_RELATIVE_TO_PARENT was specified in the ReformOptions).
+ */
+ @NonNull
+ Quatf getProposedOrientation();
+
+ /** Scale will change with distance if ReformOptions.FLAG_SCALE_WITH_DISTANCE is set. */
+ @NonNull
+ Vec3 getProposedScale();
+
+ /**
+ * For a resize event, the proposed new size in meters. Note that in the initial implementation,
+ * the Z size may not be modified.
+ */
+ @NonNull
+ Vec3 getProposedSize();
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/ReformOptions.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/ReformOptions.java
new file mode 100644
index 0000000..b427598
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/ReformOptions.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.node;
+
+import static androidx.xr.extensions.XrExtensions.IMAGE_TOO_OLD;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.xr.extensions.Consumer;
+
+import java.lang.annotation.Retention;
+import java.util.concurrent.Executor;
+
+/**
+ * Configuration options for reform (move/resize) UX. To create a ReformOptions instance, call
+ * {@code XrExtensions.createReformOptions()}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface ReformOptions {
+ // clang-format off
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(
+ flag = true,
+ value = {ALLOW_MOVE, ALLOW_RESIZE})
+ @Retention(SOURCE)
+ @interface AllowedReformTypes {}
+
+ // clang-format on
+
+ int ALLOW_MOVE = 1;
+ int ALLOW_RESIZE = 2;
+
+ /** Which reform actions are enabled. */
+ @AllowedReformTypes
+ int getEnabledReform();
+
+ /** By default, only ALLOW_MOVE is enabled. */
+ @NonNull
+ ReformOptions setEnabledReform(@AllowedReformTypes int enabledReform);
+
+ // clang-format off
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(
+ flag = true,
+ value = {
+ FLAG_SCALE_WITH_DISTANCE,
+ FLAG_ALLOW_SYSTEM_MOVEMENT,
+ FLAG_POSE_RELATIVE_TO_PARENT
+ })
+ @Retention(SOURCE)
+ public @interface ReformFlags {}
+
+ // clang-format on
+
+ int FLAG_SCALE_WITH_DISTANCE = 1;
+ int FLAG_ALLOW_SYSTEM_MOVEMENT = 2;
+ int FLAG_POSE_RELATIVE_TO_PARENT = 4;
+
+ /** Behaviour flags. */
+ @ReformFlags
+ int getFlags();
+
+ /** By default, the flags are set to 0. */
+ @NonNull
+ ReformOptions setFlags(@ReformFlags int flags);
+
+ /**
+ * Current size of the content, in meters. This is the local size (does not include any scale
+ * factors)
+ */
+ @NonNull
+ Vec3 getCurrentSize();
+
+ /** By default, the current size is set to (1, 1, 1). */
+ @NonNull
+ ReformOptions setCurrentSize(@NonNull Vec3 currentSize);
+
+ /** Minimum size of the content, in meters. This is a local size. */
+ @NonNull
+ Vec3 getMinimumSize();
+
+ /** By default, the minimum size is set to (1, 1, 1). */
+ @NonNull
+ ReformOptions setMinimumSize(@NonNull Vec3 minimumSize);
+
+ /** Maximum size of the content, in meters. This is a local size. */
+ @NonNull
+ Vec3 getMaximumSize();
+
+ /** By default, the maximum size is set to (1, 1, 1). */
+ @NonNull
+ ReformOptions setMaximumSize(@NonNull Vec3 maximumSize);
+
+ /** The aspect ratio of the content on resizing. <= 0.0f when there are no preferences. */
+ float getFixedAspectRatio();
+
+ /**
+ * The aspect ratio determined by taking the panel's width over its height. An aspect ratio
+ * value less than 0 will be ignored. A value <= 0.0f means there are no preferences.
+ *
+ * <p>This method does not immediately resize the entity. The new aspect ratio will be applied
+ * the next time the user resizes the entity through the reform UI. During this resize
+ * operation, the entity's current area will be preserved.
+ *
+ * <p>If a different resizing behavior is desired, such as fixing the width and adjusting the
+ * height, the client can manually resize the entity to the preferred dimensions before calling
+ * this method. No automatic resizing will occur when using the reform UI then.
+ */
+ @NonNull
+ ReformOptions setFixedAspectRatio(float fixedAspectRatio);
+
+ /** Returns the current value of forceShowResizeOverlay. */
+ default boolean getForceShowResizeOverlay() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * If forceShowResizeOverlay is set to true, the resize overlay will always be show (until
+ * forceShowResizeOverlay is changed to false). This can be used by apps to implement their own
+ * resize affordances.
+ */
+ @NonNull
+ default ReformOptions setForceShowResizeOverlay(boolean forceShowResizeOverlay) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /** Returns the callback that will receive reform events. */
+ @NonNull
+ Consumer<ReformEvent> getEventCallback();
+
+ /** Sets the callback that will receive reform events. */
+ @NonNull
+ ReformOptions setEventCallback(@NonNull Consumer<ReformEvent> callback);
+
+ /** Returns the executor that events will be handled on. */
+ @NonNull
+ Executor getEventExecutor();
+
+ /** Sets the executor that events will be handled on. */
+ @NonNull
+ ReformOptions setEventExecutor(@NonNull Executor executor);
+
+ // clang-format off
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(value = {SCALE_WITH_DISTANCE_MODE_DEFAULT, SCALE_WITH_DISTANCE_MODE_DMM})
+ @Retention(SOURCE)
+ @interface ScaleWithDistanceMode {}
+
+ // clang-format on
+
+ // The values MUST be identical to the ScalingCurvePreset enum for Spaceflinger in
+ // vendor/google/ix/sysui/proto/components/freeform_positioning_state.proto.
+ int SCALE_WITH_DISTANCE_MODE_DMM = 2;
+ int SCALE_WITH_DISTANCE_MODE_DEFAULT = 3;
+
+ /** Returns the current value of scaleWithDistanceMode. */
+ default @ScaleWithDistanceMode int getScaleWithDistanceMode() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * If scaleWithDistanceMode is set, and FLAG_SCALE_WITH_DISTANCE is also in use, the scale the
+ * system suggests (or automatically applies when FLAG_ALLOW_SYSTEM_MOVEMENT is also in use)
+ * follows scaleWithDistanceMode:
+ *
+ * <p>SCALE_WITH_DISTANCE_MODE_DEFAULT: The panel scales in the same way as home space mode.
+ * SCALE_WITH_DISTANCE_MODE_DMM: The panel scales in a way that the user-perceived panel size
+ * never changes.
+ *
+ * <p>When FLAG_SCALE_WITH_DISTANCE is not in use, scaleWithDistanceMode is ignored.
+ */
+ @NonNull
+ default ReformOptions setScaleWithDistanceMode(
+ @ScaleWithDistanceMode int scaleWithDistanceMode) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/ReformOptionsImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/ReformOptionsImpl.java
new file mode 100644
index 0000000..25c796b
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/ReformOptionsImpl.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.node;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.NonNull;
+import androidx.xr.extensions.Consumer;
+
+import java.util.concurrent.Executor;
+
+class ReformOptionsImpl implements ReformOptions {
+ @NonNull final com.android.extensions.xr.node.ReformOptions mOptions;
+
+ ReformOptionsImpl(@NonNull com.android.extensions.xr.node.ReformOptions options) {
+ requireNonNull(options);
+ mOptions = options;
+ }
+
+ static class ReformEventImpl implements ReformEvent {
+ @NonNull final com.android.extensions.xr.node.ReformEvent mEvent;
+
+ ReformEventImpl(@NonNull com.android.extensions.xr.node.ReformEvent event) {
+ requireNonNull(event);
+ mEvent = event;
+ }
+
+ @Override
+ public int getType() {
+ return mEvent.getType();
+ }
+
+ @Override
+ public int getState() {
+ return mEvent.getState();
+ }
+
+ @Override
+ public int getId() {
+ return mEvent.getId();
+ }
+
+ @Override
+ @NonNull
+ public Vec3 getInitialRayOrigin() {
+ return NodeTypeConverter.toLibrary(mEvent.getInitialRayOrigin());
+ }
+
+ @Override
+ @NonNull
+ public Vec3 getInitialRayDirection() {
+ return NodeTypeConverter.toLibrary(mEvent.getInitialRayDirection());
+ }
+
+ @Override
+ @NonNull
+ public Vec3 getCurrentRayOrigin() {
+ return NodeTypeConverter.toLibrary(mEvent.getCurrentRayOrigin());
+ }
+
+ @Override
+ @NonNull
+ public Vec3 getCurrentRayDirection() {
+ return NodeTypeConverter.toLibrary(mEvent.getCurrentRayDirection());
+ }
+
+ @Override
+ @NonNull
+ public Vec3 getProposedPosition() {
+ return NodeTypeConverter.toLibrary(mEvent.getProposedPosition());
+ }
+
+ @Override
+ @NonNull
+ public Quatf getProposedOrientation() {
+ return NodeTypeConverter.toLibrary(mEvent.getProposedOrientation());
+ }
+
+ @Override
+ @NonNull
+ public Vec3 getProposedScale() {
+ return NodeTypeConverter.toLibrary(mEvent.getProposedScale());
+ }
+
+ @Override
+ @NonNull
+ public Vec3 getProposedSize() {
+ return NodeTypeConverter.toLibrary(mEvent.getProposedSize());
+ }
+ }
+
+ @Override
+ public int getEnabledReform() {
+ return mOptions.getEnabledReform();
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions setEnabledReform(int enabledReform) {
+ mOptions.setEnabledReform(enabledReform);
+ return this;
+ }
+
+ @Override
+ public int getFlags() {
+ return mOptions.getFlags();
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions setFlags(@ReformFlags int flags) {
+ mOptions.setFlags(flags);
+ return this;
+ }
+
+ @Override
+ public @NonNull Vec3 getCurrentSize() {
+ return NodeTypeConverter.toLibrary(mOptions.getCurrentSize());
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions setCurrentSize(@NonNull Vec3 currentSize) {
+ mOptions.setCurrentSize(NodeTypeConverter.toFramework(currentSize));
+ return this;
+ }
+
+ @Override
+ public @NonNull Vec3 getMinimumSize() {
+ return NodeTypeConverter.toLibrary(mOptions.getMinimumSize());
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions setMinimumSize(@NonNull Vec3 minimumSize) {
+ mOptions.setMinimumSize(NodeTypeConverter.toFramework(minimumSize));
+ return this;
+ }
+
+ @Override
+ public @NonNull Vec3 getMaximumSize() {
+ return NodeTypeConverter.toLibrary(mOptions.getMaximumSize());
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions setMaximumSize(@NonNull Vec3 maximumSize) {
+ mOptions.setMaximumSize(NodeTypeConverter.toFramework(maximumSize));
+ return this;
+ }
+
+ @Override
+ public float getFixedAspectRatio() {
+ return mOptions.getFixedAspectRatio();
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions setFixedAspectRatio(float fixedAspectRatio) {
+ mOptions.setFixedAspectRatio(fixedAspectRatio);
+ return this;
+ }
+
+ @Override
+ public boolean getForceShowResizeOverlay() {
+ return mOptions.getForceShowResizeOverlay();
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions setForceShowResizeOverlay(boolean forceShowResizeOverlay) {
+ mOptions.setForceShowResizeOverlay(forceShowResizeOverlay);
+ return this;
+ }
+
+ @Override
+ public @NonNull Consumer<ReformEvent> getEventCallback() {
+ return (event) -> {
+ mOptions.getEventCallback().accept(NodeTypeConverter.toFramework(event));
+ };
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions setEventCallback(@NonNull Consumer<ReformEvent> callback) {
+ mOptions.setEventCallback(
+ (event) -> {
+ callback.accept(NodeTypeConverter.toLibrary(event));
+ });
+ return this;
+ }
+
+ @Override
+ public @NonNull Executor getEventExecutor() {
+ return mOptions.getEventExecutor();
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions setEventExecutor(@NonNull Executor executor) {
+ mOptions.setEventExecutor(executor);
+ return this;
+ }
+
+ @Override
+ public @ScaleWithDistanceMode int getScaleWithDistanceMode() {
+ return mOptions.getScaleWithDistanceMode();
+ }
+
+ @Override
+ @NonNull
+ public ReformOptions setScaleWithDistanceMode(
+ @ScaleWithDistanceMode int scaleWithDistanceMode) {
+ mOptions.setScaleWithDistanceMode(scaleWithDistanceMode);
+ return this;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/Vec3.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/Vec3.java
new file mode 100644
index 0000000..b5604af
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/node/Vec3.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.node;
+
+import androidx.annotation.RestrictTo;
+
+/** 3D vector. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class Vec3 {
+ public Vec3(float x, float y, float z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ public float x;
+ public float y;
+ public float z;
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/passthrough/PassthroughState.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/passthrough/PassthroughState.java
new file mode 100644
index 0000000..d003129
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/passthrough/PassthroughState.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.passthrough;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+
+/** Allows to configure the passthrough when the application is in full-space mode. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class PassthroughState {
+ /** Passthrough mode to be used. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(
+ value = {
+ PASSTHROUGH_MODE_OFF,
+ PASSTHROUGH_MODE_MAX,
+ PASSTHROUGH_MODE_MIN,
+ })
+ @Retention(SOURCE)
+ public @interface Mode {}
+
+ /** Node does not contribute to the opacity of the final passthrough state. */
+ public static final int PASSTHROUGH_MODE_OFF = 0;
+
+ /** Node maximizes the opacity of the final passthrough state. */
+ public static final int PASSTHROUGH_MODE_MAX = 1;
+
+ /** Node minimizes the opacity of the final passthrough state. */
+ public static final int PASSTHROUGH_MODE_MIN = 2;
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/ActivityPanel.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/ActivityPanel.java
new file mode 100644
index 0000000..5eda03b
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/ActivityPanel.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.space;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.xr.extensions.node.Node;
+
+/**
+ * Defines the panel in XR scene to support embedding activities within a host activity.
+ *
+ * <p>When the host activity is destroyed, all the activities in its embedded {@link ActivityPanel}
+ * will also be destroyed.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface ActivityPanel {
+ /**
+ * Launches an activity into this panel.
+ *
+ * @param intent the {@link Intent} to start.
+ * @param options additional options for how the Activity should be started.
+ */
+ void launchActivity(@NonNull Intent intent, @Nullable Bundle options);
+
+ /**
+ * Moves an existing activity into this panel.
+ *
+ * @param activity the {@link Activity} to move.
+ */
+ void moveActivity(@NonNull Activity activity);
+
+ /**
+ * Gets the node associated with this {@link ActivityPanel}.
+ *
+ * <p>The {@link ActivityPanel} can only be shown to the user after this node is attached to the
+ * host activity's scene.
+ *
+ * @see androidx.xr.extensions.XrExtensions#attachSpatialScene
+ */
+ @NonNull
+ Node getNode();
+
+ /**
+ * Updates the 2D window bounds of this {@link ActivityPanel}.
+ *
+ * <p>If the new bounds are smaller that the minimum dimensions of the activity embedded in this
+ * ActivityPanel, the ActivityPanel bounds will be reset to match the host Activity bounds.
+ *
+ * @param windowBounds the new 2D window bounds in the host container window coordinates.
+ */
+ void setWindowBounds(@NonNull Rect windowBounds);
+
+ /**
+ * Deletes the activity panel. All the activities in this {@link ActivityPanel} will also be
+ * destroyed.
+ */
+ void delete();
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/ActivityPanelImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/ActivityPanelImpl.java
new file mode 100644
index 0000000..1d6925c
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/ActivityPanelImpl.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.space;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.extensions.node.NodeTypeConverter;
+
+/** Implementation of {@link ActivityPanel}. */
+final class ActivityPanelImpl implements ActivityPanel {
+ @NonNull final com.android.extensions.xr.space.ActivityPanel mActivityPanel;
+
+ ActivityPanelImpl(@NonNull com.android.extensions.xr.space.ActivityPanel panel) {
+ mActivityPanel = panel;
+ }
+
+ @Override
+ public void launchActivity(@NonNull Intent intent, @Nullable Bundle options) {
+ mActivityPanel.launchActivity(intent, options);
+ }
+
+ @Override
+ public void moveActivity(@NonNull Activity activity) {
+ mActivityPanel.moveActivity(activity);
+ }
+
+ @Override
+ @NonNull
+ public Node getNode() {
+ return NodeTypeConverter.toLibrary(mActivityPanel.getNode());
+ }
+
+ @Override
+ public void setWindowBounds(@NonNull Rect windowBounds) {
+ mActivityPanel.setWindowBounds(windowBounds);
+ }
+
+ @Override
+ public void delete() {
+ mActivityPanel.delete();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/ActivityPanelLaunchParameters.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/ActivityPanelLaunchParameters.java
new file mode 100644
index 0000000..1c8d5d1
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/ActivityPanelLaunchParameters.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.space;
+
+import android.graphics.Rect;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/** Defines the launch parameters when creating an {@link ActivityPanel}. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public final class ActivityPanelLaunchParameters {
+ /** 2D window bounds in the host container window coordinates. */
+ private final @NonNull Rect mWindowBounds;
+
+ /**
+ * Constructs an {@link ActivityPanelLaunchParameters} with the given initial window bounds.
+ *
+ * @param windowBounds the initial 2D window bounds of the panel, which will be the bounds of
+ * the Activity launched into the {@link ActivityPanel}.
+ */
+ public ActivityPanelLaunchParameters(@NonNull Rect windowBounds) {
+ mWindowBounds = windowBounds;
+ }
+
+ /**
+ * @return the initial 2D window bounds.
+ */
+ public @NonNull Rect getWindowBounds() {
+ return mWindowBounds;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/Bounds.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/Bounds.java
new file mode 100644
index 0000000..4ec1f6f
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/Bounds.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.space;
+
+import androidx.annotation.RestrictTo;
+
+import java.util.Objects;
+
+/**
+ * Bounds values in meters.
+ *
+ * @see androidx.xr.extensions.XrExtensions#getSpatialState
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class Bounds {
+ public final float width;
+ public final float height;
+ public final float depth;
+
+ public Bounds(float width, float height, float depth) {
+ this.width = width;
+ this.height = height;
+ this.depth = depth;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || !(other instanceof Bounds)) {
+ return false;
+ }
+ Bounds impl = (Bounds) other;
+ return width == impl.width && height == impl.height && depth == impl.depth;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(width, height, depth);
+ }
+
+ @Override
+ public String toString() {
+ return "{width=" + width + ", height=" + height + ", depth=" + depth + "}";
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/BoundsChangeEvent.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/BoundsChangeEvent.java
new file mode 100644
index 0000000..faa719d
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/BoundsChangeEvent.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.space;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Triggers when there is a bounds change. For example, resize the panel in home space, or
+ * enter/exit FSM.
+ *
+ * @deprecated Use SpatialState instead.
+ */
+@Deprecated
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public final class BoundsChangeEvent extends SpatialStateEvent {
+ /** Width of the bounds in meters. */
+ public float width;
+
+ /** Height of the bounds in meters. */
+ public float height;
+
+ /** Depth of the bounds in meters. */
+ public float depth;
+
+ /** Bounds in meters. */
+ public Bounds bounds;
+
+ public BoundsChangeEvent(Bounds bounds) {
+ this.bounds = bounds;
+ this.width = bounds.width;
+ this.height = bounds.height;
+ this.depth = bounds.depth;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/EnvironmentControlChangeEvent.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/EnvironmentControlChangeEvent.java
new file mode 100644
index 0000000..485760f
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/EnvironmentControlChangeEvent.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.space;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Triggers when the ability to control the environment changes.
+ *
+ * @deprecated Use SpatialState instead.
+ */
+@Deprecated
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public final class EnvironmentControlChangeEvent extends SpatialStateEvent {
+ /** Whether or not the receiver can control the environment. */
+ public boolean environmentControlAllowed;
+
+ public EnvironmentControlChangeEvent(boolean allowed) {
+ this.environmentControlAllowed = allowed;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/EnvironmentVisibilityChangeEvent.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/EnvironmentVisibilityChangeEvent.java
new file mode 100644
index 0000000..3f5fbd6
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/EnvironmentVisibilityChangeEvent.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.space;
+
+import androidx.annotation.RestrictTo;
+import androidx.xr.extensions.environment.EnvironmentVisibilityState;
+
+/**
+ * For all resumed top activities with this spatialstate callback set, this is called whenever the
+ * VR background changes. This is also called when an activity becomes top resumed.
+ *
+ * @deprecated Use SpatialState instead.
+ */
+@Deprecated
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public final class EnvironmentVisibilityChangeEvent extends SpatialStateEvent {
+ /** Visibility state of the VR background */
+ public @EnvironmentVisibilityState.State int environmentState;
+
+ public EnvironmentVisibilityChangeEvent(@EnvironmentVisibilityState.State int state) {
+ this.environmentState = state;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/HitTestResult.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/HitTestResult.java
new file mode 100644
index 0000000..cc34e36
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/HitTestResult.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.space;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.xr.extensions.node.Vec3;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Hit test result.
+ *
+ * @see androidx.xr.extensions.XrExtensions#hitTest
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class HitTestResult {
+ /** Distance from the ray origin to the hit position. */
+ public float distance;
+
+ /** The hit position in task coordinates. */
+ public @NonNull Vec3 hitPosition;
+
+ /** Normal of the surface at the collision point, if known. */
+ public @Nullable Vec3 surfaceNormal;
+
+ /** Hit surface types. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(value = {SURFACE_UNKNOWN, SURFACE_PANEL, SURFACE_3D_OBJECT})
+ @Retention(SOURCE)
+ public @interface SurfaceType {}
+
+ public static final int SURFACE_UNKNOWN = 0;
+ public static final int SURFACE_PANEL = 1;
+ public static final int SURFACE_3D_OBJECT = 2;
+
+ /** The type of surface that was hit. */
+ public @SurfaceType int surfaceType;
+
+ /** Whether or not the virtual background environment is visible. */
+ public boolean virtualEnvironmentIsVisible;
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/PassthroughVisibilityChangeEvent.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/PassthroughVisibilityChangeEvent.java
new file mode 100644
index 0000000..c29176e
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/PassthroughVisibilityChangeEvent.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.space;
+
+import androidx.annotation.RestrictTo;
+import androidx.xr.extensions.environment.PassthroughVisibilityState;
+
+/**
+ * For all resumed top activities with this spatialstate callback set, this is called whenever the
+ * Passthrough state changes. This is also called when an activity becomes top resumed.
+ * TODO(runping): Ask the privacy team if it's okay to send this event in HSM.
+ *
+ * @deprecated Use SpatialState instead.
+ */
+@Deprecated
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public final class PassthroughVisibilityChangeEvent extends SpatialStateEvent {
+ /** Visibility state of Passthrough. */
+ public @PassthroughVisibilityState.State int passthroughState;
+
+ public PassthroughVisibilityChangeEvent(@PassthroughVisibilityState.State int state) {
+ this.passthroughState = state;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpaceTypeConverter.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpaceTypeConverter.java
new file mode 100644
index 0000000..0b7d2d8
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpaceTypeConverter.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.space;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.xr.extensions.node.NodeTypeConverter;
+
+/** This class is able to convert library types into platform types. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class SpaceTypeConverter {
+
+ private SpaceTypeConverter() {}
+
+ /**
+ * Converts a {@link Bounds} to a framework type.
+ *
+ * @param bounds The {@link Bounds} to convert.
+ * @return The framework type of the {@link com.android.extensions.xr.space.Bounds}.
+ */
+ @NonNull
+ public static com.android.extensions.xr.space.Bounds toFramework(@NonNull Bounds bounds) {
+ requireNonNull(bounds);
+
+ return new com.android.extensions.xr.space.Bounds(
+ bounds.width, bounds.height, bounds.depth);
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.space.Bounds} to a library type.
+ *
+ * @param bounds The {@link com.android.extensions.xr.space.Bounds} to convert.
+ * @return The library type of the {@link Bounds}.
+ */
+ @NonNull
+ public static Bounds toLibrary(@NonNull com.android.extensions.xr.space.Bounds bounds) {
+ requireNonNull(bounds);
+
+ return new Bounds(bounds.getWidth(), bounds.getHeight(), bounds.getDepth());
+ }
+
+ /**
+ * Converts a {@link SpatialCapabilities} to a framework type.
+ *
+ * @param capabilities The {@link SpatialCapabilities} to convert.
+ * @return The framework type of the {@link
+ * com.android.extensions.xr.space.SpatialCapabilities}.
+ */
+ @NonNull
+ public static com.android.extensions.xr.space.SpatialCapabilities toFramework(
+ @NonNull SpatialCapabilitiesImpl capabilities) {
+ requireNonNull(capabilities);
+
+ return capabilities.mCapabilities;
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.space.SpatialCapabilities} to a library type.
+ *
+ * @param capabilities The {@link com.android.extensions.xr.space.SpatialCapabilities} to
+ * convert.
+ * @return The library type of the {@link SpatialCapabilities}.
+ */
+ @NonNull
+ public static SpatialCapabilitiesImpl toLibrary(
+ @NonNull com.android.extensions.xr.space.SpatialCapabilities capabilities) {
+ requireNonNull(capabilities);
+
+ return new SpatialCapabilitiesImpl(capabilities);
+ }
+
+ /**
+ * Converts a {@link HitTestResult} to a framework type.
+ *
+ * @param result The {@link HitTestResult} to convert.
+ * @return The framework type of the {@link com.android.extensions.xr.space.HitTestResult}.
+ */
+ @NonNull
+ public static com.android.extensions.xr.space.HitTestResult toFramework(
+ @NonNull HitTestResult result) {
+ requireNonNull(result);
+
+ com.android.extensions.xr.space.HitTestResult.Builder builder =
+ new com.android.extensions.xr.space.HitTestResult.Builder(
+ result.distance,
+ NodeTypeConverter.toFramework(result.hitPosition),
+ result.virtualEnvironmentIsVisible,
+ result.surfaceType);
+
+ if (result.surfaceNormal != null) {
+ builder.setSurfaceNormal(NodeTypeConverter.toFramework(result.surfaceNormal));
+ }
+
+ return builder.build();
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.space.HitTestResult} to a library type.
+ *
+ * @param result The {@link com.android.extensions.xr.space.HitTestResult} to convert.
+ * @return The library type of the {@link HitTestResult}.
+ */
+ @NonNull
+ public static HitTestResult toLibrary(
+ @NonNull com.android.extensions.xr.space.HitTestResult result) {
+ requireNonNull(result);
+
+ HitTestResult hitTestResult = new HitTestResult();
+ hitTestResult.distance = result.getDistance();
+ hitTestResult.hitPosition = NodeTypeConverter.toLibrary(result.getHitPosition());
+ if (result.getSurfaceNormal() != null) {
+ hitTestResult.surfaceNormal = NodeTypeConverter.toLibrary(result.getSurfaceNormal());
+ }
+ hitTestResult.surfaceType = result.getSurfaceType();
+ hitTestResult.virtualEnvironmentIsVisible = result.getVirtualEnvironmentIsVisible();
+
+ return hitTestResult;
+ }
+
+ /**
+ * Converts a {@link ActivityPanel} to a framework type.
+ *
+ * @param panel The {@link ActivityPanel} to convert.
+ * @return The framework type of the {@link com.android.extensions.xr.space.ActivityPanel}.
+ */
+ @NonNull
+ public static com.android.extensions.xr.space.ActivityPanel toFramework(
+ @NonNull ActivityPanelImpl panel) {
+ requireNonNull(panel);
+
+ return panel.mActivityPanel;
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.space.ActivityPanel} to a library type.
+ *
+ * @param panel The {@link com.android.extensions.xr.space.ActivityPanel} to convert.
+ * @return The library type of the {@link ActivityPanel}.
+ */
+ @NonNull
+ public static ActivityPanelImpl toLibrary(
+ @NonNull com.android.extensions.xr.space.ActivityPanel panel) {
+ requireNonNull(panel);
+
+ return new ActivityPanelImpl(panel);
+ }
+
+ /**
+ * Converts a {@link ActivityPanelLaunchParameters} to a framework type.
+ *
+ * @param params The {@link ActivityPanelLaunchParameters} to convert.
+ * @return The framework type of the {@link
+ * com.android.extensions.xr.space.ActivityPanelLaunchParameters}.
+ */
+ @NonNull
+ public static com.android.extensions.xr.space.ActivityPanelLaunchParameters toFramework(
+ @NonNull ActivityPanelLaunchParameters params) {
+ requireNonNull(params);
+
+ return new com.android.extensions.xr.space.ActivityPanelLaunchParameters(
+ params.getWindowBounds());
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.space.ActivityPanelLaunchParameters} to a library
+ * type.
+ *
+ * @param params The {@link com.android.extensions.xr.space.ActivityPanelLaunchParameters} to
+ * convert.
+ * @return The library type of the {@link ActivityPanelLaunchParameters}.
+ */
+ @NonNull
+ public static ActivityPanelLaunchParameters toLibrary(
+ @NonNull com.android.extensions.xr.space.ActivityPanelLaunchParameters params) {
+ requireNonNull(params);
+
+ return new ActivityPanelLaunchParameters(params.getWindowBounds());
+ }
+
+ /**
+ * Converts a {@link SpatialState} to a library type.
+ *
+ * @param state The {@link SpatialState} to convert.
+ * @return The library type of the {@link SpatialState}.
+ */
+ @NonNull
+ public static SpatialState toLibrary(
+ @NonNull com.android.extensions.xr.space.SpatialState state) {
+ requireNonNull(state);
+
+ return new SpatialStateImpl(state);
+ }
+
+ /**
+ * Converts a {@link SpatialStateEvent} to a library type.
+ *
+ * @param event The {@link SpatialStateEvent} to convert.
+ * @return The library type of the {@link SpatialStateEvent}.
+ */
+ @NonNull
+ public static SpatialStateEvent toLibrary(
+ @NonNull com.android.extensions.xr.space.SpatialStateEvent event) {
+ requireNonNull(event);
+
+ if (event instanceof com.android.extensions.xr.space.BoundsChangeEvent) {
+ return toLibrary((com.android.extensions.xr.space.BoundsChangeEvent) event);
+ } else if (event instanceof com.android.extensions.xr.space.EnvironmentControlChangeEvent) {
+ return toLibrary((com.android.extensions.xr.space.EnvironmentControlChangeEvent) event);
+ } else if (event
+ instanceof com.android.extensions.xr.space.EnvironmentVisibilityChangeEvent) {
+ return toLibrary(
+ (com.android.extensions.xr.space.EnvironmentVisibilityChangeEvent) event);
+ } else if (event instanceof com.android.extensions.xr.space.SpatialCapabilityChangeEvent) {
+ return toLibrary((com.android.extensions.xr.space.SpatialCapabilityChangeEvent) event);
+ }
+
+ // TODO(bvanderlaan): Handle this error better.
+ throw new IllegalArgumentException("Unknown event type " + event);
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.space.BoundsChangeEvent} to a library type.
+ *
+ * @param event The {@link com.android.extensions.xr.space.BoundsChangeEvent} to convert.
+ * @return The library type of the {@link BoundsChangeEvent}.
+ */
+ @NonNull
+ public static SpatialStateEvent toLibrary(
+ @NonNull com.android.extensions.xr.space.BoundsChangeEvent event) {
+ requireNonNull(event);
+
+ return new BoundsChangeEvent(toLibrary(event.getBounds()));
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.space.EnvironmentControlChangeEvent} to a library
+ * type.
+ *
+ * @param event The {@link com.android.extensions.xr.space.EnvironmentControlChangeEvent} to
+ * convert.
+ * @return The library type of the {@link EnvironmentControlChangeEvent}.
+ */
+ @NonNull
+ public static SpatialStateEvent toLibrary(
+ @NonNull com.android.extensions.xr.space.EnvironmentControlChangeEvent event) {
+ requireNonNull(event);
+
+ return new EnvironmentControlChangeEvent(event.getEnvironmentControlAllowed());
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.space.EnvironmentVisibilityChangeEvent} to a
+ * library type.
+ *
+ * @param event The {@link com.android.extensions.xr.space.EnvironmentVisibilityChangeEvent} to
+ * convert.
+ * @return The library type of the {@link EnvironmentVisibilityChangeEvent}.
+ */
+ @NonNull
+ public static SpatialStateEvent toLibrary(
+ @NonNull com.android.extensions.xr.space.EnvironmentVisibilityChangeEvent event) {
+ requireNonNull(event);
+
+ return new EnvironmentVisibilityChangeEvent(event.getEnvironmentState());
+ }
+
+ /**
+ * Converts a {@link com.android.extensions.xr.space.SpatialCapabilityChangeEvent} to a library
+ * type.
+ *
+ * @param event The {@link com.android.extensions.xr.space.SpatialCapabilityChangeEvent} to
+ * convert.
+ * @return The library type of the {@link SpatialCapabilityChangeEvent}.
+ */
+ @NonNull
+ public static SpatialStateEvent toLibrary(
+ @NonNull com.android.extensions.xr.space.SpatialCapabilityChangeEvent event) {
+ requireNonNull(event);
+
+ return new SpatialCapabilityChangeEvent(toLibrary(event.getCurrentCapabilities()));
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpatialCapabilities.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpatialCapabilities.java
new file mode 100644
index 0000000..3ec23c8
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpatialCapabilities.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.space;
+
+import static androidx.xr.extensions.XrExtensions.IMAGE_TOO_OLD;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+
+/** Represents a set of capabilities an activity has. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface SpatialCapabilities {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(
+ value = {
+ SPATIAL_UI_CAPABLE,
+ SPATIAL_3D_CONTENTS_CAPABLE,
+ PASSTHROUGH_CONTROL_CAPABLE,
+ APP_ENVIRONMENTS_CAPABLE,
+ SPATIAL_AUDIO_CAPABLE,
+ SPATIAL_ACTIVITY_EMBEDDING_CAPABLE
+ })
+ @Retention(SOURCE)
+ @interface CapabilityType {}
+
+ /**
+ * The activity can spatialize itself by adding a spatial panel.
+ *
+ * <p>This capability allows neither 3D content creation nor spatial activity embedding.
+ */
+ int SPATIAL_UI_CAPABLE = 0;
+
+ /**
+ * The activity can create 3D contents.
+ *
+ * <p>This capability allows neither spatial panel creation nor spatial activity embedding.
+ */
+ int SPATIAL_3D_CONTENTS_CAPABLE = 1;
+
+ /** The activity can enable or disable passthrough. */
+ int PASSTHROUGH_CONTROL_CAPABLE = 2;
+
+ /** The activity can set its own environment. */
+ int APP_ENVIRONMENTS_CAPABLE = 3;
+
+ /** The activity can use spatial audio. */
+ int SPATIAL_AUDIO_CAPABLE = 4;
+
+ /**
+ * The activity can launch another activity on a spatial panel to spatially embed it.
+ *
+ * <p>This capability allows neither spatial panel creation nor 3D content creation.
+ */
+ int SPATIAL_ACTIVITY_EMBEDDING_CAPABLE = 5;
+
+ /** Returns true if the capability is available. */
+ default boolean get(@CapabilityType int capability) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpatialCapabilitiesImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpatialCapabilitiesImpl.java
new file mode 100644
index 0000000..d9640d1
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpatialCapabilitiesImpl.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.space;
+
+import androidx.annotation.NonNull;
+
+final class SpatialCapabilitiesImpl implements SpatialCapabilities {
+ @NonNull final com.android.extensions.xr.space.SpatialCapabilities mCapabilities;
+
+ SpatialCapabilitiesImpl() {
+ mCapabilities = new com.android.extensions.xr.space.SpatialCapabilities();
+ }
+
+ SpatialCapabilitiesImpl(
+ @NonNull com.android.extensions.xr.space.SpatialCapabilities capabilities) {
+ mCapabilities = capabilities;
+ }
+
+ @Override
+ public boolean get(@CapabilityType int capability) {
+ return mCapabilities.get(capability);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ SpatialCapabilitiesImpl impl = (SpatialCapabilitiesImpl) other;
+ return mCapabilities.equals(impl.mCapabilities);
+ }
+
+ @Override
+ public int hashCode() {
+ return mCapabilities.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return mCapabilities.toString();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpatialCapabilityChangeEvent.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpatialCapabilityChangeEvent.java
new file mode 100644
index 0000000..160a1fe
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpatialCapabilityChangeEvent.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.space;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Triggers when the spatial capability set has changed.
+ *
+ * @deprecated Use SpatialState instead.
+ */
+@Deprecated
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public final class SpatialCapabilityChangeEvent extends SpatialStateEvent {
+ public SpatialCapabilities currentCapabilities;
+
+ public SpatialCapabilityChangeEvent(SpatialCapabilities capabilities) {
+ this.currentCapabilities = capabilities;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpatialState.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpatialState.java
new file mode 100644
index 0000000..4ccaffa
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpatialState.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.space;
+
+import static androidx.xr.extensions.XrExtensions.IMAGE_TOO_OLD;
+
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.xr.extensions.environment.EnvironmentVisibilityState;
+import androidx.xr.extensions.environment.PassthroughVisibilityState;
+import androidx.xr.extensions.node.Node;
+
+/**
+ * An interface that represents an activity's spatial state.
+ *
+ * <p>An object of the class is effectively immutable. Once the object, which is a "snapshot" of the
+ * activity's spatial state, is returned to the client, each getters will always return the same
+ * value even if the activity's state later changes.
+ *
+ * @see androidx.xr.extensions.XrExtensions#registerSpatialStateCallback
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface SpatialState {
+ /**
+ * Gets spatial bounds of the activity. When in full space mode, (infinity, infinity, infinity)
+ * is returned.
+ *
+ * @see androidx.xr.extensions.space.Bounds
+ */
+ default @NonNull Bounds getBounds() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Gets spatial capabilities of the activity. Unlike other capabilities in Android, this may
+ * dynamically change based on the current mode the activity is in, whether the activity is the
+ * top one in its task, whether the task is the top visible one on the desktop, and so on.
+ *
+ * @see androidx.xr.extensions.space.SpatialCapabilities
+ */
+ default @NonNull SpatialCapabilities getSpatialCapabilities() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Gets the environment visibility of the activity.
+ *
+ * @see androidx.xr.extensions.environment.EnvironmentVisibilityState
+ */
+ default @NonNull EnvironmentVisibilityState getEnvironmentVisibility() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Gets the passthrough visibility of the activity.
+ *
+ * @see androidx.xr.extensions.environment.PassthroughVisibilityState
+ */
+ default @NonNull PassthroughVisibilityState getPassthroughVisibility() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * True if the scene node that is currently in use (if any) is the same as targetNode. When
+ * targetNode is null, this API returns true when no scene node is currently in use (i.e. the
+ * activity is not SPATIAL_UI_CAPABLE, the activity hasn't called attachSpatialScene API at all,
+ * or the activity hasn't called it again since the last detachSpatialScene API call.)
+ *
+ * @see androidx.xr.extensions.attachSpatialScene
+ */
+ default boolean isActiveSceneNode(@Nullable Node targetNode) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * True if the window leash node that is currently in use (if any) is the same as targetNode.
+ * When targetNode is null, this API returns true when no window leash node is currently in use
+ * (i.e. the activity is not SPATIAL_UI_CAPABLE, the activity hasn't called attachSpatialScene
+ * API at all, or the activity hasn't called it again since the last detachSpatialScene API
+ * call.)
+ *
+ * @see androidx.xr.extensions.attachSpatialScene
+ */
+ default boolean isActiveWindowLeashNode(@Nullable Node targetNode) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * True if the environment node that is currently in use (if any) is the same as targetNode.
+ * When targetNode is null, this API returns true when no environment node is currently in use
+ * (i.e. the activity is not APP_ENVIRONMENTS_CAPABLE, the activity hasn't called
+ * attachSpatialEnvironment API at all, or the activity hasn't called it again since the last
+ * detachSpatialEnvironment API call.)
+ *
+ * <p>Note that as a special case, when isEnvironmentInherited() is true, the API returns false
+ * for a null targetNode even if your activity hasn't called attachSpatialEnvironment yet.
+ *
+ * @see androidx.xr.extensions.attachSpatialEnvironment
+ * @see androidx.xr.extensions.setFullSpaceModeWithEnvironmentInherited
+ */
+ default boolean isActiveEnvironmentNode(@Nullable Node targetNode) {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * True if an activity-provided environment node is currently in use, and the node is one
+ * inherited from a different activity.
+ *
+ * @see androidx.xr.extensions.attachSpatialEnvironment
+ * @see androidx.xr.extensions.setFullSpaceModeWithEnvironmentInherited
+ */
+ default boolean isEnvironmentInherited() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Gets the main window size. (0, 0) is returned when the activity is not SPATIAL_UI_CAPABLE.
+ *
+ * <p>When the activity is not SPATIAL_UI_CAPABLE, use android.content.res.Configuration to
+ * obtain the activity's size.
+ *
+ * @see androidx.xr.extensions.setMainWindowSize
+ * @see android.content.res.Configuration
+ */
+ @NonNull
+ default Size getMainWindowSize() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+
+ /**
+ * Gets the main window's aspect ratio preference. 0.0f is returned when there's no preference
+ * set via setPreferredAspectRatio API, or the activity is currently SPATIAL_UI_CAPABLE.
+ *
+ * <p>When SPATIAL_UI_CAPABLE, activities can set a preferred aspect ratio via ReformOptions,
+ * but that reform options setting won't be reflected to the value returned from this API.
+ *
+ * @see androidx.xr.extensions.setPreferredAspectRatio
+ * @see androidx.xr.extensions.node.ReformOptions
+ */
+ default float getPreferredAspectRatio() {
+ throw new UnsupportedOperationException(IMAGE_TOO_OLD);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpatialStateEvent.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpatialStateEvent.java
new file mode 100644
index 0000000..0173a22
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpatialStateEvent.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.space;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Base class for spatial state change events.
+ *
+ * @see androidx.xr.extensions.XrExtensions#setSpatialStateCallback
+ * @deprecated Use SpatialState instead.
+ */
+@Deprecated
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public abstract class SpatialStateEvent {}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpatialStateImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpatialStateImpl.java
new file mode 100644
index 0000000..b33ac5c
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/SpatialStateImpl.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.space;
+
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.xr.extensions.environment.EnvironmentTypeConverter;
+import androidx.xr.extensions.environment.EnvironmentVisibilityState;
+import androidx.xr.extensions.environment.PassthroughVisibilityState;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.extensions.node.NodeTypeConverter;
+
+class SpatialStateImpl implements SpatialState {
+ @NonNull private final com.android.extensions.xr.space.SpatialState mState;
+
+ SpatialStateImpl(@NonNull com.android.extensions.xr.space.SpatialState state) {
+ mState = state;
+ }
+
+ @Override
+ public @NonNull Bounds getBounds() {
+ return SpaceTypeConverter.toLibrary(mState.getBounds());
+ }
+
+ @Override
+ public @NonNull SpatialCapabilities getSpatialCapabilities() {
+ return SpaceTypeConverter.toLibrary(mState.getSpatialCapabilities());
+ }
+
+ @Override
+ public @NonNull EnvironmentVisibilityState getEnvironmentVisibility() {
+ return EnvironmentTypeConverter.toLibrary(mState.getEnvironmentVisibility());
+ }
+
+ @Override
+ public @NonNull PassthroughVisibilityState getPassthroughVisibility() {
+ return EnvironmentTypeConverter.toLibrary(mState.getPassthroughVisibility());
+ }
+
+ @Override
+ public boolean isActiveSceneNode(@Nullable Node targetNode) {
+ return mState.isActiveSceneNode(NodeTypeConverter.toFramework(targetNode));
+ }
+
+ @Override
+ public boolean isActiveWindowLeashNode(@Nullable Node targetNode) {
+ return mState.isActiveWindowLeashNode(NodeTypeConverter.toFramework(targetNode));
+ }
+
+ @Override
+ public boolean isActiveEnvironmentNode(@Nullable Node targetNode) {
+ return mState.isActiveEnvironmentNode(NodeTypeConverter.toFramework(targetNode));
+ }
+
+ @Override
+ public boolean isEnvironmentInherited() {
+ return mState.isEnvironmentInherited();
+ }
+
+ @Override
+ public @NonNull Size getMainWindowSize() {
+ return mState.getMainWindowSize();
+ }
+
+ @Override
+ public float getPreferredAspectRatio() {
+ return mState.getPreferredAspectRatio();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || !(other instanceof SpatialStateImpl)) {
+ return false;
+ }
+
+ SpatialStateImpl impl = (SpatialStateImpl) other;
+ return mState.equals(impl.mState);
+ }
+
+ @Override
+ public int hashCode() {
+ return mState.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return mState.toString();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/VisibilityChangeEvent.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/VisibilityChangeEvent.java
new file mode 100644
index 0000000..e1fe354
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/space/VisibilityChangeEvent.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.space;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RestrictTo;
+import androidx.xr.extensions.ExperimentalExtensionApi;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Triggers when there is a change to spatial visibility. For example, user looks away from the
+ * activity.
+ *
+ * @deprecated Not used anymore.
+ */
+@Deprecated
+@ExperimentalExtensionApi
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public final class VisibilityChangeEvent extends SpatialStateEvent {
+ /** Possible visibility values for an activity. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(value = {UNKNOWN, HIDDEN, PARTIALLY_VISIBLE, VISIBLE})
+ @Retention(SOURCE)
+ public @interface SpatialVisibility {}
+
+ /** The visibility of this activity is not known. */
+ public static final int UNKNOWN = 0;
+
+ /** This activity is hidden outside of the user's Field of View. */
+ public static final int HIDDEN = 1;
+
+ /** Some, but not all, of the activity is within the user's Field of View. */
+ public static final int PARTIALLY_VISIBLE = 2;
+
+ /** The entirety of the activity is within the user's Field of View. */
+ public static final int VISIBLE = 3;
+
+ public @SpatialVisibility int visibility;
+
+ public VisibilityChangeEvent(@SpatialVisibility int visibility) {
+ this.visibility = visibility;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/splitengine/SplitEngineBridge.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/splitengine/SplitEngineBridge.java
new file mode 100644
index 0000000..512dfcf
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/splitengine/SplitEngineBridge.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.splitengine;
+
+import androidx.annotation.RestrictTo;
+
+/** Wrapper object around a native SplitEngineBridge. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SplitEngineBridge {
+ SplitEngineBridge() {}
+ ;
+
+ /**
+ * Opaque token to split engine bridge
+ *
+ * <p>This will be read/set by native code inside Impress via JNI.
+ *
+ * <p>JNI does not respect access modifies so this private field is publicly accessible to the
+ * native impress code.
+ *
+ * <p>This field is read from and written to by: *
+ * vendor/google/imp/core/split_engine/android/view/split_engine_jni.cc *
+ * google3/third_party/impress/core/split_engine/android/view/split_engine_jni.cc *
+ * frameworks/base/libs/xr/Jetpack/jni/android_xr_splitengine_extension.cpp
+ *
+ * <p>The latter accesses this field through the implementation of the SplitEngineBridge
+ * interface.
+ */
+ @SuppressWarnings("unused")
+ private long mNativeHandle;
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/splitengine/SplitEngineBridgeImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/splitengine/SplitEngineBridgeImpl.java
new file mode 100644
index 0000000..7f07b96
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/splitengine/SplitEngineBridgeImpl.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.splitengine;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.lang.reflect.Field;
+
+/** Implementation of SplitEngineBridge which delegates to native Impress implementation. */
+class SplitEngineBridgeImpl extends SplitEngineBridge {
+ final com.android.extensions.xr.splitengine.SplitEngineBridge mBridge;
+
+ SplitEngineBridgeImpl(@NonNull com.android.extensions.xr.splitengine.SplitEngineBridge bridge) {
+ mBridge = bridge;
+ setNativeHandle();
+ }
+
+ /**
+ * Set the shared memory split engine bridge handle.
+ *
+ * <p>The {@link mNativeHandle} field is defined as a private field on the extensions library
+ * which we would like to not change for backward compatibility. This field is read by
+ * application code and needs to contain the handle provided by the platform JNI component which
+ * is accessible from {@link com.android.extensions.xr.splitengine.SplitEngineBridge}. In order
+ * to be able to access this private field we need to use reflection.
+ *
+ * <p>This method will access the handle from the platform instance of SplitEngineBridge and
+ * store it in the private {@link mNativeHandle} field defined by the library API.
+ */
+ private void setNativeHandle() {
+ try {
+ Field privateField = SplitEngineBridge.class.getDeclaredField("mNativeHandle");
+ privateField.setAccessible(true);
+
+ privateField.set(this, mBridge.getNativeHandle());
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ Log.e("SplitEngineBridge", "Failed to set native handle", e);
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/splitengine/SplitEngineTypeConverter.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/splitengine/SplitEngineTypeConverter.java
new file mode 100644
index 0000000..d1c16c31
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/splitengine/SplitEngineTypeConverter.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.splitengine;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/** This class is able to convert library types into platform types. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class SplitEngineTypeConverter {
+
+ private SplitEngineTypeConverter() {}
+
+ /**
+ * Converts a {@link com.android.extensions.xr.splitengine.SplitEngineBridge} to a library type.
+ *
+ * @param bridge The {@link com.android.extensions.xr.splitengine.SplitEngineBridge} to convert.
+ * @return The library type of the {@link SplitEngineBridge}.
+ */
+ @NonNull
+ public static SplitEngineBridge toLibrary(
+ @NonNull com.android.extensions.xr.splitengine.SplitEngineBridge bridge) {
+ requireNonNull(bridge);
+
+ return new SplitEngineBridgeImpl(bridge);
+ }
+
+ /**
+ * Converts a {@link SplitEngineBridge} to a framework type.
+ *
+ * @param bridge The {@link SplitEngineBridge} to convert.
+ * @return The framework type of the {@link
+ * com.android.extensions.xr.splitengine.SplitEngineBridge}.
+ */
+ @NonNull
+ public static com.android.extensions.xr.splitengine.SplitEngineBridge toFramework(
+ @NonNull SplitEngineBridge bridge) {
+ requireNonNull(bridge);
+
+ return ((SplitEngineBridgeImpl) bridge).mBridge;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/subspace/Subspace.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/subspace/Subspace.java
new file mode 100644
index 0000000..00be596
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/subspace/Subspace.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.subspace;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Handle to a subspace in the system scene graph.
+ *
+ * <p>A subspace by itself does not have any visual representation. It merely defines a local space
+ * in its parent space. Once created, 3D content can be rendered in the hierarchy of that subspace.
+ *
+ * <p>Note that {@link Subspace} uses a right-hand coordinate system, i.e. +X points to the right,
+ * +Y up, and +Z points towards the camera.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface Subspace {}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/subspace/SubspaceImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/subspace/SubspaceImpl.java
new file mode 100644
index 0000000..06ebe1e
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/subspace/SubspaceImpl.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.subspace;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Handle to a subspace in the system scene graph.
+ *
+ * <p>A subspace by itself does not have any visual representation. It merely defines a local space
+ * in its parent space. Once created, 3D content can be rendered in the hierarchy of that subspace.
+ *
+ * <p>Note that {@link Subspace} uses a right-hand coordinate system, i.e. +X points to the right,
+ * +Y up, and +Z points towards the camera.
+ */
+class SubspaceImpl implements Subspace {
+ final com.android.extensions.xr.subspace.Subspace mSubspace;
+
+ /** Creates a new empty subspace. */
+ SubspaceImpl(@NonNull com.android.extensions.xr.subspace.Subspace subspace) {
+ mSubspace = subspace;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/subspace/SubspaceTypeConverter.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/subspace/SubspaceTypeConverter.java
new file mode 100644
index 0000000..847616b
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/extensions/subspace/SubspaceTypeConverter.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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 androidx.xr.extensions.subspace;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/** This class is able to convert library types into platform types. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class SubspaceTypeConverter {
+
+ private SubspaceTypeConverter() {}
+
+ /**
+ * Converts a {@link com.android.extensions.xr.subspace.Subspace} to a library type.
+ *
+ * @param subspace The {@link com.android.extensions.xr.subspace.Subspace} to convert.
+ * @return The library type of the {@link Subspace}.
+ */
+ @NonNull
+ public static Subspace toLibrary(
+ @NonNull com.android.extensions.xr.subspace.Subspace subspace) {
+ requireNonNull(subspace);
+
+ return new SubspaceImpl(subspace);
+ }
+
+ /**
+ * Converts a {@link Subspace} to a framework type.
+ *
+ * @param subspace The {@link Subspace} to convert.
+ * @return The framework type of the {@link com.android.extensions.xr.subspace.Subspace}.
+ */
+ @NonNull
+ public static com.android.extensions.xr.subspace.Subspace toFramework(
+ @NonNull Subspace subspace) {
+ requireNonNull(subspace);
+
+ return ((SubspaceImpl) subspace).mSubspace;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ActivityPose.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ActivityPose.kt
new file mode 100644
index 0000000..ec1e4e1
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ActivityPose.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import android.util.Log
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.math.Pose
+
+/**
+ * Interface for a ActivityPose.
+ *
+ * A ActivityPose contains a pose in activity space and it's pose can be transformed into a pose
+ * relative to another ActivityPose.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface ActivityPose {
+
+ /**
+ * Returns the activity space pose for this ActivityPose.
+ *
+ * @return Current [Pose] relative to the activity space root.
+ */
+ public fun getActivitySpacePose(): Pose
+
+ /**
+ * Returns a pose relative to this ActivityPose transformed into a pose relative to the
+ * destination.
+ *
+ * @param pose A pose in this ActivityPose's local coordinate space.
+ * @param destination The ActivityPose which the returned pose will be relative to.
+ * @return The pose relative to the destination ActivityPose.
+ */
+ public fun transformPoseTo(pose: Pose, destination: ActivityPose): Pose
+}
+
+/**
+ * The BaseActivityPose is an implementation of ActivityPose interface that wraps a platformAdapter
+ * ActivityPose.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public sealed class BaseActivityPose<out RtActivityPoseType : JxrPlatformAdapter.ActivityPose>(
+ internal val rtActivityPose: RtActivityPoseType
+) : ActivityPose {
+ private companion object {
+ private const val TAG = "BaseRtActivityPose"
+ }
+
+ override fun getActivitySpacePose(): Pose {
+ return rtActivityPose.activitySpacePose
+ }
+
+ override fun transformPoseTo(pose: Pose, destination: ActivityPose): Pose {
+ if (destination !is BaseActivityPose<JxrPlatformAdapter.ActivityPose>) {
+ Log.e(TAG, "Destination must be a subclass of BaseActivityPose!")
+ return Pose.Identity
+ }
+ return rtActivityPose.transformPoseTo(pose, destination.rtActivityPose)
+ }
+}
+
+/** A ActivityPose which tracks a camera's position and view into physical space. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class CameraView
+private constructor(
+ private val rtCameraViewActivityPose: JxrPlatformAdapter.CameraViewActivityPose
+) : BaseActivityPose<JxrPlatformAdapter.CameraViewActivityPose>(rtCameraViewActivityPose) {
+
+ internal companion object {
+ internal fun createLeft(runtime: JxrPlatformAdapter): CameraView? {
+ val cameraViewActivityPose =
+ runtime.getCameraViewActivityPose(
+ JxrPlatformAdapter.CameraViewActivityPose.CAMERA_TYPE_LEFT_EYE
+ )
+ return cameraViewActivityPose?.let { CameraView(it) }
+ }
+
+ internal fun createRight(runtime: JxrPlatformAdapter): CameraView? {
+ val cameraViewActivityPose =
+ runtime.getCameraViewActivityPose(
+ JxrPlatformAdapter.CameraViewActivityPose.CAMERA_TYPE_RIGHT_EYE
+ )
+ return cameraViewActivityPose?.let { CameraView(it) }
+ }
+ }
+
+ /** Describes the type of camera that this CameraView represents. */
+ public enum class CameraType {
+ /** This CameraView represents an unknown camera view. */
+ UNKNOWN,
+
+ /** This CameraView represents the user's left eye. */
+ LEFT_EYE,
+
+ /** This CameraView represents the user's right eye. */
+ RIGHT_EYE,
+ }
+
+ public val cameraType: CameraType = CameraType.UNKNOWN
+
+ /** Gets the FOV for the camera */
+ public val fov: Fov
+ get() {
+ val rtFov = rtCameraViewActivityPose.fov
+ return Fov(rtFov.angleLeft, rtFov.angleRight, rtFov.angleUp, rtFov.angleDown)
+ }
+}
+
+/**
+ * Head is a ActivityPose used to track the position of the user's head. If there is a left and
+ * right camera it is calculated as the position bettween the two.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class Head private constructor(rtActivityPose: JxrPlatformAdapter.HeadActivityPose) :
+ BaseActivityPose<JxrPlatformAdapter.HeadActivityPose>(rtActivityPose) {
+
+ internal companion object {
+
+ /** Factory function for creating [Head] instance. */
+ internal fun create(runtime: JxrPlatformAdapter): Head? {
+ return runtime.headActivityPose?.let { Head(it) }
+ }
+ }
+}
+
+/**
+ * PerceptionSpace is ActivityPose used to track the origin of the space used by ARCore for XR APIs.
+ */
+// TODO: b/360870690 - Remove suppression annotation when API council review is complete.
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class PerceptionSpace
+private constructor(rtActivityPose: JxrPlatformAdapter.PerceptionSpaceActivityPose) :
+ BaseActivityPose<JxrPlatformAdapter.PerceptionSpaceActivityPose>(rtActivityPose) {
+
+ internal companion object {
+
+ /** Factory function for creating [PerceptionSpace] instance. */
+ internal fun create(runtime: JxrPlatformAdapter): PerceptionSpace =
+ PerceptionSpace(runtime.perceptionSpaceActivityPose)
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/AnchorPlacement.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/AnchorPlacement.kt
new file mode 100644
index 0000000..50e5222
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/AnchorPlacement.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import androidx.annotation.RestrictTo
+
+/**
+ * Creates an AnchorPlacement for a MovableComponent.
+ *
+ * <p> This will enable the MovableComponent to automatically anchor the attached entity to a new
+ * entity
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class AnchorPlacement() {
+ internal val planeTypeFilter: MutableSet<@PlaneTypeValue Int> = HashSet<@PlaneTypeValue Int>()
+ internal val planeSemanticFilter: MutableSet<@PlaneSemanticValue Int> =
+ HashSet<@PlaneSemanticValue Int>()
+
+ public companion object {
+ /**
+ * Creates an anchor placement for anchoring to planes.
+ *
+ * Setting a [PlaneType] or [PlaneSemantic] filter means that the [Entity] with a
+ * [MovableComponent] will be anchored to a plane of that matches at least one of the
+ * specified [PlaneType] filters and at least one specified [PlaneSemantic] filters if it is
+ * released nearby. If no [PlaneType] or no [PlaneSemantic] is set the [Entity] will not be
+ * anchored.
+ *
+ * <p> When an entity is anchored to the plane the pose will be rotated so that it's
+ * Z-vector will be pointing our of the plane (i.e. if it is a panel it will be flat along
+ * the plane. The onMoveEnd callback can be used to listen for the [Entity] being anchored,
+ * reanchored, or unanchored. When anchored the parent will be updated to a new anchor
+ * entity. When unanchored the parent will be set to the [ActivitySpace].
+ *
+ * @param planeTypeFilter The set of plane types to filter by.
+ * @param planeSemanticFilter The set of plane semantics to filter by.
+ */
+ @JvmStatic
+ @JvmOverloads
+ public fun createForPlanes(
+ planeTypeFilter: Set<@PlaneTypeValue Int> = setOf(PlaneType.ANY),
+ planeSemanticFilter: Set<@PlaneSemanticValue Int> = setOf(PlaneSemantic.ANY),
+ ): AnchorPlacement {
+ val placement = AnchorPlacement()
+ placement.planeTypeFilter.addAll(planeTypeFilter)
+ placement.planeSemanticFilter.addAll(planeSemanticFilter)
+ return placement
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/Component.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/Component.kt
new file mode 100644
index 0000000..2337efa
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/Component.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import androidx.annotation.RestrictTo
+
+/**
+ * Base interface for all components.
+ *
+ * Components are attached to entities, to add functionality to those entities.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface Component {
+ /**
+ * Called when this component is attached to the entity.
+ *
+ * @param entity Entity this component is being attached to.
+ * @return True if the component can attach to given Entity.
+ */
+ public fun onAttach(entity: Entity): Boolean
+
+ /**
+ * Called when this component is detached from the entity.
+ *
+ * @param entity Entity this component is being detached from.
+ */
+ public fun onDetach(entity: Entity)
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/Entity.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/Entity.kt
new file mode 100644
index 0000000..5affb34
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/Entity.kt
@@ -0,0 +1,1142 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+@file:Suppress("BanConcurrentHashMap", "Deprecation")
+
+package androidx.xr.scenecore
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import android.view.Surface
+import android.view.View
+import androidx.annotation.IntDef
+import androidx.annotation.MainThread
+import androidx.annotation.RestrictTo
+import androidx.xr.arcore.Anchor
+import androidx.xr.runtime.Session as PerceptionSession
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Vector3
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity as RtEntity
+import androidx.xr.scenecore.JxrPlatformAdapter.PanelEntity as RtPanelEntity
+import java.time.Duration
+import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.ConcurrentMap
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicReference
+import java.util.function.Consumer
+
+/**
+ * Interface for a spatial Entity. An Entity's [Pose]s are represented as being relative to their
+ * parent. Applications create and manage Entity instances to construct spatial scenes.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface Entity : ActivityPose {
+
+ /**
+ * Sets this Entity to be represented in the parent's coordinate space. From a User's
+ * perspective, as the parent moves, this Entity will move with it. Setting the parent to null
+ * will cause the Entity to not be rendered.
+ *
+ * @param parent The [Entity] to attach to.
+ */
+ public fun setParent(parent: Entity?)
+
+ /**
+ * Returns the parent of this Entity.
+ *
+ * @return The [Entity] that this Entity is attached to. Returns null if this Entity has no
+ * parent and is the root of its hierarchy.
+ */
+ public fun getParent(): Entity?
+
+ /**
+ * Sets an Entity to be represented in this coordinate space. From a User's perspective, as this
+ * Entity moves, the child Entity will move with it.
+ *
+ * @param child The [Entity] to be attached.
+ */
+ public fun addChild(child: Entity)
+
+ /* TODO b/362296608: Add a getChildren() method. */
+
+ /**
+ * Sets the pose for this Entity, relative to its parent.
+ *
+ * @param pose The [Pose] offset from the parent.
+ */
+ public fun setPose(pose: Pose)
+
+ /**
+ * Returns the pose for this entity, relative to its parent.
+ *
+ * @return Current [Pose] offset from the parent.
+ */
+ public fun getPose(): Pose
+
+ /**
+ * Sets the scale of this entity relative to its parent. This value will affect the rendering of
+ * this Entity's children. As the scale increases, this will uniformly stretch the content of
+ * the Entity.
+ *
+ * @param scale The uniform scale factor from the parent.
+ */
+ public fun setScale(scale: Float)
+
+ /**
+ * Returns the local scale of this entity, not inclusive of the parent's scale.
+ *
+ * @return Current uniform scale applied to self and children.
+ */
+ public fun getScale(): Float
+
+ /**
+ * Returns the accumulated scale of this Entity. This value includes the parent's world space
+ * scale.
+ *
+ * @return Total uniform scale applied to self and children.
+ */
+ public fun getWorldSpaceScale(): Float
+
+ /**
+ * Sets the dimensions in pixels for the Entity.
+ *
+ * @param dimensions Dimensions in pixels. (Z will be ignored)
+ * @deprecated ("This method is deprecated. Use BasePanelEntity<*>.setPixelDimensions()
+ * instead.")
+ */
+ public fun setSize(dimensions: Dimensions)
+
+ /** Returns the dimensions (in meters) for this Entity. */
+ public fun getSize(): Dimensions
+
+ /**
+ * Sets the alpha transparency of the Entity and its children. Values are in the range [0, 1]
+ * with 0 being fully transparent and 1 being fully opaque.
+ *
+ * This value will affect the rendering of this Entity's children. Children of this node will
+ * have their alpha levels multiplied by this value and any alpha of this entity's ancestors.
+ */
+ public fun setAlpha(alpha: Float)
+
+ /**
+ * Returns the alpha transparency set for this Entity.
+ *
+ * This does not necessarily equal the perceived alpha of the entity as the entity may have some
+ * alpha difference applied from its parent or the system.
+ */
+ public fun getAlpha(): Float
+
+ /**
+ * Returns the global alpha of this entity computed by multiplying the parent's global alpha to
+ * this entity's local alpha.
+ *
+ * This does not necessarily equal the perceived alpha of the entity as the entity may have some
+ * alpha difference applied from the system.
+ *
+ * @return Total [Float] alpha applied to this entity.
+ */
+ public fun getActivitySpaceAlpha(): Float
+
+ /**
+ * Sets the local hidden state of this Entity. When true, this Entity and all descendants will
+ * not be rendered in the scene. When the hidden state is false, an entity will be rendered if
+ * its ancestors are not hidden.
+ *
+ * @param hidden The new local hidden state of this Entity.
+ */
+ public fun setHidden(hidden: Boolean)
+
+ /**
+ * Returns the hidden status of this Entity.
+ *
+ * @param includeParents Whether to include the hidden status of parents in the returned value.
+ * @return If includeParents is true, the returned value will be true if this Entity or any of
+ * its ancestors is hidden. If includeParents is false, the local hidden state is returned.
+ * Regardless of the local hidden state, an entity will not be rendered if any of its
+ * ancestors are hidden.
+ */
+ public fun isHidden(includeParents: Boolean = true): Boolean
+
+ /**
+ * Disposes of any system resources held by this Entity, and transitively calls dispose() on all
+ * its children. Once disposed, this Entity is invalid and cannot be used again.
+ */
+ public fun dispose()
+
+ /**
+ * Sets alternate text for this entity to be consumed by Accessibility systems.
+ *
+ * @param text A11y content.
+ */
+ public fun setContentDescription(text: String)
+
+ /**
+ * Adds a Component to this Entity.
+ *
+ * @param component the Component to be added to the Entity.
+ * @return True if given Component is added to the Entity.
+ */
+ public fun addComponent(component: Component): Boolean
+
+ /**
+ * Removes the given Component from this Entity.
+ *
+ * @param component Component to be removed from this entity.
+ */
+ public fun removeComponent(component: Component)
+
+ /**
+ * Retrieves all Components of the given type [T] and its sub-types attached to this Entity.
+ *
+ * @param type The type of Component to retrieve.
+ * @return List<Component> of the given type attached to this Entity.
+ */
+ public fun <T : Component> getComponentsOfType(type: Class<out T>): List<T>
+
+ /**
+ * Retrieves all components attached to this Entity.
+ *
+ * @return List<Component> attached to this Entity.
+ */
+ public fun getComponents(): List<Component>
+
+ /** Remove all components from this Entity. */
+ public fun removeAllComponents()
+}
+
+/**
+ * ActivitySpace is an Entity used to track the system-managed pose and boundary of the volume
+ * associated with this Spatialized Activity. The Application cannot directly control this volume,
+ * but the system might update it in response to the User moving it or entering or exiting FullSpace
+ * mode.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class ActivitySpace
+private constructor(
+ rtActivitySpace: JxrPlatformAdapter.ActivitySpace,
+ entityManager: EntityManager,
+) : BaseEntity<JxrPlatformAdapter.ActivitySpace>(rtActivitySpace, entityManager) {
+
+ internal companion object {
+ internal fun create(
+ adapter: JxrPlatformAdapter,
+ entityManager: EntityManager
+ ): ActivitySpace = ActivitySpace(adapter.activitySpace, entityManager)
+ }
+
+ private val boundsListeners:
+ ConcurrentMap<
+ Consumer<Dimensions>,
+ JxrPlatformAdapter.ActivitySpace.OnBoundsChangedListener
+ > =
+ ConcurrentHashMap()
+
+ /**
+ * The listener registered when using the deprecated registerOnBoundsChangedListener method. We
+ * keep this reference so it can be removed using the corresponding unregister method.
+ */
+ // TODO: b/370538244 - remove with deprecated spatial state callbacks
+ private var registeredBoundsListener: Consumer<Dimensions>? = null
+
+ /**
+ * Retrieves a copy of the current bounds of this ActivitySpace.
+ *
+ * @return [Dimensions] representing the current bounds of this ActivitySpace.
+ */
+ // TODO b/370618648: remove suppression after API review is complete.
+ public fun getBounds(): Dimensions = rtEntity.bounds.toDimensions()
+
+ /**
+ * Adds the given [Consumer] as a listener to be invoked when this ActivitySpace's current
+ * boundary changes. [Consumer#accept(Dimensions)] will be invoked on the main thread.
+ *
+ * @param listener The Consumer to be invoked when this ActivitySpace's current boundary
+ * changes.
+ */
+ // TODO b/370618648: remove suppression after API review is complete.
+ public fun addBoundsChangedListener(listener: Consumer<Dimensions>): Unit =
+ addBoundsChangedListener(HandlerExecutor.mainThreadExecutor, listener)
+
+ /**
+ * Adds the given [Consumer] as a listener to be invoked when this ActivitySpace's current
+ * boundary changes. [Consumer#accept(Dimensions)] will be invoked on the given executor.
+ *
+ * @param callbackExecutor The executor on which to invoke the listener on.
+ * @param listener The Consumer to be invoked when this ActivitySpace's current boundary
+ * changes.
+ */
+ // TODO b/370618648: remove suppression after API review is complete.
+ public fun addBoundsChangedListener(
+ callbackExecutor: Executor,
+ listener: Consumer<Dimensions>
+ ) {
+ val rtListener: JxrPlatformAdapter.ActivitySpace.OnBoundsChangedListener =
+ JxrPlatformAdapter.ActivitySpace.OnBoundsChangedListener { rtDimensions ->
+ callbackExecutor.execute { listener.accept(rtDimensions.toDimensions()) }
+ }
+ boundsListeners.compute(
+ listener,
+ { _, _ ->
+ rtEntity.addOnBoundsChangedListener(rtListener)
+ rtListener
+ },
+ )
+ }
+
+ /**
+ * Releases the given [Consumer] from receiving updates when the ActivitySpace's boundary
+ * changes.
+ *
+ * @param listener The Consumer to be removed from receiving updates.
+ */
+ // TODO b/370618648: remove suppression after API review is complete.
+ public fun removeBoundsChangedListener(listener: Consumer<Dimensions>): Unit {
+ boundsListeners.computeIfPresent(
+ listener,
+ { _, rtListener ->
+ rtEntity.removeOnBoundsChangedListener(rtListener)
+ null // returning null from computeIfPresent removes this entry from the Map
+ },
+ )
+ }
+
+ /**
+ * Sets a callback to be invoked when the bounds of the ActivitySpace change. The callback will
+ * be dispatched on the UI thread.
+ *
+ * @param listener A ((Dimensions) -> Unit) callback, where Dimensions are in meters.
+ */
+ // TODO: b/370538244 - remove with deprecated spatial state callbacks
+ @Deprecated(message = "use addBoundsChangedListener(Consumer<Dimensions>)")
+ public fun registerOnBoundsChangedListener(listener: OnBoundsChangeListener) {
+ if (registeredBoundsListener != null) unregisterOnBoundsChangedListener()
+ registeredBoundsListener =
+ Consumer<Dimensions> { bounds -> listener.onBoundsChanged(bounds) }
+ addBoundsChangedListener(registeredBoundsListener!!)
+ }
+
+ /** Clears the listener set by [registerOnBoundsChangedListener]. */
+ // TODO: b/370538244 - remove with deprecated spatial state callbacks
+ @Deprecated(message = "use removeBoundsChangedListener(Consumer<Dimensions>)")
+ public fun unregisterOnBoundsChangedListener() {
+ if (registeredBoundsListener != null) {
+ removeBoundsChangedListener(registeredBoundsListener!!)
+ registeredBoundsListener = null
+ }
+ }
+
+ /**
+ * Registers a listener to be called when the underlying space has moved or changed.
+ *
+ * @param listener The listener to register if non-null, else stops listening if null.
+ * @param executor The executor to run the listener on. Defaults to SceneCore executor if null.
+ */
+ @JvmOverloads
+ @Suppress("ExecutorRegistration")
+ public fun setOnSpaceUpdatedListener(
+ listener: OnSpaceUpdatedListener?,
+ executor: Executor? = null,
+ ) {
+ rtEntity.setOnSpaceUpdatedListener(listener?.let { { it.onSpaceUpdated() } }, executor)
+ }
+}
+
+/** The BaseEntity is an implementation of Entity interface that wraps a platform entity. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public sealed class BaseEntity<out RtEntityType : RtEntity>(
+ internal val rtEntity: RtEntityType,
+ internal val entityManager: EntityManager,
+) : Entity, BaseActivityPose<JxrPlatformAdapter.ActivityPose>(rtEntity) {
+
+ init {
+ entityManager.setEntityForRtEntity(rtEntity, this)
+ }
+
+ private companion object {
+ private const val TAG = "BaseEntity"
+ }
+
+ private var dimensions: Dimensions = Dimensions()
+ private val componentList = mutableListOf<Component>()
+
+ override fun setParent(parent: Entity?) {
+ if (parent == null) {
+ rtEntity.parent = null
+ return
+ }
+
+ if (parent !is BaseEntity<RtEntity>) {
+ Log.e(TAG, "Parent must be a subclass of BaseEntity")
+ return
+ }
+ rtEntity.parent = parent.rtEntity
+ }
+
+ override fun getParent(): Entity? {
+ return rtEntity.parent?.let { entityManager.getEntityForRtEntity(it) }
+ }
+
+ override fun addChild(child: Entity) {
+ if (child !is BaseEntity<RtEntity>) {
+ Log.e(TAG, "Child must be a subclass of BaseEntity!")
+ return
+ }
+ rtEntity.addChild(child.rtEntity)
+ }
+
+ override fun setPose(pose: Pose) {
+ rtEntity.setPose(pose)
+ }
+
+ override fun getPose(): Pose {
+ return rtEntity.pose
+ }
+
+ override fun setScale(scale: Float) {
+ rtEntity.setScale(Vector3(scale, scale, scale))
+ }
+
+ override fun getScale(): Float {
+ return rtEntity.scale.x
+ }
+
+ override fun getWorldSpaceScale(): Float {
+ return rtEntity.worldSpaceScale.x
+ }
+
+ override fun setSize(dimensions: Dimensions) {
+ rtEntity.setSize(dimensions.toRtDimensions())
+ this.dimensions = dimensions
+ }
+
+ // TODO: b/328620113 - Get the dimensions from EntityImpl.
+ override fun getSize(): Dimensions = dimensions
+
+ override fun setAlpha(alpha: Float) {
+ rtEntity.alpha = alpha
+ }
+
+ override fun getAlpha(): Float = rtEntity.alpha
+
+ override fun getActivitySpaceAlpha(): Float = rtEntity.activitySpaceAlpha
+
+ override fun setHidden(hidden: Boolean): Unit = rtEntity.setHidden(hidden)
+
+ override fun isHidden(includeParents: Boolean): Boolean = rtEntity.isHidden(includeParents)
+
+ override fun dispose() {
+ removeAllComponents()
+ entityManager.removeEntity(this)
+ rtEntity.dispose()
+ }
+
+ override fun addComponent(component: Component): Boolean {
+ if (component.onAttach(this)) {
+ componentList.add(component)
+ return true
+ }
+ return false
+ }
+
+ override fun removeComponent(component: Component) {
+ if (componentList.contains(component)) {
+ component.onDetach(this)
+ componentList.remove(component)
+ }
+ }
+
+ override fun <T : Component> getComponentsOfType(type: Class<out T>): List<T> {
+ return componentList.filterIsInstance(type)
+ }
+
+ override fun getComponents(): List<Component> {
+ return componentList
+ }
+
+ override fun removeAllComponents() {
+ componentList.forEach { it.onDetach(this) }
+ componentList.clear()
+ }
+
+ override fun setContentDescription(text: String) {}
+}
+
+/**
+ * An Entity that itself has no content. ContentlessEntity is useful for organizing the placement,
+ * movement of a group of SceneCore Entities.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class ContentlessEntity
+private constructor(rtEntity: RtEntity, entityManager: EntityManager) :
+ BaseEntity<RtEntity>(rtEntity, entityManager) {
+ internal companion object {
+ /** Factory method to create ContentlessEntity entities. */
+ internal fun create(
+ adapter: JxrPlatformAdapter,
+ entityManager: EntityManager,
+ name: String,
+ pose: Pose = Pose.Identity,
+ ): Entity =
+ ContentlessEntity(
+ adapter.createEntity(pose, name, adapter.activitySpaceRootImpl),
+ entityManager,
+ )
+ }
+}
+
+/** Provides implementations for common Panel functionality. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public sealed class BasePanelEntity<out RtPanelEntityType : RtPanelEntity>(
+ private val rtPanelEntity: RtPanelEntityType,
+ entityManager: EntityManager,
+) : BaseEntity<RtPanelEntity>(rtPanelEntity, entityManager) {
+
+ /**
+ * Sets the corner radius of the PanelEntity.
+ *
+ * @param radius The radius of the corners, in meters.
+ * @throws IllegalArgumentException if radius is <= 0.0f.
+ */
+ public fun setCornerRadius(radius: Float) {
+ rtPanelEntity.setCornerRadius(radius)
+ }
+
+ /** Gets the corner radius of this PanelEntity in meters. Has a default value of 0. */
+ public fun getCornerRadius(): Float {
+ return rtPanelEntity.cornerRadius
+ }
+
+ /**
+ * Returns the dimensions of the view underlying this PanelEntity.
+ *
+ * @return The current (width, height) of the underlying surface in pixels.
+ */
+ public fun getPixelDimensions(): PixelDimensions {
+ return rtPanelEntity.getPixelDimensions().toPixelDimensions()
+ }
+
+ /**
+ * Sets the pixel (not Dp) dimensions of the view underlying this PanelEntity. Calling this
+ * might cause the layout of the Panel contents to change. Updating this will not cause the
+ * scale or pixel density to change.
+ *
+ * @param pxDimensions The [PixelDimensions] of the underlying surface to set.
+ */
+ public fun setPixelDimensions(pxDimensions: PixelDimensions) {
+ rtPanelEntity.setPixelDimensions(pxDimensions.toRtPixelDimensions())
+ }
+
+ /**
+ * Gets the number of pixels per meter for this panel. This value reflects changes to scale,
+ * including parent scale.
+ *
+ * @return Vector3 scale applied to pixels within the Panel. (Z will be 0)
+ */
+ public fun getPixelDensity(): Vector3 {
+ return rtPanelEntity.pixelDensity
+ }
+
+ /**
+ * Returns the spatial size of this Panel in meters. This includes any scaling applied to this
+ * panel by itself or its parents, which might be set via changes to setScale.
+ *
+ * @return [Dimensions] size of this panel in meters. (Z will be 0)
+ */
+ override fun getSize(): Dimensions {
+ return rtPanelEntity.getSize().toDimensions()
+ }
+}
+
+/**
+ * GltfModelEntity is a concrete implementation of Entity that hosts a glTF model.
+ *
+ * Note: The size property of this Entity is always reported as {0, 0, 0}, regardless of the actual
+ * size of the model.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class GltfModelEntity
+private constructor(rtEntity: JxrPlatformAdapter.GltfEntity, entityManager: EntityManager) :
+ BaseEntity<JxrPlatformAdapter.GltfEntity>(rtEntity, entityManager) {
+ // TODO: b/362368652 - Add an OnAnimationEvent() Listener interface
+
+ /** Specifies the current animation state of the GltfModelEntity. */
+ @IntDef(AnimationState.PLAYING, AnimationState.STOPPED)
+ @Retention(AnnotationRetention.SOURCE)
+ internal annotation class AnimationStateValue
+
+ public object AnimationState {
+ public const val PLAYING: Int = 0
+ public const val STOPPED: Int = 1
+ }
+
+ internal companion object {
+ /**
+ * Factory method for GltfModelEntity.
+ *
+ * @param adapter Jetpack XR platform adapter.
+ * @param model [GltfModel] which this entity will display.
+ * @param pose Pose for this [GltfModelEntity], relative to its parent.
+ */
+ internal fun create(
+ adapter: JxrPlatformAdapter,
+ entityManager: EntityManager,
+ model: GltfModel,
+ pose: Pose = Pose.Identity,
+ ): GltfModelEntity =
+ GltfModelEntity(
+ adapter.createGltfEntity(pose, model.model, adapter.activitySpaceRootImpl),
+ entityManager,
+ )
+ }
+
+ /** Returns the current animation state of this glTF entity. */
+ @AnimationStateValue
+ public fun getAnimationState(): Int {
+ return when (rtEntity.animationState) {
+ JxrPlatformAdapter.GltfEntity.AnimationState.PLAYING -> return AnimationState.PLAYING
+ JxrPlatformAdapter.GltfEntity.AnimationState.STOPPED -> return AnimationState.STOPPED
+ else -> AnimationState.STOPPED
+ }
+ }
+
+ /**
+ * Starts the animation with the given name.
+ *
+ * This method must be called from the main thread.
+ * https://developer.android.com/guide/components/processes-and-threads
+ *
+ * @param animationName The name of the animation to start. If null, the first animation found
+ * in the glTF will be played.
+ * @param loop Whether the animation should loop.
+ */
+ @MainThread
+ @JvmOverloads
+ public fun startAnimation(loop: Boolean, animationName: String? = null) {
+ rtEntity.startAnimation(loop, animationName)
+ }
+
+ /**
+ * Stops the animation of the glTF entity.
+ *
+ * This method must be called from the main thread.
+ * https://developer.android.com/guide/components/processes-and-threads
+ */
+ @MainThread
+ public fun stopAnimation() {
+ rtEntity.stopAnimation()
+ }
+}
+
+/** StereoSurfaceEntity is a concrete implementation of Entity that hosts a StereoSurface Panel. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class StereoSurfaceEntity
+private constructor(
+ rtEntity: JxrPlatformAdapter.StereoSurfaceEntity,
+ entityManager: EntityManager,
+) : BaseEntity<JxrPlatformAdapter.StereoSurfaceEntity>(rtEntity, entityManager) {
+
+ /**
+ * Specifies how the surface content will be routed for stereo viewing. Applications must render
+ * into the surface in accordance with what is specified here in order for the compositor to
+ * correctly produce a stereoscopic view to the user.
+ *
+ * Values here match values from androidx.media3.common.C.StereoMode in
+ * //third_party/java/android_libs/media:common
+ */
+ @IntDef(StereoMode.MONO, StereoMode.TOP_BOTTOM, StereoMode.SIDE_BY_SIDE)
+ @Retention(AnnotationRetention.SOURCE)
+ internal annotation class StereoModeValue
+
+ public object StereoMode {
+ // Each eye will see the entire surface (no separation)
+ public const val MONO: Int = 0
+ // The [bottom, top] halves of the surface will map to [left, right] eyes
+ public const val TOP_BOTTOM: Int = 1
+ // The [left, right] halves of the surface will map to [left, right] eyes
+ public const val SIDE_BY_SIDE: Int = 2
+ }
+
+ internal companion object {
+ private fun getRtStereoMode(stereoMode: Int): Int {
+ return when (stereoMode) {
+ StereoMode.MONO -> JxrPlatformAdapter.StereoSurfaceEntity.StereoMode.MONO
+ StereoMode.TOP_BOTTOM ->
+ JxrPlatformAdapter.StereoSurfaceEntity.StereoMode.TOP_BOTTOM
+ else -> JxrPlatformAdapter.StereoSurfaceEntity.StereoMode.SIDE_BY_SIDE
+ }
+ }
+
+ /**
+ * Factory method for StereoSurfaceEntity.
+ *
+ * @param adapter JxrPlatformAdapter to use.
+ * @param entityManager A SceneCore EntityManager
+ * @param stereoMode An [Int] which defines how surface subregions map to eyes
+ * @param dimensions A [Dimensions] which specifies the size of the canvas relative to
+ * parent
+ * @param pose Pose for this StereoSurface entity, relative to its parent.
+ */
+ internal fun create(
+ adapter: JxrPlatformAdapter,
+ entityManager: EntityManager,
+ stereoMode: Int = StereoMode.SIDE_BY_SIDE,
+ dimensions: Dimensions = Dimensions(1.0f, 1.0f, 1.0f),
+ pose: Pose = Pose.Identity,
+ ): StereoSurfaceEntity =
+ StereoSurfaceEntity(
+ adapter.createStereoSurfaceEntity(
+ getRtStereoMode(stereoMode),
+ dimensions.toRtDimensions(),
+ pose,
+ adapter.activitySpaceRootImpl,
+ ),
+ entityManager,
+ )
+ }
+
+ /**
+ * Changes the width and height of the "spatial canvas" which the surface is mapped to. (0,0,0)
+ * for this Entity is the canvas's center. (In unscaled ActivitySpace, this is 1 unit per meter)
+ * Width and height <= 0 are unsupported (Depth is ignored).
+ */
+ public var dimensions: Dimensions
+ get() = rtEntity.dimensions.toDimensions()
+ set(value) {
+ rtEntity.dimensions = value.toRtDimensions()
+ }
+
+ /**
+ * Returns a surface into which the application can render stereo image content.
+ *
+ * This method must be called from the main thread.
+ * https://developer.android.com/guide/components/processes-and-threads
+ */
+ @MainThread
+ public fun getSurface(): Surface {
+ return rtEntity.surface
+ }
+}
+
+/** PanelEntity creates a spatial panel in Android XR. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public open class PanelEntity
+internal constructor(
+ rtEntity: JxrPlatformAdapter.PanelEntity,
+ entityManager: EntityManager,
+ // TODO(ricknels): move isMainPanelEntity check to JxrPlatformAdapter.
+ public val isMainPanelEntity: Boolean = false,
+) : BasePanelEntity<JxrPlatformAdapter.PanelEntity>(rtEntity, entityManager) {
+
+ internal companion object {
+ /**
+ * Factory method for PanelEntity.
+ *
+ * @param adapter JxrPlatformAdapter to use.
+ * @param view View to insert in this panel.
+ * @param surfaceDimensionsPx Dimensions for the underlying surface for the given view.
+ * @param dimensions The size of this spatial Panel Entity in Meters.
+ * @param name Name of this panel.
+ * @param context Activity which created this panel.
+ * @param pose Pose for this panel, relative to its parent.
+ */
+ internal fun create(
+ adapter: JxrPlatformAdapter,
+ entityManager: EntityManager,
+ view: View,
+ surfaceDimensionsPx: Dimensions,
+ dimensions: Dimensions,
+ name: String,
+ context: Context,
+ pose: Pose = Pose.Identity,
+ ): PanelEntity =
+ PanelEntity(
+ adapter.createPanelEntity(
+ pose,
+ view,
+ PixelDimensions(
+ surfaceDimensionsPx.width.toInt(),
+ surfaceDimensionsPx.height.toInt()
+ )
+ .toRtPixelDimensions(),
+ dimensions.toRtDimensions(),
+ name,
+ context,
+ adapter.activitySpaceRootImpl,
+ ),
+ entityManager,
+ )
+
+ /** Returns the PanelEntity backed by the main window for the Activity. */
+ internal fun createMainPanelEntity(
+ adapter: JxrPlatformAdapter,
+ entityManager: EntityManager,
+ ): PanelEntity =
+ PanelEntity(adapter.mainPanelEntity, entityManager, isMainPanelEntity = true)
+ }
+}
+
+/**
+ * ActivityPanelEntity creates a spatial panel for embedding an Activity in Android XR. Users can
+ * either use an intent to launch an activity in the given panel or provide an instance of activity
+ * to move into this panel. Calling dispose() on this entity will destroy the underlying activity.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class ActivityPanelEntity
+private constructor(
+ private val rtActivityPanelEntity: JxrPlatformAdapter.ActivityPanelEntity,
+ entityManager: EntityManager,
+) : PanelEntity(rtActivityPanelEntity, entityManager) {
+
+ /**
+ * Launches an activity in the given panel. Subsequent calls to this method will replace the
+ * already existing activity in the panel with the new one. If the intent fails to launch the
+ * activity, the panel will not be visible. Note this will not update the dimensions of the
+ * surface underlying the panel. The Activity will be letterboxed as required to fit the size of
+ * the panel. The underlying surface can be resized by calling setPixelDimensions().
+ *
+ * @param intent Intent to launch the activity.
+ * @param bundle Bundle to pass to the activity, can be null.
+ */
+ @JvmOverloads
+ public fun launchActivity(intent: Intent, bundle: Bundle? = null) {
+ rtActivityPanelEntity.launchActivity(intent, bundle)
+ }
+
+ /**
+ * Moves the given activity into this panel. Note this will not update the dimensions of the
+ * surface underlying the panel. The Activity will be letterboxed as required to fit the size of
+ * the panel. The underlying surface can be resized by calling setPixelDimensions().
+ *
+ * @param activity Activity to move into this panel.
+ */
+ public fun moveActivity(activity: Activity) {
+ rtActivityPanelEntity.moveActivity(activity)
+ }
+
+ internal companion object {
+ /**
+ * Factory method for ActivityPanelEntity.
+ *
+ * @param adapter JxrPlatformAdapter to use.
+ * @param windowBoundsPx Bounds for the underlying surface for the given view.
+ * @param name Name of this panel.
+ * @param hostActivity Activity which created this panel.
+ * @param pose Pose for this panel, relative to its parent.
+ */
+ internal fun create(
+ adapter: JxrPlatformAdapter,
+ entityManager: EntityManager,
+ windowBoundsPx: PixelDimensions,
+ name: String,
+ hostActivity: Activity,
+ pose: Pose = Pose.Identity,
+ ): ActivityPanelEntity =
+ ActivityPanelEntity(
+ adapter.createActivityPanelEntity(
+ pose,
+ windowBoundsPx.toRtPixelDimensions(),
+ name,
+ hostActivity,
+ adapter.activitySpaceRootImpl,
+ ),
+ entityManager,
+ )
+ }
+}
+
+/** TODO - Convert AnchorEntity into a Space (This will remove setParent) */
+
+/**
+ * An AnchorEntity is created to track a Pose relative to some position or surface in the "Real
+ * World." Children of this Entity will remain positioned relative to that location in the real
+ * world, for the purposes of creating Augmented Reality experiences.
+ *
+ * Note that Anchors are only relative to the "real world", and not virtual environments. Also,
+ * calling setParent() on an AnchorEntity has no effect, as the parenting of an Anchor is controlled
+ * by the system.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class AnchorEntity
+private constructor(rtEntity: JxrPlatformAdapter.AnchorEntity, entityManager: EntityManager) :
+ BaseEntity<JxrPlatformAdapter.AnchorEntity>(rtEntity, entityManager) {
+ private val state = AtomicReference(rtEntity.state.fromRtState())
+ private val persistState = AtomicReference(rtEntity.persistState.fromRtPersistState())
+
+ private var onStateChangedListener: OnStateChangedListener? = null
+
+ /** Specifies the current tracking state of the Anchor. */
+ @Target(AnnotationTarget.TYPE)
+ @IntDef(State.ANCHORED, State.UNANCHORED, State.TIMEDOUT, State.ERROR)
+ @Retention(AnnotationRetention.SOURCE)
+ internal annotation class StateValue
+
+ public object State {
+ /**
+ * The ANCHORED state means that this Anchor is being actively tracked and updated by the
+ * perception stack. The application should expect children to maintain their relative
+ * positioning to the system's best understanding of a pose in the real world.
+ */
+ public const val ANCHORED: Int = 0
+ /**
+ * An UNANCHORED state could mean that the perception stack hasn't found an anchor for this
+ * Space, that it has lost tracking.
+ */
+ public const val UNANCHORED: Int = 1
+ /**
+ * The AnchorEntity timed out while searching for an underlying anchor. This it is not
+ * possible to recover the AnchorEntity.
+ */
+ public const val TIMEDOUT: Int = 2
+ /**
+ * The ERROR state means that something has gone wrong and this AnchorEntity is invalid
+ * without the possibility of recovery.
+ */
+ public const val ERROR: Int = 3
+ }
+
+ /** Specifies the current persist state of the Anchor. */
+ public enum class PersistState {
+ /** The anchor hasn't been requested to persist. */
+ PERSIST_NOT_REQUESTED,
+ /** The anchor is requested to persist but hasn't been persisted yet. */
+ PERSIST_PENDING,
+ /** The anchor is persisted successfully. */
+ PERSISTED,
+ }
+
+ /** Returns the current tracking state for this AnchorEntity. */
+ public fun getState(): @StateValue Int = state.get()
+
+ /** Registers a listener callback to be issued when an anchor's state changes. */
+ @Suppress("ExecutorRegistration")
+ public fun setOnStateChangedListener(onStateChangedListener: OnStateChangedListener?) {
+ this.onStateChangedListener = onStateChangedListener
+ }
+
+ /** Updates the current state. */
+ private fun setState(newState: @StateValue Int) {
+ state.set(newState)
+ onStateChangedListener?.onStateChanged(newState)
+ }
+
+ /** Gets the current PersistState. */
+ public fun getPersistState(): PersistState = persistState.get()
+
+ /**
+ * Requests to persist the anchor. If the anchor's State is not ANCHORED, no request will be
+ * sent and null is returned. If the request is sent successfully, returns an UUID of the anchor
+ * immediately; otherwise returns null. After this call, client should use getPersistState() to
+ * check the PersistState of the anchor. If the anchor's PersistState becomes PERSISTED before
+ * the app is closed the anchor can be recreated in a new session by calling
+ * Session.createPersistedAnchorEntity(uuid). If the PersistState doesn't become PERSISTED
+ * before the app is closed, the recreation will fail.
+ */
+ public fun persist(): UUID? {
+ if (state.get() != State.ANCHORED) {
+ Log.e(TAG, "Cannot persist an anchor that is not in the ANCHORED state.")
+ return null
+ }
+ val uuid = rtEntity.persist()
+ if (uuid == null) {
+ Log.e(TAG, "Failed to get a UUID for the anchor.")
+ return null
+ }
+
+ rtEntity.registerPersistStateChangeListener { newRtPersistState ->
+ persistState.set(newRtPersistState.fromRtPersistState())
+ }
+ return uuid
+ }
+
+ /**
+ * Loads the ARCore for XR Anchor using a Jetpack XR Runtime session.
+ *
+ * @param session the Jetpack XR Runtime session to load the Anchor from.
+ * @return the ARCore for XR Anchor corresponding to the native pointer.
+ */
+ // TODO(b/373711152) : Remove this method once the ARCore for XR API migration is done.
+ public fun getAnchor(session: PerceptionSession): Anchor {
+ return Anchor.loadFromNativePointer(session, rtEntity.nativePointer())
+ }
+
+ internal companion object {
+ private const val TAG = "AnchorEntity"
+
+ /**
+ * Factory method for AnchorEntity.
+ *
+ * @param adapter JxrPlatformAdapter to use.
+ * @param bounds Bounds for this Anchor Entity.
+ * @param planeType Orientation for the plane to which this Anchor should attach.
+ * @param planeSemantic Semantics for the plane to which this Anchor should attach.
+ * @param timeout Maximum time to search for the anchor, if a suitable plane is not found
+ * within the timeout time the AnchorEntity state will be set to TIMED_OUT.
+ */
+ internal fun create(
+ adapter: JxrPlatformAdapter,
+ entityManager: EntityManager,
+ bounds: Dimensions,
+ planeType: @PlaneTypeValue Int,
+ planeSemantic: @PlaneSemanticValue Int,
+ timeout: Duration = Duration.ZERO,
+ ): AnchorEntity {
+ val rtAnchorEntity =
+ adapter.createAnchorEntity(
+ bounds.toRtDimensions(),
+ planeType.toRtPlaneType(),
+ planeSemantic.toRtPlaneSemantic(),
+ timeout,
+ )
+ return create(rtAnchorEntity, entityManager)
+ }
+
+ /**
+ * Factory method for AnchorEntity.
+ *
+ * @param anchor Anchor to create an AnchorEntity for.
+ */
+ internal fun create(
+ adapter: JxrPlatformAdapter,
+ entityManager: EntityManager,
+ anchor: Anchor,
+ ): AnchorEntity {
+ val rtAnchorEntity = adapter.createAnchorEntity(anchor)
+ return create(rtAnchorEntity, entityManager)
+ }
+
+ /**
+ * Factory method for AnchorEntity.
+ *
+ * @param rtAnchorEntity Runtime AnchorEntity instance.
+ */
+ internal fun create(
+ rtAnchorEntity: JxrPlatformAdapter.AnchorEntity,
+ entityManager: EntityManager,
+ ): AnchorEntity {
+ val anchorEntity = AnchorEntity(rtAnchorEntity, entityManager)
+ rtAnchorEntity.setOnStateChangedListener { newRtState ->
+ when (newRtState) {
+ JxrPlatformAdapter.AnchorEntity.State.UNANCHORED ->
+ anchorEntity.setState(State.UNANCHORED)
+ JxrPlatformAdapter.AnchorEntity.State.ANCHORED ->
+ anchorEntity.setState(State.ANCHORED)
+ JxrPlatformAdapter.AnchorEntity.State.TIMED_OUT ->
+ anchorEntity.setState(State.TIMEDOUT)
+ JxrPlatformAdapter.AnchorEntity.State.ERROR ->
+ anchorEntity.setState(State.ERROR)
+ }
+ }
+ return anchorEntity
+ }
+
+ /**
+ * Factory method for AnchorEntity.
+ *
+ * @param adapter JxrPlatformAdapter to use.
+ * @param uuid UUID of the persisted Anchor Entity to create.
+ * @param timeout Maximum time to search for the anchor, if a persisted anchor isn't located
+ * within the timeout time the AnchorEntity state will be set to TIMED_OUT.
+ */
+ internal fun create(
+ adapter: JxrPlatformAdapter,
+ entityManager: EntityManager,
+ uuid: UUID,
+ timeout: Duration = Duration.ZERO,
+ ): AnchorEntity {
+ val rtAnchorEntity = adapter.createPersistedAnchorEntity(uuid, timeout)
+ val anchorEntity = AnchorEntity(rtAnchorEntity, entityManager)
+ rtAnchorEntity.setOnStateChangedListener { newRtState ->
+ when (newRtState) {
+ JxrPlatformAdapter.AnchorEntity.State.UNANCHORED ->
+ anchorEntity.setState(State.UNANCHORED)
+ JxrPlatformAdapter.AnchorEntity.State.ANCHORED ->
+ anchorEntity.setState(State.ANCHORED)
+ JxrPlatformAdapter.AnchorEntity.State.TIMED_OUT ->
+ anchorEntity.setState(State.TIMEDOUT)
+ JxrPlatformAdapter.AnchorEntity.State.ERROR ->
+ anchorEntity.setState(State.ERROR)
+ }
+ }
+ return anchorEntity
+ }
+ }
+
+ /**
+ * Extension function that converts [JxrPlatformAdapter.AnchorEntity.State] to
+ * [AnchorEntity.State].
+ */
+ private fun JxrPlatformAdapter.AnchorEntity.State.fromRtState() =
+ when (this) {
+ JxrPlatformAdapter.AnchorEntity.State.UNANCHORED -> State.UNANCHORED
+ JxrPlatformAdapter.AnchorEntity.State.ANCHORED -> State.ANCHORED
+ JxrPlatformAdapter.AnchorEntity.State.TIMED_OUT -> State.TIMEDOUT
+ JxrPlatformAdapter.AnchorEntity.State.ERROR -> State.ERROR
+ }
+
+ /**
+ * Extension function that converts [JxrPlatformAdapter.AnchorEntity.PersistState] to
+ * [AnchorEntity.PersistState].
+ */
+ private fun JxrPlatformAdapter.AnchorEntity.PersistState.fromRtPersistState() =
+ when (this) {
+ JxrPlatformAdapter.AnchorEntity.PersistState.PERSIST_NOT_REQUESTED ->
+ PersistState.PERSIST_NOT_REQUESTED
+ JxrPlatformAdapter.AnchorEntity.PersistState.PERSIST_PENDING ->
+ PersistState.PERSIST_PENDING
+ JxrPlatformAdapter.AnchorEntity.PersistState.PERSISTED -> PersistState.PERSISTED
+ }
+
+ /**
+ * Registers a listener to be called when the Anchor moves relative to its underlying space.
+ *
+ * <p> The callback is triggered by any anchor movements such as those made by the underlying
+ * perception stack to maintain the anchor's position relative to the real world. Any cached
+ * data relative to the activity space or any other "space" should be updated when this callback
+ * is triggered.
+ *
+ * @param listener The listener to register if non-null, else stops listening if null.
+ * @param executor The executor to run the listener on. Defaults to SceneCore executor if null.
+ */
+ @JvmOverloads
+ @Suppress("ExecutorRegistration")
+ public fun setOnSpaceUpdatedListener(
+ listener: OnSpaceUpdatedListener?,
+ executor: Executor? = null,
+ ) {
+ rtEntity.setOnSpaceUpdatedListener(listener?.let { { it.onSpaceUpdated() } }, executor)
+ }
+}
+
+// TODO: b/370538244 - remove with deprecated spatial state callbacks
+@Deprecated(message = "Use addBoundsChangedListener(Consumer<Dimensions>)")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun interface OnBoundsChangeListener {
+ public fun onBoundsChanged(bounds: Dimensions) // Dimensions are in meters.
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun interface OnStateChangedListener {
+ public fun onStateChanged(newState: @AnchorEntity.StateValue Int)
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun interface OnSpaceUpdatedListener {
+ public fun onSpaceUpdated()
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/EntityManager.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/EntityManager.kt
new file mode 100644
index 0000000..4fe4764
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/EntityManager.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+@file:Suppress("BanConcurrentHashMap")
+
+package androidx.xr.scenecore
+
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity as RuntimeEntity
+import java.util.concurrent.ConcurrentHashMap
+
+/** Manages the mapping between [RuntimeEntity] and [Entity] for a given SceneCore [Session]. */
+internal class EntityManager {
+ private val rtEntityEntityMap = ConcurrentHashMap<RuntimeEntity, Entity>()
+
+ /**
+ * Returns the [Entity] associated with the given [RuntimeEntity].
+ *
+ * @param rtEntity the [RuntimeEntity] to get the associated [Entity] for.
+ * @return [java.util.Optional] containing the [Entity] associated with the given
+ * [RuntimeEntity], or empty if no such [Entity] exists.
+ */
+ internal fun getEntityForRtEntity(rtEntity: RuntimeEntity): Entity? =
+ rtEntityEntityMap[rtEntity]
+
+ /**
+ * Sets the [Entity] associated with the given [RuntimeEntity].
+ *
+ * @param rtEntity the [RuntimeEntity] to set the associated [Entity] for.
+ * @param entity the [Entity] to associate with the given [RuntimeEntity].
+ */
+ internal fun setEntityForRtEntity(rtEntity: RuntimeEntity, entity: Entity) {
+ rtEntityEntityMap[rtEntity] = entity
+ }
+
+ /**
+ * Inline function to get all entities of a given type.
+ *
+ * @param T the type of [Entity] to return.
+ * @return a list of all [Entity]s of type [T] (including subtypes of [T]).
+ */
+ internal inline fun <reified T : Entity> getEntities(): List<T> {
+ return rtEntityEntityMap.values.filterIsInstance<T>()
+ }
+
+ /**
+ * Returns all [Entity]s of the given type or its subtypes.
+ *
+ * @param type the type of [Entity] to return.
+ * @return a list of all [Entity]s of the given type or its subtypes.
+ */
+ internal fun <T : Entity> getEntitiesOfType(type: Class<out T>): List<T> =
+ rtEntityEntityMap.values.filterIsInstance(type)
+
+ /**
+ * Returns a collection of all [Entity]s.
+ *
+ * @return a collection of all [Entity]s.
+ */
+ internal fun getAllEntities(): Collection<Entity> {
+ return rtEntityEntityMap.values
+ }
+
+ /**
+ * Removes the given [Entity] from the map.
+ *
+ * @param entity the [Entity] to remove from the map.
+ */
+ internal fun removeEntity(entity: Entity) {
+ rtEntityEntityMap.remove((entity as BaseEntity<*>).rtEntity)
+ }
+
+ /**
+ * Removes the given [JxrPlatformAdapter.Entity] from the map.
+ *
+ * @param entity the [JxrPlatformAdapter.Entity] to remove from the map.
+ */
+ internal fun removeEntity(entity: JxrPlatformAdapter.Entity) {
+ rtEntityEntityMap.remove(entity)
+ }
+
+ /** Clears the EntityManager. */
+ internal fun clear() {
+ rtEntityEntityMap.clear()
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ExrImage.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ExrImage.kt
new file mode 100644
index 0000000..6441e0c
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ExrImage.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import androidx.annotation.RestrictTo
+import androidx.xr.scenecore.JxrPlatformAdapter.ExrImageResource as RtExrImage
+
+/** Interface for image formats in SceneCore. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Image
+
+/**
+ * ExrImage represents an EXR Image resource in SceneCore. EXR images are used by the [Environment]
+ * for drawing skyboxes.
+ */
+// TODO(b/319269278): Make this and GltfModel derive from a common Resource base class which has
+// async helpers.
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class ExrImage internal constructor(public val image: RtExrImage) : Image {
+
+ internal companion object {
+ internal fun create(runtime: JxrPlatformAdapter, name: String): ExrImage {
+ val exrImageFuture = runtime.loadExrImageByAssetName(name)
+ // TODO: b/323022003 - Implement async loading of [ExrImage].
+ return ExrImage(exrImageFuture!!.get())
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ExrImage
+
+ // Perform a structural equality check on the underlying image.
+ if (image != other.image) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return image.hashCode()
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/InputEvent.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/InputEvent.kt
new file mode 100644
index 0000000..1c9e477
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/InputEvent.kt
@@ -0,0 +1,290 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.math.Matrix4
+import androidx.xr.runtime.math.Vector3
+import kotlin.annotation.Retention
+
+/** Listener for [InputEvent]s. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun interface InputEventListener {
+ public fun onInputEvent(inputEvent: InputEvent)
+}
+
+/**
+ * Defines input events for XRCore.
+ *
+ * @param source Type of device that generated this event.
+ * @param pointerType Type of the individual pointer.
+ * @param timestamp Timestamp from android.os.SystemClock#uptimeMillis time base.
+ * @param origin The origin of the ray in the receiver's activity space.
+ * @param direction A point indicating the direction the ray is pointing, in the receiver's activity
+ * space.
+ * @param action Actions similar to Android's
+ * [MotionEvent](https://developer.android.com/reference/android/view/MotionEvent") for keeping
+ * track of a sequence of events on the same target, e.g., * HOVER_ENTER -> HOVER_MOVE ->
+ * HOVER_EXIT * DOWN -> MOVE -> UP
+ * @param hitInfo Info about the first scene entity (closest to the ray origin) that was hit by the
+ * input ray, if any. This will be null if no entity was hit. Note that the hit entity remains the
+ * same during an ongoing DOWN -> MOVE -> UP action, even if the pointer stops hitting the entity
+ * during the action.
+ * @param secondaryHitInfo Info about the second scene entity from the same task that was hit, if
+ * any.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class InputEvent(
+ @Source public val source: Int,
+ @PointerType public val pointerType: Int,
+ public val timestamp: Long,
+ public val origin: Vector3,
+ public val direction: Vector3,
+ @Action public val action: Int,
+ public val hitInfo: HitInfo? = null,
+ public val secondaryHitInfo: HitInfo? = null,
+) {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is InputEvent) return false
+
+ if (source != other.source) return false
+ if (pointerType != other.pointerType) return false
+ if (timestamp != other.timestamp) return false
+ if (origin != other.origin) return false
+ if (direction != other.direction) return false
+ if (action != other.action) return false
+ if (hitInfo != other.hitInfo) return false
+ if (secondaryHitInfo != other.secondaryHitInfo) return false
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = source.hashCode()
+ result = 31 * result + pointerType.hashCode()
+ result = 31 * result + timestamp.hashCode()
+ result = 31 * result + origin.hashCode()
+ result = 31 * result + direction.hashCode()
+ result = 31 * result + action.hashCode()
+ result = 31 * result + (hitInfo?.hashCode() ?: 0)
+ result = 31 * result + (secondaryHitInfo?.hashCode() ?: 0)
+ return result
+ }
+
+ public companion object {
+ /*
+ * There's a possibility of ABI mismatch here when the concrete platformAdapter starts receiving
+ * input events with an updated field, such as if a newer source or pointer type has been added
+ * to the underlying platform OS. We need to perform a version check when the platformAdapter is
+ * constructed to ensure that the application doesn't receive anything it wasn't compiled
+ * against.
+ */
+ // TODO: b/343468347 - Implement version check for xr extensions when loading runtime impl
+
+ /** Unknown source. */
+ public const val SOURCE_UNKNOWN: Int = 0
+
+ /**
+ * Event is based on the user's head. Ray origin is at average between eyes, pushed out to
+ * the near clipping plane for both eyes and points in direction head is facing. Action
+ * state is based on volume up button being depressed.
+ *
+ * Events from this source are considered sensitive and hover events are never sent.
+ */
+ public const val SOURCE_HEAD: Int = 1
+
+ /**
+ * Event is based on (one of) the user's controller(s). Ray origin and direction are for a
+ * controller aim pose as defined by
+ * [OpenXR](https://registry.khronos.org/OpenXR/specs/1.1/html/xrspec.html#semantic-paths-standard-pose-identifiers).
+ * Action state is based on the primary button on the controller, usually the bottom-most
+ * face button.
+ */
+ public const val SOURCE_CONTROLLER: Int = 2
+
+ /**
+ * Event is based on one of the user's hands. Ray is a hand aim pose, with origin between
+ * thumb and forefinger and points in direction based on hand orientation. Action state is
+ * based on a pinch gesture.
+ */
+ public const val SOURCE_HANDS: Int = 3
+
+ /**
+ * Event is based on a 2D mouse pointing device. Ray origin behaves the same as for
+ * SOURCE_HEAD and points in direction based on mouse movement. During a drag, the ray
+ * origin moves approximating hand motion. The scrollwheel moves the ray away from / towards
+ * the user. Action state is based on the primary mouse button.
+ */
+ public const val SOURCE_MOUSE: Int = 4
+
+ /**
+ * Event is based on a mix of the head, eyes, and hands. Ray origin is at average between
+ * eyes and points in direction based on a mix of eye gaze direction and hand motion. During
+ * a two-handed zoom/rotate gesture, left/right pointer events will be issued; otherwise,
+ * default events are issued based on the gaze ray. Action state is based on if the user has
+ * done a pinch gesture or not.
+ *
+ * Events from this source are considered sensitive and hover events are never sent.
+ */
+ public const val SOURCE_GAZE_AND_GESTURE: Int = 5
+
+ /**
+ * Default pointer type for the source (no handedness). Occurs for SOURCE_UNKNOWN,
+ * SOURCE_HEAD, SOURCE_MOUSE, and SOURCE_GAZE_AND_GESTURE.
+ */
+ public const val POINTER_TYPE_DEFAULT: Int = 0
+
+ /**
+ * Left hand / controller pointer. Occurs for SOURCE_CONTROLLER, SOURCE_HANDS, and
+ * SOURCE_GAZE_AND_GESTURE.
+ */
+ public const val POINTER_TYPE_LEFT: Int = 1
+
+ /**
+ * Right hand / controller pointer. Occurs for SOURCE_CONTROLLER, SOURCE_HANDS, and
+ * SOURCE_GAZE_AND_GESTURE.
+ */
+ public const val POINTER_TYPE_RIGHT: Int = 2
+
+ /** The primary action button or gesture was just pressed / started. */
+ public const val ACTION_DOWN: Int = 0
+
+ /**
+ * The primary action button or gesture was just released / stopped. The hit info represents
+ * the node that was originally hit (ie, as provided in the ACTION_DOWN event).
+ */
+ public const val ACTION_UP: Int = 1
+
+ /**
+ * The primary action button or gesture was pressed/active in the previous event, and is
+ * still pressed/active. The hit info represents the node that was originally hit (ie, as
+ * provided in the ACTION_DOWN event). The hit position may be null if the pointer is no
+ * longer hitting that node.
+ */
+ public const val ACTION_MOVE: Int = 2
+
+ /**
+ * While the primary action button or gesture was held, the pointer was disabled. This
+ * happens if you are using controllers and the battery runs out, or if you are using a
+ * source that transitions to a new pointer type, eg SOURCE_GAZE_AND_GESTURE.
+ */
+ public const val ACTION_CANCEL: Int = 3
+
+ /**
+ * The primary action button or gesture is not pressed, and the pointer ray continued to hit
+ * the same node. The hit info represents the node that was hit (may be null if pointer
+ * capture is enabled).
+ *
+ * Hover input events are never provided for sensitive source types.
+ */
+ public const val ACTION_HOVER_MOVE: Int = 4
+
+ /**
+ * The primary action button or gesture is not pressed, and the pointer ray started to hit a
+ * new node. The hit info represents the node that is being hit (may be null if pointer
+ * capture is enabled).
+ *
+ * Hover input events are never provided for sensitive source types.
+ */
+ public const val ACTION_HOVER_ENTER: Int = 5
+
+ /**
+ * The primary action button or gesture is not pressed, and the pointer ray stopped hitting
+ * the node that it was previously hitting. The hit info represents the node that was being
+ * hit (may be null if pointer capture is enabled).
+ *
+ * Hover input events are never provided for sensitive source types.
+ */
+ public const val ACTION_HOVER_EXIT: Int = 6
+ }
+
+ /** The type of the source of this event. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(
+ value =
+ [
+ SOURCE_UNKNOWN,
+ SOURCE_HEAD,
+ SOURCE_CONTROLLER,
+ SOURCE_HANDS,
+ SOURCE_MOUSE,
+ SOURCE_GAZE_AND_GESTURE,
+ ]
+ )
+ internal annotation class Source
+
+ /** The type of the individual pointer. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(value = [POINTER_TYPE_DEFAULT, POINTER_TYPE_LEFT, POINTER_TYPE_RIGHT])
+ internal annotation class PointerType
+
+ // TODO: b/342226522 - Add the HitInfo field to InputEvent.
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(
+ value =
+ [
+ ACTION_DOWN,
+ ACTION_UP,
+ ACTION_MOVE,
+ ACTION_CANCEL,
+ ACTION_HOVER_MOVE,
+ ACTION_HOVER_ENTER,
+ ACTION_HOVER_EXIT,
+ ]
+ )
+ internal annotation class Action
+
+ /**
+ * Information about the hit result of the ray.
+ *
+ * @param inputEntity The entity that was hit by the input ray. <p> ACTION_MOVE, ACTION_UP, and
+ * ACTION_CANCEL events will report the same node as was hit during the initial ACTION_DOWN.
+ * @param hitPosition The position of the hit in the receiver's activity space. <p> All events
+ * may report the current ray's hit position. This can be null if there no longer is a
+ * collision between the ray and the input node (eg, during a drag event).
+ * @param transform The matrix transforming activity space coordinates into the hit entity's
+ * local coordinate space.
+ */
+ public class HitInfo(
+ public val inputEntity: Entity,
+ public val hitPosition: Vector3?,
+ public val transform: Matrix4,
+ ) {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is HitInfo) return false
+ if (inputEntity != other.inputEntity) return false
+ if (hitPosition != other.hitPosition) return false
+ if (transform != other.transform) return false
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = inputEntity.hashCode()
+ result = 31 * result + inputEntity.hashCode()
+ result = 31 * result + (hitPosition?.hashCode() ?: 0)
+ result = 31 * result + transform.hashCode()
+ return result
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/InteractableComponent.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/InteractableComponent.kt
new file mode 100644
index 0000000..3ba2924
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/InteractableComponent.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import android.util.Log
+import androidx.annotation.RestrictTo
+import java.util.concurrent.Executor
+
+/**
+ * Provides access to raw input events for given Entity, so a client can implement their own
+ * interaction logic.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class InteractableComponent
+private constructor(
+ private val runtime: JxrPlatformAdapter,
+ private val entityManager: EntityManager,
+ private val executor: Executor,
+ private val inputEventListener: InputEventListener,
+) : Component {
+ private val rtInputEventListener =
+ JxrPlatformAdapter.InputEventListener { rtEvent ->
+ inputEventListener.onInputEvent(rtEvent.toInputEvent(entityManager))
+ }
+ private val rtInteractableComponent by lazy {
+ runtime.createInteractableComponent(executor, rtInputEventListener)
+ }
+ private var entity: Entity? = null
+
+ /**
+ * Attaches this component to the given entity.
+ *
+ * @param entity The entity to attach this component to.
+ * @return `true` if the component was successfully attached, `false` otherwise.
+ */
+ override fun onAttach(entity: Entity): Boolean {
+ if (this.entity != null) {
+ Log.e("InteractableComponent", "Already attached to entity ${this.entity}")
+ return false
+ }
+ this.entity = entity
+ return (entity as BaseEntity<*>).rtEntity.addComponent(rtInteractableComponent)
+ }
+
+ /**
+ * Detaches this component from the given entity.
+ *
+ * @param entity The entity to detach this component from.
+ */
+ override fun onDetach(entity: Entity) {
+ (entity as BaseEntity<*>).rtEntity.removeComponent(rtInteractableComponent)
+ this.entity = null
+ }
+
+ internal companion object {
+ /** Factory for Interactable component. */
+ internal fun create(
+ runtime: JxrPlatformAdapter,
+ entityManager: EntityManager,
+ executor: Executor,
+ inputEventListener: InputEventListener,
+ ): InteractableComponent {
+ return InteractableComponent(runtime, entityManager, executor, inputEventListener)
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/JxrPlatformAdapter.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/JxrPlatformAdapter.java
new file mode 100644
index 0000000..f12c28e3
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/JxrPlatformAdapter.java
@@ -0,0 +1,2038 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioTrack;
+import android.media.MediaPlayer;
+import android.media.SoundPool;
+import android.os.Bundle;
+import android.view.Surface;
+import android.view.View;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.xr.arcore.Anchor;
+import androidx.xr.runtime.math.Matrix4;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Vector3;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Duration;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/** Interface for SceneCore Platform operations. This is not intended to be used by Applications. */
+// TODO Add API versioning
+// TODO: b/322549913 - Move subclasses into separate files
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface JxrPlatformAdapter {
+
+ /** Returns the Environment for the Session. */
+ @NonNull
+ SpatialEnvironment getSpatialEnvironment();
+
+ /** A function to create a SceneCore Entity */
+ @NonNull
+ LoggingEntity createLoggingEntity(@NonNull Pose pose);
+
+ /** Returns the Activity Space entity at the root of the scene. */
+ @NonNull
+ ActivitySpace getActivitySpace();
+
+ /** Returns the HeadActivityPose for the Session or null if it is not ready */
+ @Nullable
+ HeadActivityPose getHeadActivityPose();
+
+ /**
+ * Returns the CameraViewActivityPose for the specified camera type or null if it is not
+ * ready/available.
+ */
+ @Nullable
+ CameraViewActivityPose getCameraViewActivityPose(
+ @CameraViewActivityPose.CameraType int cameraType);
+
+ /** Returns the PerceptionSpaceActivityPose for the Session. */
+ @NonNull
+ PerceptionSpaceActivityPose getPerceptionSpaceActivityPose();
+
+ /**
+ * Returns the entity that represents the ActivitySpace root.
+ *
+ * <p>SDK's factory methods are expected to use this entity as the default parent for all
+ * content entities when no parent is specified.
+ */
+ // TODO: b/378680989 - Remove this method.
+ @NonNull
+ Entity getActivitySpaceRootImpl();
+
+ /** Loads glTF Asset for the given asset name from the assets folder. */
+ // Suppressed to allow CompletableFuture.
+ @SuppressWarnings({"AndroidJdkLibsChecker", "AsyncSuffixFuture"})
+ @Nullable
+ ListenableFuture<GltfModelResource> loadGltfByAssetName(@NonNull String assetName);
+
+ /**
+ * Loads glTF Asset for the given asset name from the assets folder using the Split Engine
+ * route. The future returned by this method will fire listeners on the UI thread if
+ * Runnable::run is supplied.
+ */
+ @SuppressWarnings("AsyncSuffixFuture")
+ @Nullable
+ ListenableFuture<GltfModelResource> loadGltfByAssetNameSplitEngine(@NonNull String assetName);
+
+ /** Loads an ExrImage for the given asset name from the assets folder. */
+ // Suppressed to allow CompletableFuture.
+ @SuppressWarnings({"AndroidJdkLibsChecker", "AsyncSuffixFuture"})
+ @Nullable
+ ListenableFuture<ExrImageResource> loadExrImageByAssetName(@NonNull String assetName);
+
+ /**
+ * A factory function to create a SceneCore GltfEntity. The parent may be the activity space or
+ * GltfEntity in the scene.
+ */
+ @NonNull
+ GltfEntity createGltfEntity(
+ @NonNull Pose pose,
+ @NonNull GltfModelResource loadedGltf,
+ @Nullable Entity parentEntity);
+
+ /** A factory function for an Entity which displays drawable surfaces. */
+ @NonNull
+ StereoSurfaceEntity createStereoSurfaceEntity(
+ @StereoSurfaceEntity.StereoMode int stereoMode,
+ @NonNull Dimensions dimensions,
+ @NonNull Pose pose,
+ @NonNull Entity parentEntity);
+
+ /** Return the Spatial Capabilities set that are currently supported by the platform. */
+ @NonNull
+ SpatialCapabilities getSpatialCapabilities();
+
+ /**
+ * Adds the given {@link Consumer} as a listener to be invoked when this Session's current
+ * SpatialCapabilities change. {@link Consumer#accept(SpatialCapabilities)} will be invoked on
+ * the given Executor.
+ */
+ void addSpatialCapabilitiesChangedListener(
+ @NonNull Executor callbackExecutor, @NonNull Consumer<SpatialCapabilities> listener);
+
+ /**
+ * Releases the given {@link Consumer} from receiving updates when the Session's {@link
+ * SpatialCapabilities} change.
+ */
+ void removeSpatialCapabilitiesChangedListener(@NonNull Consumer<SpatialCapabilities> listener);
+
+ /**
+ * If the primary Activity for this Session has focus, causes it to be placed in FullSpace Mode.
+ * Otherwise, this call does nothing.
+ */
+ void requestFullSpaceMode();
+
+ /**
+ * If the primary Activity for this Session has focus, causes it to be placed in HomeSpace Mode.
+ * Otherwise, this call does nothing.
+ */
+ void requestHomeSpaceMode();
+
+ /**
+ * A factory function to create a platform PanelEntity. The parent can be any entity.
+ *
+ * @param pose Initial pose of the panel.
+ * @param view View inflating this panel.
+ * @param surfaceDimensionsPx Dimensions for the underlying surface for the given view.
+ * @param dimensions Size of the panel in meters.
+ * @param name Name of the panel.
+ * @param context Application Context.
+ * @param parent Parent entity.
+ */
+ @NonNull
+ PanelEntity createPanelEntity(
+ @NonNull Pose pose,
+ @NonNull View view,
+ @NonNull PixelDimensions surfaceDimensionsPx,
+ @NonNull Dimensions dimensions,
+ @NonNull String name,
+ @SuppressWarnings("ContextFirst") @NonNull Context context,
+ @NonNull Entity parent);
+
+ /** Get the PanelEntity associated with the main window for the Activity. */
+ @NonNull
+ PanelEntity getMainPanelEntity();
+
+ /**
+ * Factory function to create ActivityPanel to launch/move activity into.
+ *
+ * @param pose Initial pose of the panel.
+ * @param windowBoundsPx Boundary for the window
+ * @param name Name of the panel.
+ * @param hostActivity Activity to host the panel.
+ * @param parent Parent entity.
+ */
+ @NonNull
+ ActivityPanelEntity createActivityPanelEntity(
+ @NonNull Pose pose,
+ @NonNull PixelDimensions windowBoundsPx,
+ @NonNull String name,
+ @NonNull Activity hostActivity,
+ @NonNull Entity parent);
+
+ /**
+ * A factory function to create an Anchor entity.
+ *
+ * @param bounds Bounds for this Anchor.
+ * @param planeType Orientation of the plane to which this anchor should attach.
+ * @param planeSemantic Semantic type of the plane to which this anchor should attach.
+ * @param searchTimeout How long to search for an anchor. If this is Duration.ZERO, this will
+ * search for an anchor indefinitely.
+ */
+ @NonNull
+ AnchorEntity createAnchorEntity(
+ @NonNull Dimensions bounds,
+ @NonNull PlaneType planeType,
+ @NonNull PlaneSemantic planeSemantic,
+ @NonNull Duration searchTimeout);
+
+ /**
+ * A factory function to create an Anchor entity from a {@link androidx.xr.arcore.Anchor}.
+ *
+ * @param anchor The {@link androidx.xr.arcore.Anchor} to create the Anchor entity from.
+ */
+ @NonNull
+ AnchorEntity createAnchorEntity(@NonNull Anchor anchor);
+
+ /**
+ * Unpersist an AnchorEntity. It will clean up the data in the storage that is required to
+ * retrieve the anchor. Returns whether the anchor was successfully unpersisted.
+ *
+ * @param uuid UUID of the anchor to unpersist.
+ */
+ boolean unpersistAnchor(@NonNull UUID uuid);
+
+ /**
+ * A factory function to create a content-less entity. This entity is used as a connection point
+ * for attaching children entities and managing them (i.e. setPose()) as a group.
+ *
+ * @param pose Initial pose of the entity.
+ * @param name Name of the entity.
+ * @param parent Parent entity.
+ */
+ @NonNull
+ Entity createEntity(@NonNull Pose pose, @NonNull String name, @NonNull Entity parent);
+
+ /**
+ * Create an Interactable component.
+ *
+ * @param executor Executor to use for input callbacks.
+ * @param listener [JxrPlatformAdapter.InputEventListener] for this component.
+ * @return InteractableComponent instance.
+ */
+ @SuppressLint("ExecutorRegistration")
+ @NonNull
+ InteractableComponent createInteractableComponent(
+ @NonNull Executor executor, @NonNull InputEventListener listener);
+
+ /**
+ * Create an instance of [MovableComponent]. This component allows the user to move the entity.
+ *
+ * @param systemMovable A [boolean] which causes the system to automatically apply transform
+ * updates to the entity in response to user interaction.
+ * @param scaleInZ A [boolean] which tells the system to update the scale of the Entity as the
+ * user moves it closer and further away. This is mostly useful for Panel auto-rescaling
+ * with Distance
+ * @param anchorPlacement AnchorPlacement information for when to anchor the entity.
+ * @param shouldDisposeParentAnchor A [boolean] which tells the system to dispose of the parent
+ * anchor if that entity was created by the moveable component and is moved off of it.
+ * @return [MovableComponent] instance.
+ */
+ @NonNull
+ MovableComponent createMovableComponent(
+ boolean systemMovable,
+ boolean scaleInZ,
+ @NonNull Set<AnchorPlacement> anchorPlacement,
+ boolean shouldDisposeParentAnchor);
+
+ /**
+ * Creates an instance of an AnchorPlacement object.
+ *
+ * <p>This can be used in movable components to specify the anchor placement for the entity.
+ *
+ * @param planeTypeFilter A set of plane types to filter for.
+ * @param planeSemanticFilter A set of plane semantics to filter for.
+ * @return [AnchorPlacement] instance.
+ */
+ @NonNull
+ AnchorPlacement createAnchorPlacementForPlanes(
+ @NonNull Set<PlaneType> planeTypeFilter,
+ @NonNull Set<PlaneSemantic> planeSemanticFilter);
+
+ /**
+ * Create an instance of [ResizableComponent]. This component allows the user to resize the
+ * entity.
+ *
+ * @param minimumSize Minimum size constraint.
+ * @param maximumSize Maximum size constraint.
+ * @return [ResizableComponent] instance.
+ */
+ @NonNull
+ ResizableComponent createResizableComponent(
+ @NonNull Dimensions minimumSize, @NonNull Dimensions maximumSize);
+
+ /**
+ * Create an instance of {@link PointerCaptureComponent}. This component allows the user to
+ * capture and redirect to itself all input that would be received by entities other than the
+ * Entity it is attached to and that entity's children.
+ *
+ * <p>In order to enable pointer capture, an application must be in full space and the entity it
+ * is attached to must be visible.
+ *
+ * <p>Attach this component to the entity to enable pointer capture, detach the component to
+ * restore normal input flow.
+ *
+ * @param executor Executor used to propagate state and input events.
+ * @param stateListener Callback for updates to the state of pointer capture. Pointer capture
+ * may be temporarily lost by the application for a variety of reasons and this callback
+ * will notify of when that happens.
+ * @param inputListener Callback that will receive captured [InputEvent]s
+ */
+ @NonNull
+ PointerCaptureComponent createPointerCaptureComponent(
+ @NonNull Executor executor,
+ @NonNull PointerCaptureComponent.StateListener stateListener,
+ @NonNull InputEventListener inputListener);
+
+ /**
+ * A factory function to recreate an Anchor entity which was persisted in a previous session.
+ *
+ * @param uuid The UUID of the persisted anchor.
+ * @param searchTimeout How long to search for an anchor. If this is Duration.ZERO, this will
+ * search for an anchor indefinitely.
+ */
+ @NonNull
+ AnchorEntity createPersistedAnchorEntity(@NonNull UUID uuid, @NonNull Duration searchTimeout);
+
+ /**
+ * Sets the full space mode flag to the given {@link Bundle}.
+ *
+ * <p>The {@link Bundle} then could be used to launch an {@link Activity} with requesting to
+ * enter full space mode through {@link Activity#startActivity}. If there's a bundle used for
+ * customizing how the {@link Activity} should be started by {@link ActivityOptions.toBundle} or
+ * {@link androidx.core.app.ActivityOptionsCompat.toBundle}, it's suggested to use the bundle to
+ * call this method.
+ *
+ * <p>The flag will be ignored when no {@link Intent.FLAG_ACTIVITY_NEW_TASK} is set in the
+ * bundle, or it is not started from a focused Activity context.
+ *
+ * <p>This flag is also ignored when the {@link android.window.PROPERTY_XR_ACTIVITY_START_MODE}
+ * property is set to a value other than XR_ACTIVITY_START_MODE_UNDEFINED in the
+ * AndroidManifest.xml file for the activity being launched.
+ *
+ * @param bundle the input bundle to set with the full space mode flag.
+ * @return the input {@code bundle} with the full space mode flag set.
+ */
+ @NonNull
+ Bundle setFullSpaceMode(@NonNull Bundle bundle);
+
+ /**
+ * Sets the inherit full space mode environment flag to the given {@link Bundle}.
+ *
+ * <p>The {@link Bundle} then could be used to launch an {@link Activity} with requesting to
+ * enter full space mode while inherit the existing environment through {@link
+ * Activity#startActivity}. If there's a bundle used for customizing how the {@link Activity}
+ * should be started by {@link ActivityOptions.toBundle} or {@link
+ * androidx.core.app.ActivityOptionsCompat.toBundle}, it's suggested to use the bundle to call
+ * this method.
+ *
+ * <p>When launched, the activity will be in full space mode and also inherits the environment
+ * from the launching activity. If the inherited environment needs to be animated, the launching
+ * activity has to continue updating the environment even after the activity is put into the
+ * stopped state.
+ *
+ * <p>The flag will be ignored when no {@link Intent.FLAG_ACTIVITY_NEW_TASK} is set in the
+ * intent, or it is not started from a focused Activity context.
+ *
+ * <p>The flag will also be ignored when there is no environment to inherit or the activity has
+ * its own environment set already.
+ *
+ * <p>This flag is ignored too when the {@link android.window.PROPERTY_XR_ACTIVITY_START_MODE}
+ * property is set to a value other than XR_ACTIVITY_START_MODE_UNDEFINED in the
+ * AndroidManifest.xml file for the activity being launched.
+ *
+ * <p>For security reasons, Z testing for the new activity is disabled, and the activity is
+ * always drawn on top of the inherited environment. Because Z testing is disabled, the activity
+ * should not spatialize itself, and should not curve its panel too much either.
+ *
+ * @param bundle the input bundle to set with the inherit full space mode environment flag.
+ * @return the input {@code bundle} with the inherit full space mode flag set.
+ */
+ @NonNull
+ Bundle setFullSpaceModeWithEnvironmentInherited(@NonNull Bundle bundle);
+
+ /**
+ * Sets a preferred main panel aspect ratio for home space mode.
+ *
+ * <p>The ratio is only applied to the activity. If the activity launches another activity in
+ * the same task, the ratio is not applied to the new activity. Also, while the activity is in
+ * full space mode, the preference is temporarily removed.
+ *
+ * @param activity the activity to set the preference.
+ * @param preferredRatio the aspect ratio determined by taking the panel's width over its
+ * height. A value <= 0.0f means there are no preferences.
+ */
+ void setPreferredAspectRatio(@NonNull Activity activity, float preferredRatio);
+
+ /** Starts the SceneCore renderer. */
+ void startRenderer();
+
+ /** Stops the SceneCore renderer. */
+ void stopRenderer();
+
+ /** Disposes of the resources used by the platform adapter. */
+ void dispose();
+
+ /** Type of plane based on orientation i.e. Horizontal or Vertical. */
+ enum PlaneType {
+ HORIZONTAL,
+ VERTICAL,
+ ANY
+ }
+
+ /** Semantic plane types. */
+ enum PlaneSemantic {
+ WALL,
+ FLOOR,
+ CEILING,
+ TABLE,
+ ANY
+ }
+
+ /** Base interface for all components. */
+ interface Component {
+ /**
+ * Lifecycle event, called when component is attached to an Entity.
+ *
+ * @param entity Entity the component is attached to.
+ * @return True if the component can attach to the given entity.
+ */
+ boolean onAttach(@NonNull Entity entity);
+
+ /**
+ * Lifecycle event, called when component is detached from an Entity.
+ *
+ * @param entity Entity the component detached from.
+ */
+ void onDetach(@NonNull Entity entity);
+ }
+
+ /** Component to enable input interactions. */
+ interface InteractableComponent extends Component {}
+
+ /** Component to enable a high level user movement affordance. */
+ interface MovableComponent extends Component {
+ /**
+ * Modes for scaling the entity as the user moves it closer and further away. *
+ *
+ * <p>DEFAULT: The panel scales in the same way as home space mode.
+ *
+ * <p>DMM: The panel scales in a way that the user-perceived panel size never changes.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ ScaleWithDistanceMode.DEFAULT,
+ ScaleWithDistanceMode.DMM,
+ })
+ public @interface ScaleWithDistanceMode {
+ int DEFAULT = 3;
+ int DMM = 2;
+ }
+
+ /** Returns the current scale with distance mode. */
+ @ScaleWithDistanceMode
+ int getScaleWithDistanceMode();
+
+ /**
+ * Sets the scale with distance mode.
+ *
+ * @param scaleWithDistanceMode The scale with distance mode to set
+ */
+ void setScaleWithDistanceMode(@ScaleWithDistanceMode int scaleWithDistanceMode);
+
+ /** Sets the size of the interaction highlight extent. */
+ void setSize(@NonNull Dimensions dimensions);
+
+ /**
+ * Adds the listener to the set of active listeners for the move events.
+ *
+ * <p>The listener is invoked on the provided executor. If the app intends to modify the UI
+ * elements/views during the callback, the app should provide the thread executor that is
+ * appropriate for the UI operations. For example, if the app is using the main thread to
+ * render the UI, the app should provide the main thread (Looper.getMainLooper()) executor.
+ * If the app is using a separate thread to render the UI, the app should provide the
+ * executor for that thread.
+ *
+ * @param executor The executor to run the listener on.
+ * @param moveEventListener The move event listener to set.
+ */
+ void addMoveEventListener(
+ @NonNull Executor executor, @NonNull MoveEventListener moveEventListener);
+
+ /**
+ * Removes the listener from the set of active listeners for the move events.
+ *
+ * @param moveEventListener the move event listener to remove
+ */
+ void removeMoveEventListener(@NonNull MoveEventListener moveEventListener);
+ }
+
+ /**
+ * Interface for an AnchorPlacement.
+ *
+ * <p>This is used to set possible conditions in which an entity with a MovableComponent can be
+ * anchored. This can be set with createAnchorPlacementForPlanes.
+ */
+ interface AnchorPlacement {}
+
+ /** Component to enable resize semantics. */
+ interface ResizableComponent extends Component {
+ /**
+ * Sets the size of the entity.
+ *
+ * <p>The size of the entity is the size of the bounding box that contains the content of
+ * the entity. The size of the content inside that bounding box is fully controlled by the
+ * application.
+ *
+ * @param dimensions Dimensions for the Entity in meters.
+ */
+ void setSize(@NonNull Dimensions dimensions);
+
+ /**
+ * Sets the minimum size constraint for the entity.
+ *
+ * <p>The minimum size constraint is used to set constraints on how small the user can
+ * resize the bounding box of the entity up to. The size of the content inside that bounding
+ * box is fully controlled by the application.
+ *
+ * @param minSize Minimum size constraint for the Entity in meters.
+ */
+ void setMinimumSize(@NonNull Dimensions minSize);
+
+ /**
+ * Sets the maximum size constraint for the entity.
+ *
+ * <p>The maximum size constraint is used to set constraints on how large the user can
+ * resize the bounding box of the entity up to. The size of the content inside that bounding
+ * box is fully controlled by the application.
+ *
+ * @param maxSize Maximum size constraint for the Entity in meters.
+ */
+ void setMaximumSize(@NonNull Dimensions maxSize);
+
+ /**
+ * Sets the aspect ratio of the entity during resizing.
+ *
+ * <p>The aspect ratio is determined by taking the panel's width over its height. A value of
+ * 0.0f (or negative) means there are no preferences.
+ *
+ * <p>This method does not immediately resize the entity. The new aspect ratio will be
+ * applied the next time the user resizes the entity through the reform UI. During this
+ * resize operation, the entity's current area will be preserved.
+ *
+ * <p>If a different resizing behavior is desired, such as fixing the width and adjusting
+ * the height, the client can manually resize the entity to the preferred dimensions before
+ * calling this method. No automatic resizing will occur when using the reform UI then.
+ *
+ * @param fixedAspectRatio Aspect ratio during resizing.
+ */
+ void setFixedAspectRatio(float fixedAspectRatio);
+
+ /**
+ * Adds the listener to the set of listeners that are invoked through the resize operation,
+ * such as start, ongoing and end.
+ *
+ * <p>The listener is invoked on the provided executor. If the app intends to modify the UI
+ * elements/views during the callback, the app should provide the thread executor that is
+ * appropriate for the UI operations. For example, if the app is using the main thread to
+ * render the UI, the app should provide the main thread (Looper.getMainLooper()) executor.
+ * If the app is using a separate thread to render the UI, the app should provide the
+ * executor for that thread.
+ *
+ * @param executor The executor to use for the listener callback.
+ * @param resizeEventListener The listener to be invoked when a resize event occurs.
+ */
+ // TODO: b/361638845 - Mirror the Kotlin API for ResizeListener.
+ void addResizeEventListener(
+ @NonNull Executor executor, @NonNull ResizeEventListener resizeEventListener);
+
+ /**
+ * Removes the given listener from the set of listeners for the resize events.
+ *
+ * @param resizeEventListener The listener to be removed.
+ */
+ void removeResizeEventListener(@NonNull ResizeEventListener resizeEventListener);
+ }
+
+ /** Component to enable pointer capture. */
+ interface PointerCaptureComponent extends Component {
+ public static final int POINTER_CAPTURE_STATE_PAUSED = 0;
+ public static final int POINTER_CAPTURE_STATE_ACTIVE = 1;
+ public static final int POINTER_CAPTURE_STATE_STOPPED = 2;
+
+ /** The possible states of pointer capture. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ POINTER_CAPTURE_STATE_PAUSED,
+ POINTER_CAPTURE_STATE_ACTIVE,
+ POINTER_CAPTURE_STATE_STOPPED,
+ })
+ public @interface PointerCaptureState {}
+
+ /** Functional interface for receiving updates about the state of pointer capture. */
+ interface StateListener {
+ void onStateChanged(@PointerCaptureState int newState);
+ }
+ }
+
+ /** Interface for a SceneCore resource. A resource represents a loadable resource. */
+ interface Resource {}
+
+ /**
+ * Interface for an EXR resource. These HDR images can be used for image based lighting and
+ * skyboxes.
+ */
+ interface ExrImageResource extends Resource {}
+
+ /** Interface for a glTF resource. This can be used for creating glTF entities. */
+ interface GltfModelResource extends Resource {}
+
+ /** Interface for Input listener. */
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ @FunctionalInterface
+ interface InputEventListener {
+ void onInputEvent(@NonNull InputEvent event);
+ }
+
+ /** Interface for MoveEvent listener. */
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ @FunctionalInterface
+ interface MoveEventListener {
+ void onMoveEvent(@NonNull final MoveEvent event);
+ }
+
+ /** Interface for ResizeEvent listener. */
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ @FunctionalInterface
+ interface ResizeEventListener {
+ void onResizeEvent(@NonNull final ResizeEvent event);
+ }
+
+ /** Interface for a SceneCore ActivityPose */
+ interface ActivityPose {
+ /** Returns the pose for this entity, relative to the activity space root. */
+ @NonNull
+ Pose getActivitySpacePose();
+
+ // TODO: b/364303733 - Consider deprecating this method.
+ /**
+ * Returns the scale of this ActivityPose. For base ActivityPoses, the scale is (1,1,1). For
+ * entities this returns the accumulated scale. This value includes the parent's scale, and
+ * is similar to a ActivitySpace scale.
+ *
+ * @return Total [Vector3] scale applied to self and children.
+ */
+ @NonNull
+ Vector3 getWorldSpaceScale();
+
+ /**
+ * Returns the scale of this WorldPose relative to the activity space. This returns the
+ * accumulated scale which includes the parent's scale, but does not include the scale of
+ * the activity space itself.
+ *
+ * @return Total [Vector3] scale applied to self and children relative to the activity
+ * space.
+ */
+ @NonNull
+ Vector3 getActivitySpaceScale();
+
+ /**
+ * Returns a pose relative to this entity transformed into a pose relative to the
+ * destination.
+ *
+ * @param pose A pose in this entity's local coordinate space.
+ * @param destination The entity which the returned pose will be relative to.
+ * @return The pose relative to the destination entity.
+ */
+ @NonNull
+ Pose transformPoseTo(@NonNull Pose pose, @NonNull ActivityPose destination);
+ }
+
+ /** Interface for a SceneCore head ActivityPose. This is the position of the user's head. */
+ interface HeadActivityPose extends ActivityPose {}
+
+ /**
+ * Interface for a SceneCore camera view ActivityPose. This is the position of a user's camera.
+ *
+ * <p>The camera's field of view can be retrieved from this CameraViewActivityPose.
+ */
+ interface CameraViewActivityPose extends ActivityPose {
+ public static final int CAMERA_TYPE_UNKNOWN = 0;
+ public static final int CAMERA_TYPE_LEFT_EYE = 1;
+ public static final int CAMERA_TYPE_RIGHT_EYE = 2;
+
+ /** Returns the type of camera that this space represents. */
+ @CameraType
+ public int getCameraType();
+
+ /**
+ * The angles (in radians) representing the sides of the view frustum. These are not
+ * expected to change over the lifetime of the session but in rare cases may change due to
+ * updated camera settings
+ */
+ static class Fov {
+
+ public final float angleLeft;
+ public final float angleRight;
+ public final float angleUp;
+ public final float angleDown;
+
+ public Fov(float angleLeft, float angleRight, float angleUp, float angleDown) {
+ this.angleLeft = angleLeft;
+ this.angleRight = angleRight;
+ this.angleUp = angleUp;
+ this.angleDown = angleDown;
+ }
+ }
+
+ @NonNull
+ Fov getFov();
+
+ /** Describes the type of camera that this space represents. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ CAMERA_TYPE_UNKNOWN,
+ CAMERA_TYPE_LEFT_EYE,
+ CAMERA_TYPE_RIGHT_EYE,
+ })
+ public @interface CameraType {}
+ }
+
+ /**
+ * Interface for the perception space ActivityPose. This is the origin of the space used by
+ * ARCore for XR.
+ */
+ interface PerceptionSpaceActivityPose extends ActivityPose {}
+
+ /** Interface for a SceneCore Entity */
+ interface Entity extends ActivityPose {
+
+ /** Returns the pose for this entity, relative to its parent. */
+ @NonNull
+ Pose getPose();
+
+ /** Updates the pose (position and rotation) of the Entity relative to its parent. */
+ void setPose(@NonNull Pose pose);
+
+ /**
+ * Returns the scale of this entity, relative to its parent. Note that this doesn't include
+ * the parent's scale.
+ *
+ * @return Current [Vector3] scale applied to self and children.
+ */
+ @NonNull
+ Vector3 getScale();
+
+ /**
+ * Sets the scale of this entity relative to its parent. This value will affect the
+ * rendering of this Entity's children. As the scale increases, this will stretch the
+ * content of the Entity.
+ *
+ * @param scale The [Vector3] scale factor from the parent.
+ */
+ void setScale(@NonNull Vector3 scale);
+
+ /**
+ * Add given Entity as child. The child Entity's pose will be relative to the pose of its
+ * parent
+ *
+ * @param child The child entity.
+ */
+ void addChild(@NonNull Entity child);
+
+ // Sets the provided Entities to be children of the Entity.
+ void addChildren(@NonNull List<Entity> children);
+
+ // Returns the parent entity for this Entity.
+ @Nullable
+ Entity getParent();
+
+ // Sets the parent Entity for this Entity. The child Entity's pose will be relative to the
+ // pose of its parent.
+ void setParent(@Nullable Entity parent);
+
+ // Sets context-text for this entity to be consumed by Accessibility systems.
+ void setContentDescription(@NonNull String text);
+
+ // Returns the all child entities of this Entity.
+ @NonNull
+ List<Entity> getChildren();
+
+ /**
+ * Sets the size for the given Entity.
+ *
+ * @param dimensions Dimensions for the Entity in meters.
+ */
+ void setSize(@NonNull Dimensions dimensions);
+
+ /** Returns the set alpha transparency level for this Entity. */
+ float getAlpha();
+
+ /**
+ * Sets the alpha transparency for the given Entity.
+ *
+ * @param alpha Alpha transparency level for the Entity.
+ */
+ void setAlpha(float alpha);
+
+ /** Returns the total alpha transparency level for this Entity. */
+ float getActivitySpaceAlpha();
+
+ /**
+ * Sets the local hidden state of this Entity. When true, this Entity and all descendants
+ * will not be rendered in the scene. When the hidden state is false, an entity will be
+ * rendered if its ancestors are not hidden.
+ *
+ * @param hidden The new local hidden state of this Entity.
+ */
+ void setHidden(boolean hidden);
+
+ /**
+ * Returns the hidden status of this Entity.
+ *
+ * @param includeParents Whether to include the hidden status of parents in the returned
+ * value.
+ * @return If includeParents is true, the returned value will be true if this Entity or any
+ * of its ancestors is hidden. If includeParents is false, the local hidden state is
+ * returned. Regardless of the local hidden state, an entity will not be rendered if any
+ * of its ancestors are hidden.
+ */
+ boolean isHidden(boolean includeParents);
+
+ /**
+ * Adds the listener to the set of active input listeners, for input events targeted to this
+ * entity or its child entities.
+ *
+ * @param executor The executor to run the listener on.
+ * @param listener The input event listener to add.
+ */
+ void addInputEventListener(
+ @NonNull Executor executor, @NonNull InputEventListener listener);
+
+ /** Removes the given listener from the set of active input listeners. */
+ void removeInputEventListener(@NonNull InputEventListener listener);
+
+ /**
+ * Dispose any system resources held by this entity, and transitively calls dispose() on all
+ * the children. Once disposed, Entity shouldn't be used again.
+ */
+ void dispose();
+
+ /**
+ * Add these components to entity.
+ *
+ * @param component Component to add to the Entity.
+ * @return True if the given component is added to the Entity.
+ */
+ boolean addComponent(@NonNull Component component);
+
+ /**
+ * Remove the given component from the entity.
+ *
+ * @param component Component to remove from the entity.
+ */
+ void removeComponent(@NonNull Component component);
+
+ /** Remove all components from this entity. */
+ void removeAllComponents();
+ }
+
+ /**
+ * Interface for updating the background image/geometry and passthrough settings.
+ *
+ * <p>The application can set either / both a skybox and a glTF for geometry, then toggle their
+ * visibility by enabling or disabling passthrough. The skybox and geometry will be remembered
+ * across passthrough mode changes.
+ */
+ interface SpatialEnvironment {
+
+ /** A class that represents the user's preferred spatial environment. */
+ class SpatialEnvironmentPreference {
+ /**
+ * The preferred geometry for the environment based on a pre-loaded glTF model. If null,
+ * there will be no geometry
+ */
+ @Nullable public final GltfModelResource geometry;
+
+ /**
+ * The preferred skybox for the environment based on a pre-loaded EXR Image. If null, it
+ * will be all black.
+ */
+ @Nullable public final ExrImageResource skybox;
+
+ public SpatialEnvironmentPreference(
+ @Nullable ExrImageResource skybox, @Nullable GltfModelResource geometry) {
+ this.skybox = skybox;
+ this.geometry = geometry;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof SpatialEnvironmentPreference) {
+ SpatialEnvironmentPreference other = (SpatialEnvironmentPreference) o;
+ return Objects.equals(other.skybox, skybox)
+ && Objects.equals(other.geometry, geometry);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(skybox, geometry);
+ }
+ }
+
+ /**
+ * Sets the preference for passthrough state by requesting a change in passthrough opacity.
+ *
+ * <p>Passthrough visibility cannot be set directly to on/off modes. Instead, a desired
+ * passthrough opacity value between 0.0f and 1.0f can be requested which will dictate which
+ * mode is used. A passthrough opacity within 0.01f of 0.0f will disable passthrough, and
+ * will be returned as 0.0f by [getPassthroughOpacityPreference]. An opacity value within
+ * 0.01f of 1.0f will enable full passthrough and it will be returned as 1.0f by
+ * [getPassthroughOpacityPreference]. Any other value in the range will result in a
+ * semi-transparent passthrough.
+ *
+ * <p>Requesting to set passthrough opacity to a value that is not in the range of 0.0f to
+ * 1.0f will result in the value getting clamped to 0.0f or 1.0f depending on which one is
+ * closer.
+ *
+ * <p>If the value is set to null, the opacity will be managed by the system.
+ *
+ * <p>Requests to change opacity are only immediately attempted to be honored if the
+ * activity has the [SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL] capability.
+ * When the request is honored, this returns [SetPassthroughOpacityPreferenceChangeApplied].
+ * When the activity does not have the capability to control the passthrough state, this
+ * returns [SetPassthroughOpacityPreferenceChangePending] to indicate that the application
+ * passthrough opacity preference has been set and is pending to be automatically applied
+ * when the app regains capabilities to control passthrough state.
+ *
+ * <p>When passthrough state changes, whether due to this request succeeding or due to any
+ * other system or user initiated change, [OnPassthroughOpacityChangedListener] will be
+ * notified.
+ */
+ @CanIgnoreReturnValue
+ @NonNull
+ public SetPassthroughOpacityPreferenceResult setPassthroughOpacityPreference(
+ @SuppressWarnings("AutoBoxing") @Nullable Float passthroughOpacityPreference);
+
+ /**
+ * Gets the current passthrough opacity value between 0 and 1 where 0.0f means no
+ * passthrough, and 1.0f means full passthrough.
+ *
+ * <p>This value can be overwritten by user-enabled or system-enabled passthrough and will
+ * not always match the opacity value returned by [getPassthroughOpacityPreference].
+ */
+ public float getCurrentPassthroughOpacity();
+
+ /**
+ * Gets the last passthrough opacity requested through [setPassthroughOpacityPreference].
+ *
+ * <p>This may be different from the actual current state returned by
+ * [getCurrentPassthroughOpacity], but it should be applied as soon as the
+ * [SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL] capability is gained.
+ * Defaults to null, if [setPassthroughOpacityPreference] was never called.
+ *
+ * <p>If set to null, the passthrough opacity will default to the user preference managed
+ * through the system.
+ */
+ @SuppressWarnings("AutoBoxing")
+ @Nullable
+ public Float getPassthroughOpacityPreference();
+
+ /**
+ * Notifies an application when the passthrough state changes, such as when the application
+ * enters or exits passthrough or when the passthrough opacity changes. This [listener] will
+ * be called on the Application's UI thread.
+ */
+ public void addOnPassthroughOpacityChangedListener(@NonNull Consumer<Float> listener);
+
+ /** Remove a listener previously added by [addOnPassthroughOpacityChangedListener]. */
+ public void removeOnPassthroughOpacityChangedListener(@NonNull Consumer<Float> listener);
+
+ /**
+ * Returns true if the environment set by [setSpatialEnvironmentPreference] is active.
+ *
+ * <p>Spatial environment preference set through [setSpatialEnvironmentPreference] are shown
+ * when this is true, but passthrough or other objects in the scene could partially or
+ * totally occlude them. When this is false, the default system environment will be active
+ * instead.
+ */
+ boolean isSpatialEnvironmentPreferenceActive();
+
+ /**
+ * Sets the preferred spatial environment for the application.
+ *
+ * <p>Note that this method only sets a preference and does not cause an immediate change
+ * unless [isSpatialEnvironmentPreferenceActive] is already true. Once the device enters a
+ * state where the XR background can be changed and the
+ * [SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENTS] capability is available, the
+ * preferred spatial environment for the application will be automatically displayed.
+ *
+ * <p>Setting the preference to null will disable the preferred spatial environment for the
+ * application, meaning the default system environment will be displayed instead.
+ *
+ * <p>If the given [SpatialEnvironmentPreference] is not null, but all of its properties are
+ * null, then the spatial environment will consist of a black skybox and no geometry
+ * [isSpatialEnvironmentPreferenceActive] is true.
+ *
+ * <p>Changes to the Environment state will be notified via the
+ * [OnSpatialEnvironmentChangedListener].
+ */
+ @NonNull
+ @CanIgnoreReturnValue
+ SetSpatialEnvironmentPreferenceResult setSpatialEnvironmentPreference(
+ @Nullable SpatialEnvironmentPreference preference);
+
+ /**
+ * Gets the preferred spatial environment for the application.
+ *
+ * <p>The returned value is always what was most recently supplied to
+ * [setSpatialEnvironmentPreference], or null if no preference has been set.
+ *
+ * <p>See [isSpatialEnvironmentPreferenceActive] or the
+ * [OnSpatialEnvironmentChangedListener] events to know when this preference becomes active.
+ */
+ @Nullable
+ SpatialEnvironmentPreference getSpatialEnvironmentPreference();
+
+ /**
+ * Notifies an application whether or not the preferred spatial environment for the
+ * application is active.
+ *
+ * <p>The environment will try to transition to the application environment when a non-null
+ * preference is set through [setSpatialEnvironmentPreference] and the application has the
+ * [SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENTS] capability. The environment
+ * preferences will otherwise not be active.
+ *
+ * <p>The listener consumes a boolean value that is true if the environment preference is
+ * active when the listener is notified.
+ *
+ * <p>This listener will be invoked on the Application's UI thread.
+ */
+ void addOnSpatialEnvironmentChangedListener(@NonNull Consumer<Boolean> listener);
+
+ /** Remove a listener previously added by [addOnSpatialEnvironmentChangedListener]. */
+ void removeOnSpatialEnvironmentChangedListener(@NonNull Consumer<Boolean> listener);
+
+ /** Result values for calls to SpatialEnvironment.setPassthroughOpacityPreference */
+ enum SetPassthroughOpacityPreferenceResult {
+ /**
+ * The call to [setPassthroughOpacityPreference] succeeded and should now be visible.
+ */
+ CHANGE_APPLIED,
+
+ /**
+ * The preference has been set, but will be applied only when the
+ * [SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL] is acquired
+ */
+ CHANGE_PENDING,
+ }
+
+ /** Result values for calls to SpatialEnvironment.setSpatialEnvironmentPreference */
+ enum SetSpatialEnvironmentPreferenceResult {
+ /**
+ * The call to [setSpatialEnvironmentPreference] succeeded and should now be visible.
+ */
+ CHANGE_APPLIED,
+
+ /**
+ * The call to [setSpatialEnvironmentPreference] successfully applied the preference,
+ * but it is not immediately visible due to requesting a state change while the activity
+ * does not have the [SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENTS]
+ * capability to control the app environment state. The preference was still set and
+ * will be applied when the capability is gained.
+ */
+ CHANGE_PENDING,
+ }
+ }
+
+ /** Interface for a SceneCore Entity that only logs the pose. */
+ interface LoggingEntity extends Entity {}
+
+ /** Interface for a system-controlled SceneCore Entity that defines its own coordinate space. */
+ interface SystemSpaceEntity extends Entity {
+ /**
+ * Registers a listener to be called when the underlying space has moved or changed.
+ *
+ * @param listener The listener to register if non-null, else stops listening if null.
+ * @param executor The executor to run the listener on. Defaults to SceneCore executor if
+ * null.
+ */
+ void setOnSpaceUpdatedListener(
+ @Nullable OnSpaceUpdatedListener listener, @Nullable Executor executor);
+
+ /** Interface for a listener which receives changes to the underlying space. */
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ @FunctionalInterface
+ interface OnSpaceUpdatedListener {
+ void onSpaceUpdated();
+ }
+ }
+
+ /**
+ * Interface for a SceneCore activity space. There is one activity space and it is the ancestor
+ * for all elements in the scene. The activity space does not have a parent.
+ */
+ interface ActivitySpace extends SystemSpaceEntity {
+
+ /** Returns the bounds of this ActivitySpace. */
+ @NonNull
+ Dimensions getBounds();
+
+ /**
+ * Adds a listener to be called when the bounds of the primary Activity change. If the same
+ * listener is added multiple times, it will only fire each event on time.
+ *
+ * @param listener The listener to register.
+ */
+ @SuppressWarnings("ExecutorRegistration")
+ void addOnBoundsChangedListener(@NonNull OnBoundsChangedListener listener);
+
+ /**
+ * Removes a listener to be called when the bounds of the primary Activity change. If the
+ * given listener was not added, this call does nothing.
+ *
+ * @param listener The listener to unregister.
+ */
+ void removeOnBoundsChangedListener(@NonNull OnBoundsChangedListener listener);
+
+ /**
+ * Interface for a listener which receives changes to the bounds of the primary Activity.
+ */
+ interface OnBoundsChangedListener {
+ // Is called by the system when the bounds of the primary Activity change
+ /**
+ * Called by the system when the bounds of the primary Activity change.
+ *
+ * @param bounds The new bounds of the primary Activity in Meters
+ */
+ void onBoundsChanged(@NonNull Dimensions bounds);
+ }
+ }
+
+ /** Interface for a SceneCore [GltfEntity]. */
+ interface GltfEntity extends Entity {
+ // TODO: b/362368652 - Add an OnAnimationFinished() Listener interface
+ // Add a getAnimationTimeRemaining() interface
+
+ /** Specifies the current animation state of the [GltfEntity]. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({AnimationState.PLAYING, AnimationState.STOPPED})
+ public @interface AnimationState {
+ public static final int PLAYING = 0;
+ public static final int STOPPED = 1;
+ }
+
+ /**
+ * Starts the animation with the given name.
+ *
+ * @param animationName The name of the animation to start. If null is supplied, will play
+ * the first animation found in the glTF.
+ * @param loop Whether the animation should loop.
+ */
+ void startAnimation(boolean loop, @Nullable String animationName);
+
+ /** Stops the animation of the glTF entity. */
+ void stopAnimation();
+
+ /** Returns the current animation state of the glTF entity. */
+ @AnimationState
+ int getAnimationState();
+ }
+
+ /** Interface for a SceneCore Panel entity */
+ interface PanelEntity extends Entity {
+ /**
+ * Returns the dimensions of the view underlying this PanelEntity.
+ *
+ * @return The current [PixelDimensions] of the underlying surface.
+ */
+ @NonNull
+ PixelDimensions getPixelDimensions();
+
+ /**
+ * Sets the pixel (not Dp) dimensions of the view underlying this PanelEntity. Calling this
+ * might cause the layout of the Panel contents to change. Updating this will not cause the
+ * scale or pixel density to change.
+ *
+ * @param dimensions The [PixelDimensions] of the underlying surface to set.
+ */
+ void setPixelDimensions(@NonNull PixelDimensions dimensions);
+
+ /**
+ * Sets a corner radius on all four corners of this PanelEntity.
+ *
+ * @param value Corner radius in meters.
+ * @throws IllegalArgumentException if radius is <= 0.0f.
+ */
+ void setCornerRadius(float value);
+
+ /** Gets the corner radius of this PanelEntity in meters. Has a default value of 0. */
+ float getCornerRadius();
+
+ /**
+ * Gets the number of pixels per meter for this panel. This value reflects changes to scale,
+ * including parent scale.
+ *
+ * @return Vector3 scale applied to pixels within the Panel. (Z will be 0)
+ */
+ @NonNull
+ Vector3 getPixelDensity();
+
+ /**
+ * Returns the spatial size of this Panel in meters. This includes any scaling applied to
+ * this panel by itself or its parents, which might be set via changes to setScale.
+ *
+ * @return [Dimensions] size of this panel in meters. (Z will be 0)
+ */
+ @NonNull
+ Dimensions getSize();
+ }
+
+ /** Interface for a SceneCore ActivityPanel entity. */
+ interface ActivityPanelEntity extends PanelEntity {
+ /**
+ * Launches the given activity into the panel.
+ *
+ * @param intent Intent to launch the activity.
+ * @param bundle Bundle to pass to the activity, can be null.
+ */
+ void launchActivity(@NonNull Intent intent, @Nullable Bundle bundle);
+
+ /**
+ * Moves the given activity into the panel.
+ *
+ * @param activity Activity to move into the ActivityPanel.
+ */
+ void moveActivity(@NonNull Activity activity);
+ }
+
+ /** Interface for a surface which images can be rendered into. */
+ interface StereoSurfaceEntity extends Entity {
+ /**
+ * Selects the view configuration for the surface. MONO creates a surface contains a single
+ * view. SIDE_BY_SIDE means the surface is split in half with two views. The first half of
+ * the surface maps to the left eye and the second half mapping to the right eye.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({StereoMode.MONO, StereoMode.TOP_BOTTOM, StereoMode.SIDE_BY_SIDE})
+ public @interface StereoMode {
+ // Each eye will see the entire surface (no separation)
+ public static final int MONO = 0;
+ // The [top, bottom] halves of the surface will map to [left, right] eyes
+ public static final int TOP_BOTTOM = 1;
+ // The [left, right] halves of the surface will map to [left, right] eyes
+ public static final int SIDE_BY_SIDE = 2;
+ }
+
+ /**
+ * Specifies how the surface content will be routed for stereo viewing. Applications must
+ * render into the surface in accordance with what is specified here in order for the
+ * compositor to correctly produce a stereoscopic view to the user.
+ *
+ * @param mode An int StereoMode
+ */
+ void setStereoMode(@StereoMode int mode);
+
+ /**
+ * Retrieves the StereoMode for this Entity.
+ *
+ * @return An int StereoMode
+ */
+ @StereoMode
+ int getStereoMode();
+
+ /**
+ * Changes the width and height of the "spatial canvas" which the surface is mapped to.
+ * (0,0,0) for this Entity is the canvas's center. (In ActivitySpace, this is
+ * 1/getWorldSpaceScale() unit per meter) (Z is ignored)
+ *
+ * @param dimensions A [Dimensions] to set the canvas size.
+ */
+ void setDimensions(@NonNull Dimensions dimensions);
+
+ /**
+ * Retrieves the dimensions of the "spatial canvas" which the surface is mapped to. These
+ * values are not impacted by scale.
+ *
+ * @return The canvas [Dimensions].
+ */
+ @NonNull
+ Dimensions getDimensions();
+
+ /**
+ * Retrieves the surface that the Entity will display. The app can write into this surface
+ * however it wants, i.e. MediaPlayer, ExoPlayer, or custom rendering.
+ *
+ * @return an Android [Surface]
+ */
+ @NonNull
+ Surface getSurface();
+ }
+
+ /** Interface for Anchor entity. */
+ interface AnchorEntity extends SystemSpaceEntity {
+ /* Returns the current state of the anchor synchronously.*/
+ @NonNull
+ State getState();
+
+ /** Registers a listener to be called when the state of the anchor changes. */
+ @SuppressWarnings("ExecutorRegistration")
+ void setOnStateChangedListener(@Nullable OnStateChangedListener onStateChangedListener);
+
+ /**
+ * Persists the anchor. If the query is sent to perception service successful returns an
+ * UUID, which could be used retrieve the anchor. Otherwise, return null.
+ */
+ @Nullable
+ UUID persist();
+
+ /** Returns the current persist state of the anchor synchronously. */
+ @NonNull
+ PersistState getPersistState();
+
+ /** Returns the native pointer of the anchor. */
+ // TODO(b/373711152) : Remove this method once the Jetpack XR Runtime API migration is done.
+ long nativePointer();
+
+ /** Registers a listener to be called when the persist state of the anchor changes. */
+ @SuppressWarnings({"ExecutorRegistration", "PairedRegistration"})
+ void registerPersistStateChangeListener(
+ @NonNull PersistStateChangeListener persistStateChangeListener);
+
+ /** Specifies the current tracking state of the Anchor. */
+ enum State {
+ /**
+ * An UNANCHORED state could mean that the perception stack hasn't found an anchor for
+ * this Space, that it has lost tracking.
+ */
+ UNANCHORED,
+ /**
+ * The ANCHORED state means that this Anchor is being actively tracked and updated by
+ * the perception stack. The application should expect children to maintain their
+ * relative positioning to the system's best understanding of a pose in the real world.
+ */
+ ANCHORED,
+ /**
+ * The AnchorEntity timed out while searching for an underlying anchor. This it is not
+ * possible to recover the AnchorEntity.
+ */
+ TIMED_OUT,
+ /**
+ * The ERROR state means that something has gone wrong and this AnchorSpace is invalid
+ * without the possibility of recovery.
+ */
+ ERROR,
+ }
+
+ /** Specifies the current persistence state of the Anchor. */
+ enum PersistState {
+ /** The anchor hasn't been requested to persist. */
+ PERSIST_NOT_REQUESTED,
+ /** The anchor is requested to persist but hasn't been persisted yet. */
+ PERSIST_PENDING,
+ /** The anchor is persisted successfully. */
+ PERSISTED,
+ }
+
+ /** Interface for listening to Anchor state changes. */
+ interface OnStateChangedListener {
+ void onStateChanged(@NonNull State newState);
+ }
+
+ /** Interface for listening to Anchor persist state changes. */
+ interface PersistStateChangeListener {
+ void onPersistStateChanged(@NonNull PersistState newPersistState);
+ }
+ }
+
+ /** The dimensions of a UI element in pixels. These are always two dimensional. */
+ class PixelDimensions {
+ public final int width;
+ public final int height;
+
+ public PixelDimensions(int width, int height) {
+ this.width = width;
+ this.height = height;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + ": w " + width + " x h " + height;
+ }
+ }
+
+ /** The dimensions of a UI element in meters. */
+ class Dimensions {
+ // TODO: b/332588978 - Add a TypeAlias for Meters here.
+ @SuppressWarnings("MutableBareField")
+ public float width;
+
+ @SuppressWarnings("MutableBareField")
+ public float height;
+
+ @SuppressWarnings("MutableBareField")
+ public float depth;
+
+ public Dimensions(float width, float height, float depth) {
+ this.width = width;
+ this.height = height;
+ this.depth = depth;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + ": w " + width + " x h " + height + " x d " + depth;
+ }
+ }
+
+ /** Ray in 3D Cartesian space. */
+ class Ray {
+ @NonNull public final Vector3 origin;
+ @NonNull public final Vector3 direction;
+
+ public Ray(@NonNull Vector3 origin, @NonNull Vector3 direction) {
+ this.origin = origin;
+ this.direction = direction;
+ }
+ }
+
+ /** MoveEvent for SceneCore Platform. */
+ class MoveEvent {
+ // TODO: b/350370142 - Use public getter/setter interfaces instead of public fields.
+ public static final int MOVE_STATE_START = 1;
+ public static final int MOVE_STATE_ONGOING = 2;
+ public static final int MOVE_STATE_END = 3;
+
+ /** State of the move action. */
+ @MoveState public final int moveState;
+
+ /** Initial ray origin and direction in activity space. */
+ @NonNull public final Ray initialInputRay;
+
+ /** Current ray origin and direction in activity space. */
+ @NonNull public final Ray currentInputRay;
+
+ /** Previous pose of the entity, relative to its parent. */
+ @NonNull public final Pose previousPose;
+
+ /** Current pose of the entity, relative to its parent. */
+ @NonNull public final Pose currentPose;
+
+ /** Previous scale of the entity. */
+ @NonNull public final Vector3 previousScale;
+
+ /** Current scale of the entity. */
+ @NonNull public final Vector3 currentScale;
+
+ /** Initial Parent of the entity at the start of the move. */
+ @NonNull public final Entity initialParent;
+
+ /** Updates parent of the entity at the end of the move or null if not updated. */
+ @Nullable public final Entity updatedParent;
+
+ /**
+ * Reports an entity that was disposed and needs to be removed from the sdk EntityManager.
+ */
+ @Nullable public final Entity disposedEntity;
+
+ public MoveEvent(
+ int moveState,
+ @NonNull Ray initialInputRay,
+ @NonNull Ray currentInputRay,
+ @NonNull Pose previousPose,
+ @NonNull Pose currentPose,
+ @NonNull Vector3 previousScale,
+ @NonNull Vector3 currentScale,
+ @NonNull Entity initialParent,
+ @Nullable Entity updatedParent,
+ @Nullable Entity disposedEntity) {
+ this.moveState = moveState;
+ this.initialInputRay = initialInputRay;
+ this.currentInputRay = currentInputRay;
+ this.previousPose = previousPose;
+ this.currentPose = currentPose;
+ this.previousScale = previousScale;
+ this.currentScale = currentScale;
+ this.initialParent = initialParent;
+ this.updatedParent = updatedParent;
+ this.disposedEntity = disposedEntity;
+ }
+
+ /** States of the Move action. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ MOVE_STATE_START,
+ MOVE_STATE_ONGOING,
+ MOVE_STATE_END,
+ })
+ public @interface MoveState {}
+ }
+
+ /** ResizeEvent for SceneCore Platform. */
+ class ResizeEvent {
+ public static final int RESIZE_STATE_UNKNOWN = 0;
+ public static final int RESIZE_STATE_START = 1;
+ public static final int RESIZE_STATE_ONGOING = 2;
+ public static final int RESIZE_STATE_END = 3;
+
+ /**
+ * Proposed (width, height, depth) size in meters. The resize event listener must use this
+ * proposed size to resize the content.
+ */
+ @NonNull public final Dimensions newSize;
+
+ /** Current state of the Resize action. */
+ @ResizeState public final int resizeState;
+
+ public ResizeEvent(@ResizeState int resizeState, @NonNull Dimensions newSize) {
+ this.resizeState = resizeState;
+ this.newSize = newSize;
+ }
+
+ /** States of the Resize action. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ RESIZE_STATE_UNKNOWN,
+ RESIZE_STATE_START,
+ RESIZE_STATE_ONGOING,
+ RESIZE_STATE_END,
+ })
+ public @interface ResizeState {}
+ }
+
+ /** InputEvent for SceneCore Platform. */
+ class InputEvent {
+ /**
+ * There's a possibility of ABI mismatch here when the concrete platformAdapter starts
+ * receiving input events with an updated field, such as if a newer source or pointer type
+ * has been added to the underlying platform OS. We need to perform a version check when the
+ * platformAdapter is constructed to ensure that the application doesn't receive anything it
+ * wasn't compiled against.
+ */
+ // TODO: b/343468347 - Implement a version check for xr extensions when creating the
+ // concrete
+ // platform adapter.
+
+ /** Unknown source. */
+ public static final int SOURCE_UNKNOWN = 0;
+
+ /**
+ * Event is based on the user's head. Ray origin is at average between eyes, pushed out to
+ * the near clipping plane for both eyes and points in direction head is facing. Action
+ * state is based on volume up button being depressed.
+ *
+ * <p>Events from this source are considered sensitive and hover events are never sent.
+ */
+ public static final int SOURCE_HEAD = 1;
+
+ /**
+ * Event is based on (one of) the user's controller(s). Ray origin and direction are for a
+ * controller aim pose as defined by OpenXR. (<a
+ * href="https://registry.khronos.org/OpenXR/specs/1.1/html/xrspec.html#semantic-paths-standard-pose-identifiers">...</a>)
+ * Action state is based on the primary button on the controller, usually the bottom-most
+ * face button.
+ */
+ public static final int SOURCE_CONTROLLER = 2;
+
+ /**
+ * Event is based on one of the user's hands. Ray is a hand aim pose, with origin between
+ * thumb and forefinger and points in direction based on hand orientation. Action state is
+ * based on a pinch gesture.
+ */
+ public static final int SOURCE_HANDS = 3;
+
+ /**
+ * Event is based on a 2D mouse pointing device. Ray origin behaves the same as for
+ * DEVICE_TYPE_HEAD and points in direction based on mouse movement. During a drag, the ray
+ * origin moves approximating hand motion. The scrollwheel moves the ray away from / towards
+ * the user. Action state is based on the primary mouse button.
+ */
+ public static final int SOURCE_MOUSE = 4;
+
+ /**
+ * Event is based on a mix of the head, eyes, and hands. Ray origin is at average between
+ * eyes and points in direction based on a mix of eye gaze direction and hand motion. During
+ * a two-handed zoom/rotate gesture, left/right pointer events will be issued; otherwise,
+ * default events are issued based on the gaze ray. Action state is based on if the user has
+ * done a pinch gesture or not.
+ *
+ * <p>Events from this source are considered sensitive and hover events are never sent.
+ */
+ public static final int SOURCE_GAZE_AND_GESTURE = 5;
+
+ /**
+ * Default pointer type for the source (no handedness). Occurs for SOURCE_UNKNOWN,
+ * SOURCE_HEAD, SOURCE_MOUSE, and SOURCE_GAZE_AND_GESTURE.
+ */
+ public static final int POINTER_TYPE_DEFAULT = 0;
+
+ /**
+ * Left hand / controller pointer. Occurs for SOURCE_CONTROLLER, SOURCE_HANDS, and
+ * SOURCE_GAZE_AND_GESTURE.
+ */
+ public static final int POINTER_TYPE_LEFT = 1;
+
+ /**
+ * Right hand / controller pointer. Occurs for SOURCE_CONTROLLER, SOURCE_HANDS, and
+ * SOURCE_GAZE_AND_GESTURE.
+ */
+ public static final int POINTER_TYPE_RIGHT = 2;
+
+ /** The primary action button or gesture was just pressed / started. */
+ public static final int ACTION_DOWN = 0;
+
+ /**
+ * The primary action button or gesture was just released / stopped. The hit info represents
+ * the node that was originally hit (ie, as provided in the ACTION_DOWN event).
+ */
+ public static final int ACTION_UP = 1;
+
+ /**
+ * The primary action button or gesture was pressed/active in the previous event, and is
+ * still pressed/active. The hit info represents the node that was originally hit (ie, as
+ * provided in the ACTION_DOWN event). The hit position may be null if the pointer is no
+ * longer hitting that node.
+ */
+ public static final int ACTION_MOVE = 2;
+
+ /**
+ * While the primary action button or gesture was held, the pointer was disabled. This
+ * happens if you are using controllers and the battery runs out, or if you are using a
+ * source that transitions to a new pointer type, eg SOURCE_GAZE_AND_GESTURE.
+ */
+ public static final int ACTION_CANCEL = 3;
+
+ /**
+ * The primary action button or gesture is not pressed, and the pointer ray continued to hit
+ * the same node. The hit info represents the node that was hit (may be null if pointer
+ * capture is enabled).
+ *
+ * <p>Hover input events are never provided for sensitive source types.
+ */
+ public static final int ACTION_HOVER_MOVE = 4;
+
+ /**
+ * The primary action button or gesture is not pressed, and the pointer ray started to hit a
+ * new node. The hit info represents the node that is being hit (may be null if pointer
+ * capture is enabled).
+ *
+ * <p>Hover input events are never provided for sensitive source types.
+ */
+ public static final int ACTION_HOVER_ENTER = 5;
+
+ /**
+ * The primary action button or gesture is not pressed, and the pointer ray stopped hitting
+ * the node that it was previously hitting. The hit info represents the node that was being
+ * hit (may be null if pointer capture is enabled).
+ *
+ * <p>Hover input events are never provided for sensitive source types.
+ */
+ public static final int ACTION_HOVER_EXIT = 6;
+
+ @SuppressWarnings("MutableBareField")
+ @Source
+ public int source;
+
+ @SuppressWarnings("MutableBareField")
+ @PointerType
+ public int pointerType;
+
+ /** The time this event occurred, in the android.os.SystemClock#uptimeMillis time base. */
+ @SuppressWarnings({
+ "GoodTime",
+ "MutableBareField"
+ }) // This field mirrors the XR Extensions InputEvent.
+ public long timestamp;
+
+ /**
+ * The origin of the ray, in the receiver's activity space. Will be zero if the source is
+ * not ray-based (eg, direct touch).
+ */
+ @SuppressWarnings("MutableBareField")
+ @NonNull
+ public Vector3 origin;
+
+ /**
+ * A point indicating the direction the ray is pointing in, in the receiver's activity
+ * space. The ray is a vector starting at the origin point and passing through the direction
+ * point.
+ */
+ @SuppressWarnings("MutableBareField")
+ @NonNull
+ public Vector3 direction;
+
+ /** Info about the hit result of the ray. */
+ public static class HitInfo {
+ /**
+ * The entity that was hit by the input ray.
+ *
+ * <p>ACTION_MOVE, ACTION_UP, and ACTION_CANCEL events will report the same node as was
+ * hit during the initial ACTION_DOWN.
+ */
+ @Nullable public final Entity inputEntity;
+
+ /**
+ * The position of the hit in the receiver's activity space.
+ *
+ * <p>All events may report the current ray's hit position. This can be null if there no
+ * longer is a collision between the ray and the input node (eg, during a drag event).
+ */
+ @Nullable public final Vector3 hitPosition;
+
+ /**
+ * The matrix transforming activity space coordinates into the hit entity's local
+ * coordinate space.
+ */
+ @NonNull public final Matrix4 transform;
+
+ /**
+ * @param inputEntity the entity that was hit by the input ray.
+ * @param hitPosition the position of the hit in the receiver's activity space.
+ * @param transform the matrix transforming activity space coordinates into the hit
+ * entity's local coordinate space.
+ */
+ public HitInfo(
+ @Nullable Entity inputEntity,
+ @Nullable Vector3 hitPosition,
+ @NonNull Matrix4 transform) {
+ this.inputEntity = inputEntity;
+ this.hitPosition = hitPosition;
+ this.transform = transform;
+ }
+ }
+
+ /** Returns the current action associated with this input event. */
+ @SuppressWarnings("MutableBareField")
+ public int action;
+
+ /**
+ * Info on the first entity (closest to the ray origin) that was hit by the input ray, if
+ * any. This info will be null if no Entity was hit.
+ */
+ @SuppressWarnings("MutableBareField")
+ @Nullable
+ public HitInfo hitInfo;
+
+ /** Info on the second entity for the same task that was hit by the input ray, if any. */
+ @SuppressWarnings("MutableBareField")
+ @Nullable
+ public HitInfo secondaryHitInfo;
+
+ @SuppressWarnings("GoodTime")
+ public InputEvent(
+ @Source int source,
+ @PointerType int pointerType,
+ long timestamp,
+ @NonNull Vector3 origin,
+ @NonNull Vector3 direction,
+ @Action int action,
+ @Nullable HitInfo hitInfo,
+ @Nullable HitInfo secondaryHitInfo) {
+ this.source = source;
+ this.pointerType = pointerType;
+ this.timestamp = timestamp;
+ this.origin = origin;
+ this.direction = direction;
+ this.action = action;
+ this.hitInfo = hitInfo;
+ this.secondaryHitInfo = secondaryHitInfo;
+ }
+
+ /** Describes the hardware source of the event. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ SOURCE_UNKNOWN,
+ SOURCE_HEAD,
+ SOURCE_CONTROLLER,
+ SOURCE_HANDS,
+ SOURCE_MOUSE,
+ SOURCE_GAZE_AND_GESTURE,
+ })
+ public @interface Source {}
+
+ /** The type of the individual pointer. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ POINTER_TYPE_DEFAULT, // Default for the source.
+ POINTER_TYPE_LEFT, // Left hand/controller.
+ POINTER_TYPE_RIGHT, // Right hand/controller.
+ })
+ public @interface PointerType {}
+
+ /**
+ * Actions similar to Android's MotionEvent actions: <a
+ * href="https://developer.android.com/reference/android/view/MotionEvent"></a> for keeping
+ * track of a sequence of events on the same target, e.g., * HOVER_ENTER -> HOVER_MOVE ->
+ * HOVER_EXIT * DOWN -> MOVE -> UP
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ ACTION_DOWN,
+ ACTION_UP,
+ ACTION_MOVE,
+ ACTION_CANCEL,
+ ACTION_HOVER_MOVE,
+ ACTION_HOVER_ENTER,
+ ACTION_HOVER_EXIT,
+ })
+ public @interface Action {}
+ }
+
+ /** Spatial Capabilities for SceneCore Platform. */
+ class SpatialCapabilities {
+
+ /** The activity can spatialize itself by e.g. adding a spatial panel. */
+ public static final int SPATIAL_CAPABILITY_UI = 1 << 0;
+
+ /** The activity can create 3D contents. */
+ public static final int SPATIAL_CAPABILITY_3D_CONTENT = 1 << 1;
+
+ /** The activity can enable or disable passthrough. */
+ public static final int SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL = 1 << 2;
+
+ /** The activity can set its own environment. */
+ public static final int SPATIAL_CAPABILITY_APP_ENVIRONMENT = 1 << 3;
+
+ /** The activity can use spatial audio. */
+ public static final int SPATIAL_CAPABILITY_SPATIAL_AUDIO = 1 << 4;
+
+ /** The activity can spatially embed another activity. */
+ public static final int SPATIAL_CAPABILITY_EMBED_ACTIVITY = 1 << 5;
+
+ /** Spatial Capabilities for SceneCore Platform. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {
+ SPATIAL_CAPABILITY_UI,
+ SPATIAL_CAPABILITY_3D_CONTENT,
+ SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL,
+ SPATIAL_CAPABILITY_APP_ENVIRONMENT,
+ SPATIAL_CAPABILITY_SPATIAL_AUDIO,
+ SPATIAL_CAPABILITY_EMBED_ACTIVITY,
+ })
+ public @interface SpatialCapability {}
+
+ @SuppressWarnings("MutableBareField")
+ @SpatialCapability
+ public int capabilities;
+
+ public SpatialCapabilities(@SpatialCapability int capabilities) {
+ this.capabilities = capabilities;
+ }
+
+ public boolean hasCapability(@SpatialCapability int capability) {
+ return (capabilities & capability) != 0;
+ }
+ }
+
+ /** Interface for a SceneCore SoundPoolExtensionsWrapper. */
+ interface SoundPoolExtensionsWrapper {
+
+ int play(
+ @NonNull SoundPool soundPool,
+ int soundId,
+ @NonNull PointSourceAttributes attributes,
+ float volume,
+ int priority,
+ int loop,
+ float rate);
+
+ int play(
+ @NonNull SoundPool soundPool,
+ int soundId,
+ @NonNull SoundFieldAttributes attributes,
+ float volume,
+ int priority,
+ int loop,
+ float rate);
+
+ @SpatializerConstants.SourceType
+ int getSpatialSourceType(@NonNull SoundPool soundPool, int streamId);
+ }
+
+ /** Interface for a SceneCore AudioTrackExtensionsWrapper */
+ interface AudioTrackExtensionsWrapper {
+
+ @Nullable
+ PointSourceAttributes getPointSourceAttributes(@NonNull AudioTrack track);
+
+ @Nullable
+ SoundFieldAttributes getSoundFieldAttributes(@NonNull AudioTrack track);
+
+ int getSpatialSourceType(@NonNull AudioTrack track);
+
+ @NonNull
+ AudioTrack.Builder setPointSourceAttributes(
+ @NonNull AudioTrack.Builder builder, @NonNull PointSourceAttributes attributes);
+
+ @NonNull
+ AudioTrack.Builder setSoundFieldAttributes(
+ @NonNull AudioTrack.Builder builder, @NonNull SoundFieldAttributes attributes);
+ }
+
+ /** Interface for a SceneCore MediaPlayerExtensionsWrapper */
+ interface MediaPlayerExtensionsWrapper {
+
+ void setPointSourceAttributes(
+ @NonNull MediaPlayer mediaPlayer, @NonNull PointSourceAttributes attributes);
+
+ void setSoundFieldAttributes(
+ @NonNull MediaPlayer mediaPlayer, @NonNull SoundFieldAttributes attributes);
+ }
+
+ /** Represents a SceneCore PointSourceAttributes */
+ class PointSourceAttributes {
+ private final Entity entity;
+
+ public PointSourceAttributes(@NonNull Entity entity) {
+ this.entity = entity;
+ }
+
+ /** Gets the SceneCore {@link Entity} for this instance. */
+ @NonNull
+ public Entity getEntity() {
+ return this.entity;
+ }
+ }
+
+ /** Represents a SceneCore SoundFieldAttributes */
+ class SoundFieldAttributes {
+
+ @SpatializerConstants.AmbisonicsOrder private final int ambisonicsOrder;
+
+ public SoundFieldAttributes(int ambisonicsOrder) {
+ this.ambisonicsOrder = ambisonicsOrder;
+ }
+
+ public int getAmbisonicsOrder() {
+ return ambisonicsOrder;
+ }
+ }
+
+ /** Contains the constants used to spatialize audio in SceneCore. */
+ final class SpatializerConstants {
+
+ private SpatializerConstants() {}
+
+ /** Used to set the Ambisonics order of a [SoundFieldAttributes]. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ AMBISONICS_ORDER_FIRST_ORDER,
+ AMBISONICS_ORDER_SECOND_ORDER,
+ AMBISONICS_ORDER_THIRD_ORDER,
+ })
+ public @interface AmbisonicsOrder {}
+
+ /** Specifies spatial rendering using First Order Ambisonics */
+ public static final int AMBISONICS_ORDER_FIRST_ORDER = 0;
+
+ /** Specifies spatial rendering using Second Order Ambisonics */
+ public static final int AMBISONICS_ORDER_SECOND_ORDER = 1;
+
+ /** Specifies spatial rendering using Third Order Ambisonics */
+ public static final int AMBISONICS_ORDER_THIRD_ORDER = 2;
+
+ /** Represents the type of spatialization for an audio source. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ SOURCE_TYPE_BYPASS,
+ SOURCE_TYPE_POINT_SOURCE,
+ SOURCE_TYPE_SOUND_FIELD,
+ })
+ public @interface SourceType {}
+
+ /** The sound source has not been spatialized with the Spatial Audio SDK. */
+ public static final int SOURCE_TYPE_BYPASS = 0;
+
+ /** The sound source has been spatialized as a 3D point source. */
+ public static final int SOURCE_TYPE_POINT_SOURCE = 1;
+
+ /** The sound source is an ambisonics sound field. */
+ public static final int SOURCE_TYPE_SOUND_FIELD = 2;
+ }
+
+ /** Returns a [SoundPoolExtensionsWrapper] instance. */
+ @NonNull
+ public SoundPoolExtensionsWrapper getSoundPoolExtensionsWrapper();
+
+ /** Returns an [AudioTrackExtensionssWrapper] instance. */
+ @NonNull
+ public AudioTrackExtensionsWrapper getAudioTrackExtensionsWrapper();
+
+ /** Returns a [MediaPlayerExtensionsWrapper] instance. */
+ @NonNull
+ public MediaPlayerExtensionsWrapper getMediaPlayerExtensionsWrapper();
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/Model.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/Model.kt
new file mode 100644
index 0000000..3d6459e
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/Model.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import androidx.annotation.RestrictTo
+import androidx.concurrent.futures.ResolvableFuture
+import androidx.xr.scenecore.JxrPlatformAdapter.GltfModelResource as RtGltfModel
+import com.google.common.util.concurrent.ListenableFuture
+
+/** Represents a 3D model in SceneCore. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Model
+
+/**
+ * [GltfModel] represents a glTF resource in SceneCore. These can be used as part of the
+ * [Environment] or to display 3D models with [GltfModelEntity]. These are created by the [Session].
+ */
+// TODO: b/319269278 - Make this and ExrImage derive from a common Resource base class which has
+// async helpers.
+// TODO: b/362368652 - Add an interface which returns an integer animation IDX given a string
+// animation name for a loaded glTF, as well as an interface for selecting the
+// playback animation from the integer index.
+// TODO: b/362368652 - Add an interface which returns a list of available animation names
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class GltfModel internal constructor(public val model: RtGltfModel) : Model {
+
+ internal companion object {
+ @Deprecated(
+ message = "This function is deprecated, use createAsync() instead",
+ replaceWith = ReplaceWith("createAsync()"),
+ )
+ internal fun create(runtime: JxrPlatformAdapter, name: String): GltfModel {
+ val gltfResourceFuture = runtime.loadGltfByAssetName(name)
+ // TODO: b/320858652 - Implement async loading of GltfModel.
+ return GltfModel(gltfResourceFuture!!.get())
+ }
+
+ // ResolvableFuture is marked as RestrictTo(LIBRARY_GROUP_PREFIX), which is intended for
+ // classes
+ // within AndroidX. We're in the process of migrating to AndroidX. Without suppressing this
+ // warning, however, we get a build error - go/bugpattern/RestrictTo.
+ @SuppressWarnings("RestrictTo")
+ internal fun createAsync(
+ runtime: JxrPlatformAdapter,
+ name: String,
+ ): ListenableFuture<GltfModel> {
+ val gltfResourceFuture = runtime.loadGltfByAssetNameSplitEngine(name)
+ val modelFuture = ResolvableFuture.create<GltfModel>()
+
+ // TODO: b/375070346 - remove this `!!` when we're sure the future is non-null.
+ gltfResourceFuture!!.addListener(
+ {
+ try {
+ val gltfResource = gltfResourceFuture.get()
+ modelFuture.set(GltfModel(gltfResource))
+ } catch (e: Exception) {
+ if (e is InterruptedException) {
+ Thread.currentThread().interrupt()
+ }
+ modelFuture.setException(e)
+ }
+ },
+ Runnable::run,
+ )
+ return modelFuture
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as GltfModel
+
+ // Perform a structural equality check on the underlying model.
+ if (model != other.model) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return model.hashCode()
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/MovableComponent.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/MovableComponent.kt
new file mode 100644
index 0000000..9e3bde9
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/MovableComponent.kt
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+@file:Suppress("BanConcurrentHashMap")
+
+package androidx.xr.scenecore
+
+import android.util.Log
+import androidx.annotation.RestrictTo
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.Executor
+
+/**
+ * Allows users to interactively move the Entity. This component can be attached to a single
+ * instance of any PanelEntity.
+ *
+ * NOTE: This Component is currently unsupported on GltfModelEntity.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class MovableComponent
+private constructor(
+ private val runtime: JxrPlatformAdapter,
+ private val entityManager: EntityManager,
+ private val systemMovable: Boolean = true,
+ private val scaleInZ: Boolean = true,
+ private val anchorPlacement: Set<AnchorPlacement> = emptySet(),
+ private val shouldDisposeParentAnchor: Boolean = true,
+) : Component {
+ private val rtMovableComponent by lazy {
+ runtime.createMovableComponent(
+ systemMovable,
+ scaleInZ,
+ anchorPlacement.toRtAnchorPlacement(runtime),
+ shouldDisposeParentAnchor,
+ )
+ }
+ private val moveListenersMap =
+ ConcurrentHashMap<MoveListener, JxrPlatformAdapter.MoveEventListener>()
+
+ private var entity: Entity? = null
+
+ /**
+ * The current size of the entity, in meters. The size of the entity determines the size of the
+ * bounding box that is used to draw the draggable move affordances around the entity.
+ */
+ public var size: Dimensions = kDimensionsOneMeter
+ set(value) {
+ if (field != value) {
+ field = value
+ rtMovableComponent.setSize(value.toRtDimensions())
+ }
+ }
+
+ override fun onAttach(entity: Entity): Boolean {
+ if (this.entity != null) {
+ Log.e("MovableComponent", "Already attached to entity ${this.entity}")
+ return false
+ }
+ this.entity = entity
+ return (entity as BaseEntity<*>).rtEntity.addComponent(rtMovableComponent)
+ }
+
+ override fun onDetach(entity: Entity) {
+ (entity as BaseEntity<*>).rtEntity.removeComponent(rtMovableComponent)
+ this.entity = null
+ }
+
+ /**
+ * Adds a listener to the set of active listeners for the move events. The listener will be
+ * invoked regardless of whether the entity is being moved by the system or the user.
+ *
+ * <p>The listener is invoked on the provided executor. If the app intends to modify the UI
+ * elements/views during the callback, the app should provide the thread executor that is
+ * appropriate for the UI operations. For example, if the app is using the main thread to render
+ * the UI, the app should provide the main thread (Looper.getMainLooper()) executor. If the app
+ * is using a separate thread to render the UI, the app should provide the executor for that
+ * thread.
+ *
+ * @param executor The executor to run the listener on.
+ * @param moveListener The move event listener to set.
+ */
+ public fun addMoveListener(executor: Executor, moveListener: MoveListener) {
+ val rtMoveEventListener =
+ JxrPlatformAdapter.MoveEventListener { rtMoveEvent ->
+ run {
+ // TODO: b/369157703 - Mirror the callback hierarchy in the runtime API.
+ val moveEvent = rtMoveEvent.toMoveEvent(entityManager)
+ when (moveEvent.moveState) {
+ MoveEvent.MOVE_STATE_START ->
+ entity?.let {
+ moveListener.onMoveStart(
+ it,
+ moveEvent.initialInputRay,
+ moveEvent.previousPose,
+ moveEvent.previousScale,
+ moveEvent.initialParent,
+ )
+ }
+ MoveEvent.MOVE_STATE_ONGOING ->
+ entity?.let {
+ moveListener.onMoveUpdate(
+ it,
+ moveEvent.currentInputRay,
+ moveEvent.currentPose,
+ moveEvent.currentScale,
+ )
+ }
+ MoveEvent.MOVE_STATE_END ->
+ entity?.let {
+ moveListener.onMoveEnd(
+ it,
+ moveEvent.currentInputRay,
+ moveEvent.currentPose,
+ moveEvent.currentScale,
+ moveEvent.updatedParent,
+ )
+ }
+ }
+ }
+ }
+ rtMovableComponent.addMoveEventListener(executor, rtMoveEventListener)
+ moveListenersMap[moveListener] = rtMoveEventListener
+ }
+
+ /**
+ * Removes a listener from the set of active listeners for the move events.
+ *
+ * @param moveListener The move event listener to remove.
+ */
+ public fun removeMoveListener(moveListener: MoveListener) {
+ val rtMoveEventListener = moveListenersMap.remove(moveListener)
+ if (rtMoveEventListener != null) {
+ rtMovableComponent.removeMoveEventListener(rtMoveEventListener)
+ }
+ }
+
+ internal companion object {
+ private val kDimensionsOneMeter = Dimensions(1f, 1f, 1f)
+
+ /** Factory function for creating a MovableComponent. */
+ internal fun create(
+ runtime: JxrPlatformAdapter,
+ entityManager: EntityManager,
+ systemMovable: Boolean = true,
+ scaleInZ: Boolean = true,
+ anchorPlacement: Set<AnchorPlacement> = emptySet(),
+ shouldDisposeParentAnchor: Boolean = true,
+ ): MovableComponent {
+ return MovableComponent(
+ runtime,
+ entityManager,
+ systemMovable,
+ scaleInZ,
+ anchorPlacement,
+ shouldDisposeParentAnchor,
+ )
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/MoveEvent.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/MoveEvent.kt
new file mode 100644
index 0000000..7f5002e
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/MoveEvent.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Ray
+
+/**
+ * A high-level move event which is sent in response to the User interacting with the Entity.
+ *
+ * @param moveState State of the move action i.e. move started, ongoing or ended.
+ * @param initialInputRay Ray for the user's input at initial update.
+ * @param currentInputRay Ray for the user's input at the new update.
+ * @param previousPose Pose before this event, relative to its parent.
+ * @param currentPose Pose when this event is applied, relative to its parent.
+ * @param previousScale Scale before this event.
+ * @param currentScale Scale when this event is applied.
+ */
+internal class MoveEvent(
+ @MoveState public val moveState: Int,
+ public val initialInputRay: Ray,
+ public val currentInputRay: Ray,
+ public val previousPose: Pose,
+ public val currentPose: Pose,
+ public val previousScale: Float,
+ public val currentScale: Float,
+ public val initialParent: Entity,
+ public val updatedParent: Entity?,
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is MoveEvent) return false
+
+ if (moveState != other.moveState) return false
+ if (initialInputRay != other.initialInputRay) return false
+ if (currentInputRay != other.currentInputRay) return false
+ if (previousPose != other.previousPose) return false
+ if (currentPose != other.currentPose) return false
+ if (previousScale != other.previousScale) return false
+ if (currentScale != other.currentScale) return false
+ if (initialParent != other.initialParent) return false
+ if (updatedParent != other.updatedParent) return false
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = moveState.hashCode()
+ result = 31 * result + initialInputRay.hashCode()
+ result = 31 * result + currentInputRay.hashCode()
+ result = 31 * result + previousPose.hashCode()
+ result = 31 * result + currentPose.hashCode()
+ result = 31 * result + previousScale.hashCode()
+ result = 31 * result + currentScale.hashCode()
+ result = 31 * result + initialParent.hashCode()
+ if (updatedParent != null) {
+ result = 31 * result + updatedParent.hashCode()
+ }
+ return result
+ }
+
+ public companion object {
+ public const val MOVE_STATE_START: Int = 1
+ public const val MOVE_STATE_ONGOING: Int = 2
+ public const val MOVE_STATE_END: Int = 3
+ }
+
+ @IntDef(value = [MOVE_STATE_START, MOVE_STATE_ONGOING, MOVE_STATE_END])
+ public annotation class MoveState
+}
+
+/** Listener for move actions. Callbacks are invoked as user interacts with the entity. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface MoveListener {
+ /**
+ * Called when the user starts moving the entity.
+ *
+ * @param entity The entity being moved.
+ * @param initialInputRay Ray for the user's input at initial update.
+ * @param initialPose Initial Pose of the entity relative to its parent.
+ * @param initialScale Initial scale of the entity.
+ * @param initialParent Initial parent of the entity.
+ */
+ public fun onMoveStart(
+ entity: Entity,
+ initialInputRay: Ray,
+ initialPose: Pose,
+ initialScale: Float,
+ initialParent: Entity,
+ ) {}
+
+ /**
+ * Called continuously while the user is moving the entity.
+ *
+ * @param entity The entity being moved.
+ * @param currentInputRay Ray for the user's input at the new update.
+ * @param currentPose Pose of the entity during this event relative to its parent.
+ * @param currentScale Scale of the entity during this event.
+ */
+ public fun onMoveUpdate(
+ entity: Entity,
+ currentInputRay: Ray,
+ currentPose: Pose,
+ currentScale: Float,
+ ) {}
+
+ /**
+ * Called when the user has finished moving the entity.
+ *
+ * @param entity The entity being moved.
+ * @param finalInputRay Ray for the user's input at the final update.
+ * @param finalPose Pose of the entity during this event relative to its parent.
+ * @param finalScale Scale of the entity during this event.
+ * @param updatedParent If anchorPlacement is set, the entity may have a new parent when the
+ * movement completes. This will be a new AnchorEntity, if it was anchored or re-anchored
+ * during the movement, or the activity space, if it was unanchored. This will be null if
+ * there was no updated parent on the entity.
+ */
+ public fun onMoveEnd(
+ entity: Entity,
+ finalInputRay: Ray,
+ finalPose: Pose,
+ finalScale: Float,
+ updatedParent: Entity?,
+ ) {}
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/PermissionHelper.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/PermissionHelper.kt
new file mode 100644
index 0000000..cde0f02
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/PermissionHelper.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.provider.Settings
+import androidx.annotation.RestrictTo
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+
+/**
+ * Utility class for handling Android permissions. SceneCore applications should use this before
+ * creating Anchors.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public object PermissionHelper {
+ public const val SCENE_UNDERSTANDING_PERMISSION_CODE: Int = 0
+ public const val SCENE_UNDERSTANDING_PERMISSION: String =
+ "android.permission.SCENE_UNDERSTANDING"
+
+ public fun hasPermission(activity: Activity, permission: String): Boolean =
+ ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED
+
+ public fun requestPermission(
+ activity: Activity,
+ permission: String,
+ permissionCode: Int
+ ): Unit = ActivityCompat.requestPermissions(activity, arrayOf(permission), permissionCode)
+
+ public fun shouldShowRequestPermissionRationale(
+ activity: Activity,
+ permission: String
+ ): Boolean = ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
+
+ public fun launchPermissionSettings(activity: Activity) {
+ val intent = Intent()
+ intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ intent.setData(Uri.fromParts("package", activity.packageName, null))
+ activity.startActivity(intent)
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/PointSourceAttributes.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/PointSourceAttributes.kt
new file mode 100644
index 0000000..7095c66
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/PointSourceAttributes.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import androidx.annotation.RestrictTo
+
+/**
+ * PointSourceAttributes is used to configure a sound be spatialized as a 3D point.
+ *
+ * If the audio being played is stereo or multichannel AND the AudioAttributes USAGE_TYPE is
+ * USAGE_MEDIA then the point provided will serve as the focal point of the media sound bed.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class PointSourceAttributes(public val entity: Entity) {
+
+ internal val rtPointSourceAttributes: JxrPlatformAdapter.PointSourceAttributes =
+ JxrPlatformAdapter.PointSourceAttributes((entity as BaseEntity<*>).rtEntity)
+}
+
+internal fun JxrPlatformAdapter.PointSourceAttributes.toPointSourceAttributes(
+ session: Session
+): PointSourceAttributes? {
+ val jxrEntity = session.getEntityForRtEntity(entity)
+ return jxrEntity?.let { PointSourceAttributes(it) }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/PointerCaptureComponent.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/PointerCaptureComponent.kt
new file mode 100644
index 0000000..48b1150
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/PointerCaptureComponent.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import android.util.Log
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import java.util.concurrent.Executor
+
+/**
+ * Provides pointer capture capabilities for a given entity.
+ *
+ * To enable pointer capture, the task must be in full space, and the entity must be visible.
+ *
+ * Only one PointerCaptureComponent can be attached to an entity at a given time. If a second one
+ * tries to attach to an entity, it will fail.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class PointerCaptureComponent
+private constructor(
+ private val runtime: JxrPlatformAdapter,
+ private val entityManager: EntityManager,
+ private val executor: Executor,
+ private val stateListener: StateListener,
+ private val inputEventListener: InputEventListener,
+) : Component {
+
+ private var attachedEntity: Entity? = null
+
+ private val rtInputEventListener =
+ JxrPlatformAdapter.InputEventListener { rtEvent ->
+ inputEventListener.onInputEvent(rtEvent.toInputEvent(entityManager))
+ }
+
+ private val rtStateListener =
+ JxrPlatformAdapter.PointerCaptureComponent.StateListener { pcState: Int ->
+ when (pcState) {
+ JxrPlatformAdapter.PointerCaptureComponent.POINTER_CAPTURE_STATE_PAUSED ->
+ stateListener.onStateChanged(POINTER_CAPTURE_STATE_PAUSED)
+ JxrPlatformAdapter.PointerCaptureComponent.POINTER_CAPTURE_STATE_ACTIVE ->
+ stateListener.onStateChanged(POINTER_CAPTURE_STATE_ACTIVE)
+ JxrPlatformAdapter.PointerCaptureComponent.POINTER_CAPTURE_STATE_STOPPED ->
+ stateListener.onStateChanged(POINTER_CAPTURE_STATE_STOPPED)
+ else -> {
+ Log.e(TAG, "Unknown pointer capture state received: ${pcState}")
+ stateListener.onStateChanged(pcState)
+ }
+ }
+ }
+
+ private val rtComponent by lazy {
+ runtime.createPointerCaptureComponent(executor, rtStateListener, rtInputEventListener)
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(
+ value =
+ [
+ POINTER_CAPTURE_STATE_PAUSED,
+ POINTER_CAPTURE_STATE_ACTIVE,
+ POINTER_CAPTURE_STATE_STOPPED
+ ]
+ )
+ internal annotation class PointerCaptureState
+
+ /** Listener for pointer capture state changes. */
+ public interface StateListener {
+ public fun onStateChanged(@PointerCaptureState newState: Int)
+ }
+
+ override fun onAttach(entity: Entity): Boolean {
+ if (attachedEntity != null) {
+ Log.e(TAG, "Already attached to entity ${attachedEntity}")
+ return false
+ }
+ attachedEntity = entity
+
+ return (entity as BaseEntity<*>).rtEntity.addComponent(rtComponent)
+ }
+
+ override fun onDetach(entity: Entity) {
+ if (entity != attachedEntity) {
+ Log.e(TAG, "Detaching from non-attached entity, ignoring")
+ return
+ }
+ (entity as BaseEntity<*>).rtEntity.removeComponent(rtComponent)
+ attachedEntity = null
+ }
+
+ public companion object {
+ /** Pointer Capture is enabled for this component. */
+ public const val POINTER_CAPTURE_STATE_PAUSED: Int = 0
+
+ /** Pointer Capture is disabled for this component. */
+ public const val POINTER_CAPTURE_STATE_ACTIVE: Int = 1
+
+ /** Pointer Capture has been stopped for this component. */
+ public const val POINTER_CAPTURE_STATE_STOPPED: Int = 2
+
+ private const val TAG: String = "PointerCaptureComponent"
+
+ /** Factory function for creating [PointerCaptureComponent] instances. */
+ @JvmStatic
+ public fun create(
+ session: Session,
+ executor: Executor,
+ stateListener: StateListener,
+ inputListener: InputEventListener,
+ ): PointerCaptureComponent =
+ PointerCaptureComponent(
+ session.runtime,
+ session.entityManager,
+ executor,
+ stateListener,
+ inputListener,
+ )
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ResizableComponent.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ResizableComponent.kt
new file mode 100644
index 0000000..def8015
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ResizableComponent.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+@file:Suppress("BanConcurrentHashMap")
+
+package androidx.xr.scenecore
+
+import android.util.Log
+import androidx.annotation.RestrictTo
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.Executor
+
+/**
+ * A [Component] which when attached to a [PanelEntity] provides a user-resize affordance.
+ *
+ * Note: This Component is currently unsupported on GltfModelEntity.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class ResizableComponent
+private constructor(
+ private val runtime: JxrPlatformAdapter,
+ minimumSize: Dimensions,
+ maximumSize: Dimensions,
+) : Component {
+ private val resizeListenerMap =
+ ConcurrentHashMap<ResizeListener, JxrPlatformAdapter.ResizeEventListener>()
+ /**
+ * The current size of the entity, in meters. This property is automatically updated after
+ * resize events to match the resize affordance to the newly suggested size of the content. The
+ * apps can still override it. The default value is set to 1 meter, updated to the size of the
+ * entity when attached.
+ */
+ public var size: Dimensions = kDimensionsOneMeter
+ set(value) {
+ if (field != value) {
+ field = value
+ rtResizableComponent.setSize(value.toRtDimensions())
+ }
+ }
+
+ /**
+ * A lower bound for the User's resize actions, in meters. This value constrains how small the
+ * user can resize the bounding box of the entity. The size of the content inside that bounding
+ * box is fully controlled by the application.
+ */
+ public var minimumSize: Dimensions = minimumSize
+ set(value) {
+ if (field != value) {
+ field = value
+ rtResizableComponent.setMinimumSize(value.toRtDimensions())
+ }
+ }
+
+ /**
+ * An upper bound for the User's resize actions, in meters. This value constrains large the user
+ * can resize the bounding box of the entity. The size of the content inside that bounding box
+ * is fully controlled by the application.
+ */
+ public var maximumSize: Dimensions = maximumSize
+ set(value) {
+ if (field != value) {
+ field = value
+ rtResizableComponent.setMaximumSize(value.toRtDimensions())
+ }
+ }
+
+ /**
+ * The aspect ratio of the entity during resizing. The aspect ratio is determined by taking the
+ * entity's width over its height. A value of 0.0f (or negative) means there are no preferences.
+ *
+ * This method does not immediately resize the entity. The new aspect ratio will be applied the
+ * next time the user resizes the entity through the reform UI. During this resize operation,
+ * the entity's current area will be preserved.
+ *
+ * If a different resizing behavior is desired, such as fixing the width and adjusting the
+ * height, the client can manually resize the entity to the preferred dimensions before calling
+ * this method. No automatic resizing will occur when using the reform UI then.
+ */
+ public var fixedAspectRatio: Float = 0.0f
+ set(value) {
+ if (field != value) {
+ field = value
+ rtResizableComponent.setFixedAspectRatio(value)
+ }
+ }
+
+ private val rtResizableComponent by lazy {
+ runtime.createResizableComponent(minimumSize.toRtDimensions(), maximumSize.toRtDimensions())
+ }
+
+ private var entity: Entity? = null
+
+ /**
+ * Attaches this component to the given entity.
+ *
+ * @param entity The entity to attach this component to.
+ * @return `true` if the component was successfully attached, `false` otherwise.
+ */
+ override fun onAttach(entity: Entity): Boolean {
+ if (this.entity != null) {
+ Log.e("MovableComponent", "Already attached to entity ${this.entity}")
+ return false
+ }
+ this.entity = entity
+ return (entity as BaseEntity<*>).rtEntity.addComponent(rtResizableComponent)
+ }
+
+ /**
+ * Detaches this component from the entity it is attached to.
+ *
+ * @param entity The entity to detach this component from.
+ */
+ override fun onDetach(entity: Entity) {
+ (entity as BaseEntity<*>).rtEntity.removeComponent(rtResizableComponent)
+ this.entity = null
+ }
+
+ /**
+ * Adds the listener to the set of listeners that are invoked through the resize operation, such
+ * as start, ongoing and end.
+ *
+ * The listener is invoked on the provided executor. If the app intends to modify the UI
+ * elements/views during the callback, the app should provide the thread executor that is
+ * appropriate for the UI operations. For example, if the app is using the main thread to render
+ * the UI, the app should provide the main thread (Looper.getMainLooper()) executor. If the app
+ * is using a separate thread to render the UI, the app should provide the executor for that
+ * thread.
+ *
+ * @param executor The executor to use for the listener callback.
+ * @param resizeListener The listener to be invoked when a resize event occurs.
+ */
+ public fun addResizeListener(executor: Executor, resizeListener: ResizeListener) {
+ val rtResizeEventListener =
+ JxrPlatformAdapter.ResizeEventListener { rtResizeEvent ->
+ run {
+ val resizeEvent = rtResizeEvent.toResizeEvent()
+ when (resizeEvent.resizeState) {
+ ResizeEvent.RESIZE_STATE_ONGOING ->
+ entity?.let { resizeListener.onResizeUpdate(it, resizeEvent.newSize) }
+ ResizeEvent.RESIZE_STATE_END ->
+ entity?.let { resizeListener.onResizeEnd(it, resizeEvent.newSize) }
+ ResizeEvent.RESIZE_STATE_START ->
+ entity?.let { resizeListener.onResizeStart(it, size) }
+ }
+ }
+ }
+ rtResizableComponent.addResizeEventListener(executor, rtResizeEventListener)
+ resizeListenerMap[resizeListener] = rtResizeEventListener
+ }
+
+ /**
+ * Removes a listener from the set listening to resize events.
+ *
+ * @param resizeListener The listener to be removed.
+ */
+ public fun removeResizeListener(resizeListener: ResizeListener) {
+ if (resizeListenerMap.containsKey(resizeListener)) {
+ rtResizableComponent.removeResizeEventListener(resizeListenerMap[resizeListener]!!)
+ resizeListenerMap.remove(resizeListener)
+ }
+ }
+
+ internal companion object {
+ private val kDimensionsOneMeter = Dimensions(1f, 1f, 1f)
+ /** Defaults min and max sizes in meters. */
+ internal val kMinimumSize: Dimensions = Dimensions(0f, 0f, 0f)
+ internal val kMaximumSize: Dimensions = Dimensions(10f, 10f, 10f)
+
+ /** Factory function for creating [ResizableComponent] instance. */
+ internal fun create(
+ runtime: JxrPlatformAdapter,
+ minimumSize: Dimensions = kMinimumSize,
+ maximumSize: Dimensions = kMaximumSize,
+ ): ResizableComponent {
+ return ResizableComponent(runtime, minimumSize, maximumSize)
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ResizeEvent.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ResizeEvent.kt
new file mode 100644
index 0000000..2bc2c94
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ResizeEvent.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+
+/**
+ * A high-level resize event which is sent in response to the User interacting with the Entity.
+ *
+ * @param resizeState The state of the resize event.
+ * @param newSize The new proposed size of the Entity in meters.
+ */
+internal data class ResizeEvent(
+ @ResizeState public val resizeState: Int,
+ public val newSize: Dimensions,
+) {
+ public companion object {
+ /** Constant for {@link resizeState}: The resize state is unknown. */
+ public const val RESIZE_STATE_UNKNOWN: Int = 0
+ /** Constant for {@link resizeState}: The user has started dragging the resize handles. */
+ public const val RESIZE_STATE_START: Int = 1
+ /** Constant for {@link resizeState}: The user is continuing to drag the resize handles. */
+ public const val RESIZE_STATE_ONGOING: Int = 2
+ /** Constant for {@link resizeState}: The user has stopped dragging the resize handles. */
+ public const val RESIZE_STATE_END: Int = 3
+ }
+
+ @IntDef(
+ value = [RESIZE_STATE_UNKNOWN, RESIZE_STATE_START, RESIZE_STATE_ONGOING, RESIZE_STATE_END]
+ )
+ public annotation class ResizeState
+}
+
+/**
+ * Listener for resize actions. Callbacks are invoked as the user interacts with the resize
+ * affordance.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface ResizeListener {
+ /**
+ * Called when the user starts resizing the entity.
+ *
+ * @param entity The entity being resized.
+ * @param originalSize The original size of the entity in meters at the start of the resize
+ * operation.
+ */
+ public fun onResizeStart(entity: Entity, originalSize: Dimensions) {}
+
+ /**
+ * Called continuously while the user is resizing the entity.
+ *
+ * @param entity The entity being resized.
+ * @param newSize The new proposed size of the entity in meters.
+ */
+ public fun onResizeUpdate(entity: Entity, newSize: Dimensions) {}
+
+ /**
+ * Called when the user has finished resizing the entity, for example when the user concludes
+ * the resize gesture.
+ *
+ * @param entity The entity being resized.
+ * @param finalSize The final proposed size of the entity in meters.
+ */
+ public fun onResizeEnd(entity: Entity, finalSize: Dimensions) {}
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/Session.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/Session.kt
new file mode 100644
index 0000000..9c77976
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/Session.kt
@@ -0,0 +1,623 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+@file:Suppress("BanConcurrentHashMap", "Deprecation")
+
+package androidx.xr.scenecore
+
+import android.app.Activity
+import android.app.Application.ActivityLifecycleCallbacks
+import android.graphics.Rect
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import androidx.annotation.MainThread
+import androidx.annotation.RestrictTo
+import androidx.xr.arcore.Anchor
+import androidx.xr.runtime.math.Pose
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity as RtEntity
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatialCapabilities as RtSpatialCapabilities
+import androidx.xr.scenecore.SpatialCapabilities.SpatialCapability
+import androidx.xr.scenecore.impl.JxrPlatformAdapterAxr
+import com.google.common.util.concurrent.ListenableFuture
+import java.time.Duration
+import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.ConcurrentMap
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+import java.util.concurrent.ThreadFactory
+import java.util.function.Consumer
+
+/**
+ * The Session provides the primary interface to SceneCore functionality for the application. Each
+ * spatialized Activity must create and hold an instance of Session.
+ *
+ * Once created, the application can use the Session interfaces to create spatialized entities, such
+ * as Widget panels and geometric models, set the background environment, and anchor content to the
+ * real world.
+ */
+// TODO: Make this class thread safe.
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class Session(
+ public val activity: Activity,
+ public val runtime: JxrPlatformAdapter,
+ public val spatialEnvironment: SpatialEnvironment,
+) {
+ internal val entityManager by lazy { EntityManager() }
+
+ private val spatialCapabilitiesListeners:
+ ConcurrentMap<Consumer<SpatialCapabilities>, Consumer<RtSpatialCapabilities>> =
+ ConcurrentHashMap()
+
+ /**
+ * The ActivitySpace is a special entity that represents the space in which the application is
+ * launched. It is the default parent of all entities in the scene.
+ *
+ * The ActivitySpace is created automatically when the Session is created.
+ */
+ public val activitySpace: ActivitySpace = ActivitySpace.create(runtime, entityManager)
+
+ /** The SpatialUser contains information about the user. */
+ public val spatialUser: SpatialUser = SpatialUser.create(runtime)
+
+ /**
+ * A spatialized PanelEntity associated with the "main window" for the Activity. When in
+ * HomeSpace mode, this is the application's "main window".
+ *
+ * If called multiple times, this will return the same PanelEntity.
+ */
+ public val mainPanelEntity: PanelEntity =
+ PanelEntity.createMainPanelEntity(runtime, entityManager)
+
+ /**
+ * The PerceptionSpace represents the origin of the space in which the ARCore for XR API
+ * provides tracking info. The transformations provided by the PerceptionSpace are only valid
+ * for the call frame, as the transformation can be changed by the system at any time.
+ */
+ public val perceptionSpace: PerceptionSpace = PerceptionSpace.create(runtime)
+
+ // TODO: 378706624 - Remove this method once we have a better way to handle the root entity.
+ public val activitySpaceRoot: Entity by lazy {
+ entityManager.getEntityForRtEntity(runtime.activitySpaceRootImpl)!!
+ }
+
+ public companion object {
+ private const val TAG = "Session"
+ private val activitySessionMap = ConcurrentHashMap<Activity, Session>()
+
+ // TODO: b/323060217 - Move the platformAdapter behind a loader class that loads it in.
+ /**
+ * Creates a session and pairs it with an Activity and its lifecycle. If a session is
+ * already paired with an Activity, return that Session instead of creating a new one.
+ *
+ * For our Alpha release, we just directly instantiate the Android XR PlatformAdapter.
+ */
+ // TODO(b/326748782): Change the returned Session here to be nullable or asynchronous.
+ // TODO: b/372299691 - Rename the runtime parameter to platformAdapter.
+ @JvmStatic
+ @JvmOverloads
+ public fun create(activity: Activity, runtime: JxrPlatformAdapter? = null): Session {
+ // TODO(bhavsar): Rethink moving this check when integration with Spatial Activity
+ // happens.
+ if (
+ !PermissionHelper.hasPermission(
+ activity,
+ PermissionHelper.SCENE_UNDERSTANDING_PERMISSION
+ )
+ ) {
+ PermissionHelper.requestPermission(
+ activity,
+ PermissionHelper.SCENE_UNDERSTANDING_PERMISSION,
+ PermissionHelper.SCENE_UNDERSTANDING_PERMISSION_CODE,
+ )
+ }
+ return activitySessionMap.computeIfAbsent(activity) {
+ Log.i(TAG, "Creating session for activity $activity")
+ val session =
+ when (runtime) {
+ null -> {
+ val runtimeImpl =
+ JxrPlatformAdapterAxr.create(
+ activity,
+ Executors.newSingleThreadScheduledExecutor(
+ object : ThreadFactory {
+ override fun newThread(r: Runnable): Thread {
+ return Thread(r, "JXRCoreSession")
+ }
+ }
+ ),
+ )
+ Session(activity, runtimeImpl, SpatialEnvironment(runtimeImpl))
+ }
+ else -> Session(activity, runtime, SpatialEnvironment(runtime))
+ }
+ activity.registerActivityLifecycleCallbacks(
+ object : ActivityLifecycleCallbacks {
+ override fun onActivityCreated(
+ activity: Activity,
+ savedInstanceState: Bundle?
+ ) {}
+
+ override fun onActivityStarted(activity: Activity) {}
+
+ override fun onActivityResumed(activity: Activity) {
+ session.runtime.startRenderer()
+ }
+
+ override fun onActivityPaused(activity: Activity) {
+ session.runtime.stopRenderer()
+ }
+
+ override fun onActivityStopped(activity: Activity) {}
+
+ override fun onActivitySaveInstanceState(
+ activity: Activity,
+ outState: Bundle
+ ) {}
+
+ override fun onActivityDestroyed(activity: Activity) {
+ activitySessionMap.remove(activity)
+ session.entityManager.clear()
+ session.runtime.dispose()
+ }
+ }
+ )
+ session
+ }
+ }
+ }
+
+ /**
+ * Returns true if the Session is currently capable of the given [SpatialCapability], false
+ * otherwise. The available set of capabilities can change within a session.
+ */
+ @Deprecated(message = "Removing in favor of getSpatialCapabilities().hasCapability()")
+ // TODO: b/366214441 - Remove this method
+ public fun hasSpatialCapability(@SpatialCapability capability: Int): Boolean =
+ getSpatialCapabilities().hasCapability(capability)
+
+ /**
+ * Returns the current [SpatialCapabilities] of the Session. The set of capabilities can change
+ * within a session. The returned object will not update if the capabilities change; this method
+ * should be called again to get the latest set of capabilities.
+ */
+ public fun getSpatialCapabilities(): SpatialCapabilities =
+ runtime.spatialCapabilities.toSpatialCapabilities()
+
+ /**
+ * Adds the given [Consumer] as a listener to be invoked when this Session's current
+ * [SpatialCapabilities] change. [Consumer#accept(SpatialCapabilities)] will be invoked on the
+ * main thread.
+ */
+ public fun addSpatialCapabilitiesChangedListener(
+ listener: Consumer<SpatialCapabilities>
+ ): Unit = addSpatialCapabilitiesChangedListener(HandlerExecutor.mainThreadExecutor, listener)
+
+ /**
+ * Adds the given [Consumer] as a listener to be invoked when this Session's current
+ * [SpatialCapabilities] change. [Consumer#accept(SpatialCapabilities)] will be invoked on the
+ * given callbackExecutor, or the main thread if the callbackExecutor is null (default).
+ */
+ public fun addSpatialCapabilitiesChangedListener(
+ callbackExecutor: Executor,
+ listener: Consumer<SpatialCapabilities>,
+ ): Unit {
+ // wrap the client's listener in a callback that receives & converts the runtime
+ // SpatialCapabilities type.
+ val rtListener: Consumer<RtSpatialCapabilities> =
+ Consumer<RtSpatialCapabilities> { rtCaps: RtSpatialCapabilities ->
+ listener.accept(rtCaps.toSpatialCapabilities())
+ }
+ spatialCapabilitiesListeners.compute(
+ listener,
+ { _, _ ->
+ runtime.addSpatialCapabilitiesChangedListener(callbackExecutor, rtListener)
+ rtListener
+ },
+ )
+ }
+
+ /**
+ * Releases the given [Consumer] from receiving updates when the Session's [SpatialCapabilities]
+ * change.
+ */
+ @Suppress("PairedRegistration") // The corresponding remove method does not accept an Executor
+ public fun removeSpatialCapabilitiesChangedListener(
+ listener: Consumer<SpatialCapabilities>
+ ): Unit {
+ spatialCapabilitiesListeners.computeIfPresent(
+ listener,
+ { _, rtListener ->
+ runtime.removeSpatialCapabilitiesChangedListener(rtListener)
+ null
+ },
+ )
+ }
+
+ /**
+ * If the primary Activity for this Session has focus, causes it to be placed in FullSpace Mode.
+ * Otherwise, this call does nothing.
+ */
+ public fun requestFullSpaceMode(): Unit = runtime.requestFullSpaceMode()
+
+ /**
+ * If the primary Activity for this Session has focus, causes it to be placed in HomeSpace Mode.
+ * Otherwise, this call does nothing.
+ */
+ public fun requestHomeSpaceMode(): Unit = runtime.requestHomeSpaceMode()
+
+ /**
+ * Public factory function for a [GltfModel], where the glTF is asynchronously loaded.
+ *
+ * This method must be called from the main thread.
+ * https://developer.android.com/guide/components/processes-and-threads
+ *
+ * Currently, only URLs and relative paths from the android_assets/ directory are supported.
+ * Currently, only binary glTF (.glb) files are supported.
+ *
+ * @param name The URL or asset-relative path of a binary glTF (.glb) model to be loaded
+ * @return a ListenableFuture<GltfModel>. Listeners will be called on the main thread if
+ * Runnable::run is supplied.
+ */
+ @MainThread
+ public fun createGltfResourceAsync(name: String): ListenableFuture<GltfModel> {
+ return GltfModel.createAsync(runtime, name)
+ }
+
+ /**
+ * Public factory function for an EXRImage, where the EXR is loaded from a local file.
+ *
+ * @param name The path for an EXR image to be loaded
+ * @return an EXRImage instance.
+ */
+ public fun createExrImageResource(name: String): ExrImage = ExrImage.create(runtime, name)
+
+ /**
+ * Public factory function for a [GltfModelEntity].
+ *
+ * This method must be called from the main thread.
+ * https://developer.android.com/guide/components/processes-and-threads
+ *
+ * @param model The [GltfModel] this Entity is referencing.
+ * @param pose The initial pose of the entity.
+ * @return a GltfModelEntity instance
+ */
+ // TODO: b/341372472 - Rename createGltfEntity to createGltfModelEntity
+ @JvmOverloads
+ @MainThread
+ public fun createGltfEntity(model: GltfModel, pose: Pose = Pose.Identity): GltfModelEntity =
+ GltfModelEntity.create(runtime, entityManager, model, pose)
+
+ /**
+ * Public factory function for a StereoSurfaceEntity.
+ *
+ * This method must be called from the main thread.
+ * https://developer.android.com/guide/components/processes-and-threads
+ *
+ * @param stereoMode Stereo mode for the surface.
+ * @param dimensions Dimensions for the surface.
+ * @param pose Pose of this entity relative to its parent, default value is Identity.
+ * @return a StereoSurfaceEntity instance
+ */
+ @MainThread
+ @JvmOverloads
+ public fun createStereoSurfaceEntity(
+ @StereoSurfaceEntity.StereoModeValue
+ stereoMode: Int = StereoSurfaceEntity.StereoMode.SIDE_BY_SIDE,
+ dimensions: Dimensions = Dimensions(1.0f, 1.0f, 1.0f),
+ pose: Pose = Pose.Identity,
+ ): StereoSurfaceEntity {
+ return StereoSurfaceEntity.create(runtime, entityManager, stereoMode, dimensions, pose)
+ }
+
+ // TODO(b/352629832): Update surfaceDimensionsPx to be a PixelDimensions
+ /**
+ * Public factory function for a spatialized PanelEntity.
+ *
+ * @param view View to embed in this panel entity.
+ * @param surfaceDimensionsPx Dimensions for the underlying surface for the given view.
+ * @param dimensions Dimensions for the panel in meters.
+ * @param name Name of the panel.
+ * @param pose Pose of this entity relative to its parent, default value is Identity.
+ * @return a PanelEntity instance.
+ */
+ @JvmOverloads
+ public fun createPanelEntity(
+ view: View,
+ surfaceDimensionsPx: Dimensions,
+ dimensions: Dimensions,
+ name: String,
+ pose: Pose = Pose.Identity,
+ ): PanelEntity =
+ PanelEntity.create(
+ runtime,
+ entityManager,
+ view,
+ surfaceDimensionsPx,
+ dimensions,
+ name,
+ activity,
+ pose,
+ )
+
+ /** Helper function to query if given activity can be a host to ActivityPanel. */
+ @Deprecated(
+ message = "Use getSpatialCapabilities.hasCapabilility(SPATIAL_CAPABILITY_EMBED_ACTIVITY)"
+ )
+ public fun canEmbedActivityPanel(@Suppress("UNUSED_PARAMETER") activity: Activity): Boolean =
+ getSpatialCapabilities()
+ .hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY)
+
+ // TODO(b/352629832): Update windowBoundsPx to be a PixelDimensions
+ /**
+ * Public factory function for a spatial ActivityPanelEntity.
+ *
+ * @param windowBoundsPx Bounds for the panel window in pixels.
+ * @param name Name of the panel.
+ * @param pose Pose of this entity relative to its parent, default value is Identity.
+ * @return an ActivityPanelEntity instance.
+ */
+ @JvmOverloads
+ public fun createActivityPanelEntity(
+ windowBoundsPx: Rect,
+ name: String,
+ pose: Pose = Pose.Identity,
+ ): ActivityPanelEntity =
+ ActivityPanelEntity.create(
+ runtime,
+ entityManager,
+ PixelDimensions(windowBoundsPx.width(), windowBoundsPx.height()),
+ name,
+ activity,
+ pose,
+ )
+
+ /**
+ * Public factory function for an AnchorEntity which searches for a location to create an Anchor
+ * among the tracked planes available to the perception system.
+ *
+ * Note that this function will fail if the application has not been granted the
+ * "android.permission.SCENE_UNDERSTANDING" permission. Consider using PermissionHelper to help
+ * request permission from the User.
+ *
+ * @param bounds Bounds for this AnchorEntity.
+ * @param planeType Orientation of plane to which this Anchor should attach.
+ * @param planeSemantic Semantics of the plane to which this Anchor should attach.
+ * @param timeout The amount of time as a [Duration] to search for the a suitable plane to
+ * attach to. If a plane is not found within the timeout, the returned AnchorEntity state will
+ * be set to AnchorEntity.State.TIMEDOUT. It may take longer than the timeout period before
+ * the anchor state is updated. If the timeout duration is zero it will search for the anchor
+ * indefinitely.
+ */
+ @JvmOverloads
+ public fun createAnchorEntity(
+ bounds: Dimensions,
+ planeType: @PlaneTypeValue Int,
+ planeSemantic: @PlaneSemanticValue Int,
+ timeout: Duration = Duration.ZERO,
+ ): AnchorEntity {
+ return AnchorEntity.create(
+ runtime,
+ entityManager,
+ bounds,
+ planeType,
+ planeSemantic,
+ timeout
+ )
+ }
+
+ /**
+ * Public factory function for an AnchorEntity which uses an Anchor from ARCore for XR.
+ *
+ * @param anchor The PerceptionAnchor to use for this AnchorEntity.
+ */
+ public fun createAnchorEntity(anchor: Anchor): AnchorEntity {
+ return AnchorEntity.create(runtime, entityManager, anchor)
+ }
+
+ /**
+ * Unpersists an anchor. It will clean up the data in the storage that is required to retrieve
+ * the anchor.
+ *
+ * @param uuid UUID of the anchor to unpersist.
+ */
+ public fun unpersistAnchor(uuid: UUID): Boolean {
+ return runtime.unpersistAnchor(uuid)
+ }
+
+ /**
+ * Public factory function for creating a content-less entity. This entity is used as a
+ * connection point for attaching children entities and managing them (i.e. setPose()) as a
+ * group.
+ *
+ * @param name Name of the entity.
+ * @param pose Initial pose of the entity.
+ */
+ @JvmOverloads
+ public fun createEntity(name: String, pose: Pose = Pose.Identity): Entity =
+ ContentlessEntity.create(runtime, entityManager, name, pose)
+
+ /**
+ * Public factory function for a persisted AnchorEntity using UUID. Note that the system keeps a
+ * limited number of scenes and anchors. If the anchor is pruned due to the system limitation,
+ * the creation will fail. It will return null if there is a failure.
+ *
+ * @param uuid The UUID of the persisted anchor to recreate.
+ * @return a persisted AnchorEntity instance.
+ */
+ public fun createPersistedAnchorEntity(uuid: UUID): AnchorEntity {
+ return AnchorEntity.create(runtime, entityManager, uuid)
+ }
+
+ /**
+ * Public factory for creating an [InteractableComponent]. It enables access to raw input
+ * events.
+ *
+ * @param executor Executor for invoking [InputEventListener].
+ * @param inputEventListener [InputEventListener] that accepts [InputEvent]s.
+ * @return [InteractableComponent] instance.
+ */
+ @Suppress("ExecutorRegistration")
+ public fun createInteractableComponent(
+ executor: Executor,
+ inputEventListener: InputEventListener,
+ ): InteractableComponent =
+ InteractableComponent.create(runtime, entityManager, executor, inputEventListener)
+
+ /**
+ * Public factory function for creating a MovableComponent. This component can be attached to a
+ * single instance of any non-Anchor Entity.
+ *
+ * When attached, this Component will enable the user to translate the Entity by pointing and
+ * dragging on it.
+ *
+ * @param systemMovable A [Boolean] which causes the system to automatically apply transform
+ * updates to the entity in response to user interaction.
+ * @param scaleInZ A [Boolean] which tells the system to update the scale of the Entity as the
+ * user moves it closer and further away. This is mostly useful for Panel auto-rescaling with
+ * Distance
+ * @param anchorPlacement A Set containing different [AnchorPlacement] for how to anchor the
+ * [Entity] movable component. If this is not empty the movement semantics will be slightly
+ * different from the system as it will add the ability to anchor to nearby planes.
+ * @param shouldDisposeParentAnchor A [Boolean], which if set to true, when an entity is moved
+ * off of an [AnchorEntity] that was created by the underlying [MovableComponent], and the
+ * [AnchorEntity] has no other children, the AnchorEntity will be disposed, and the underlying
+ * Anchor will be detached.
+ * @return [MovableComponent] instance.
+ */
+ @JvmOverloads
+ public fun createMovableComponent(
+ systemMovable: Boolean = true,
+ scaleInZ: Boolean = true,
+ anchorPlacement: Set<AnchorPlacement> = emptySet(),
+ shouldDisposeParentAnchor: Boolean = true,
+ ): MovableComponent =
+ MovableComponent.create(
+ runtime = runtime,
+ entityManager = entityManager,
+ systemMovable = systemMovable,
+ scaleInZ = scaleInZ,
+ anchorPlacement = anchorPlacement,
+ shouldDisposeParentAnchor = shouldDisposeParentAnchor,
+ )
+
+ /**
+ * Public factory function for creating a ResizableComponent. This component can be attached to
+ * a single instance of any non-Anchor Entity.
+ *
+ * When attached, this Component will enable the user to resize the Entity by dragging along the
+ * boundaries of the interaction highlight.
+ *
+ * @param minimumSize A lower bound for the User's resize actions, in meters. This value is used
+ * to set constraints on how small the user can resize the bounding box of the entity down to.
+ * The size of the content inside that bounding box is fully controlled by the application.
+ * The default value for this param is 0 meters.
+ * @param maximumSize An upper bound for the User's resize actions, in meters. This value is
+ * used to set constraints on how large the user can resize the bounding box of the entity up
+ * to. The size of the content inside that bounding box is fully controlled by the
+ * application. The default value for this param is 10 meters.
+ * @return [ResizableComponent] instance.
+ */
+ @JvmOverloads
+ public fun createResizableComponent(
+ minimumSize: Dimensions = ResizableComponent.kMinimumSize,
+ maximumSize: Dimensions = ResizableComponent.kMaximumSize,
+ ): ResizableComponent = ResizableComponent.create(runtime, minimumSize, maximumSize)
+
+ /**
+ * Sets the full space mode flag to the given [android.os.Bundle].
+ *
+ * The [android.os.Bundle] then could be used to launch an [android.app.Activity] with
+ * requesting to enter full space mode through [android.app.Activity.startActivity]. If there's
+ * a bundle used for customizing how the [android.app.Activity] should be started by
+ * [android.app.ActivityOptions.toBundle] or [androidx.core.app.ActivityOptionsCompat.toBundle],
+ * it's suggested to use the bundle to call this method.
+ *
+ * The flag will be ignored when no [android.content.Intent.FLAG_ACTIVITY_NEW_TASK] is set in
+ * the bundle, or it is not started from a focused Activity context.
+ *
+ * This flag is also ignored when the [android.window.PROPERTY_XR_ACTIVITY_START_MODE] property
+ * is set to a value other than [XR_ACTIVITY_START_MODE_UNDEFINED] in the AndroidManifest.xml
+ * file for the activity being launched.
+ *
+ * @param bundle the input bundle to set with the full space mode flag.
+ * @return the input bundle with the full space mode flag set.
+ */
+ public fun setFullSpaceMode(bundle: Bundle): Bundle = runtime.setFullSpaceMode(bundle)
+
+ /**
+ * Sets the inherit full space mode environvment flag to the given [android.os.Bundle].
+ *
+ * The [android.os.Bundle] then could be used to launch an [android.app.Activity] with
+ * requesting to enter full space mode while inherit the existing environment through
+ * [android.app.Activity.startActivity]. If there's a bundle used for customizing how the
+ * [android.app.Activity] should be started by [android.app.ActivityOptions.toBundle] or
+ * [androidx.core.app.ActivityOptionsCompat.toBundle], it's suggested to use the bundle to call
+ * this method.
+ *
+ * When launched, the activity will be in full space mode and also inherits the environment from
+ * the launching activity. If the inherited environment needs to be animated, the launching
+ * activity has to continue updating the environment even after the activity is put into the
+ * stopped state.
+ *
+ * The flag will be ignored when no [android.content.Intent.FLAG_ACTIVITY_NEW_TASK] is set in
+ * the intent, or it is not started from a focused Activity context.
+ *
+ * The flag will also be ignored when there is no environment to inherit or the activity has its
+ * own environment set already.
+ *
+ * This flag is ignored too when the [android.window.PROPERTY_XR_ACTIVITY_START_MODE] property
+ * is set to a value other than [XR_ACTIVITY_START_MODE_UNDEFINED] in the AndroidManifest.xml
+ * file for the activity being launched.
+ *
+ * For security reasons, Z testing for the new activity is disabled, and the activity is always
+ * drawn on top of the inherited environment. Because Z testing is disabled, the activity should
+ * not spatialize itself, and should not curve its panel too much either.
+ *
+ * @param bundle the input bundle to set with the inherit full space mode environment flag.
+ * @return the input bundle with the inherit full space mode flag set.
+ */
+ public fun setFullSpaceModeWithEnvironmentInherited(bundle: Bundle): Bundle =
+ runtime.setFullSpaceModeWithEnvironmentInherited(bundle)
+
+ /**
+ * Sets a preferred main panel aspect ratio for home space mode.
+ *
+ * The ratio is only applied to the activity. If the activity launches another activity in the
+ * same task, the ratio is not applied to the new activity. Also, while the activity is in full
+ * space mode, the preference is temporarily removed.
+ *
+ * @param activity the activity to set the preference.
+ * @param preferredRatio the aspect ratio determined by taking the panel's width over its
+ * height. A value <= 0.0f means there are no preferences.
+ */
+ public fun setPreferredAspectRatio(activity: Activity, preferredRatio: Float): Unit =
+ runtime.setPreferredAspectRatio(activity, preferredRatio)
+
+ /**
+ * Returns all [Entity]s of the given type or its subtypes.
+ *
+ * @param type the type of [Entity] to return.
+ * @return a list of all [Entity]s of the given type.
+ */
+ public fun <T : Entity> getEntitiesOfType(type: Class<out T>): List<T> =
+ entityManager.getEntitiesOfType(type)
+
+ internal fun getEntityForRtEntity(entity: RtEntity): Entity? {
+ return entityManager.getEntityForRtEntity(entity)
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SoundFieldAttributes.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SoundFieldAttributes.kt
new file mode 100644
index 0000000..3a631ad
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SoundFieldAttributes.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import android.util.Log
+import androidx.annotation.RestrictTo
+
+/** Configures ambisonics sound sources. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SoundFieldAttributes(@SpatializerConstants.AmbisonicsOrder public val order: Int) {
+
+ internal val rtSoundFieldAttributes: JxrPlatformAdapter.SoundFieldAttributes
+
+ init {
+ val rtOrder =
+ when (order) {
+ SpatializerConstants.AMBISONICS_ORDER_FIRST_ORDER ->
+ JxrPlatformAdapter.SpatializerConstants.AMBISONICS_ORDER_FIRST_ORDER
+ SpatializerConstants.AMBISONICS_ORDER_SECOND_ORDER ->
+ JxrPlatformAdapter.SpatializerConstants.AMBISONICS_ORDER_SECOND_ORDER
+ SpatializerConstants.AMBISONICS_ORDER_THIRD_ORDER ->
+ JxrPlatformAdapter.SpatializerConstants.AMBISONICS_ORDER_THIRD_ORDER
+ else -> {
+ Log.e(TAG, "Unknown ambisonics order.")
+ order
+ }
+ }
+
+ rtSoundFieldAttributes = JxrPlatformAdapter.SoundFieldAttributes(rtOrder)
+ }
+
+ private companion object {
+ const val TAG = "SoundFieldAttributes"
+ }
+}
+
+internal fun JxrPlatformAdapter.SoundFieldAttributes.toSoundFieldAttributes():
+ SoundFieldAttributes {
+ return SoundFieldAttributes(this.ambisonicsOrder.ambisonicsOrderToJxr())
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatialAudioTrack.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatialAudioTrack.kt
new file mode 100644
index 0000000..6ce3da5
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatialAudioTrack.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import android.media.AudioTrack
+import androidx.annotation.RestrictTo
+
+@Suppress("ClassShouldBeObject")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SpatialAudioTrack {
+
+ public companion object {
+ /**
+ * Gets the [SourceType] of the provided [AudioTrack]. This value is implicitly set
+ * depending one which type of attributes was used to configure the builder. Will return
+ * [SpatializerExtensions.NOT_SPATIALIZED] for tracks that didn't use spatial audio
+ * attributes.
+ *
+ * @param session The current SceneCore [Session] instance.
+ * @param track The [AudioTrack] from which to get the [SpatializerConstants.SourceType].
+ * @return The [SpatializerExtensions.SourceType] of the provided track.
+ */
+ @JvmStatic
+ @SpatializerConstants.SourceType
+ public fun getSpatialSourceType(session: Session, track: AudioTrack): Int {
+ return session.runtime.audioTrackExtensionsWrapper
+ .getSpatialSourceType(track)
+ .sourceTypeToJxr()
+ }
+
+ /**
+ * Gets the [PointSourceAttributes] of the provided [AudioTrack].
+ *
+ * @param session The current SceneCore [Session] instance.
+ * @param track The [AudioTrack] from which to get the [PointSourceAttributes].
+ * @return The [PointSourceAttributes] of the provided track, null if not set.
+ */
+ @JvmStatic
+ public fun getPointSourceAttributes(
+ session: Session,
+ track: AudioTrack,
+ ): PointSourceAttributes? {
+ val rtAttributes =
+ session.runtime.audioTrackExtensionsWrapper.getPointSourceAttributes(track)
+ return rtAttributes?.toPointSourceAttributes(session)
+ }
+
+ /**
+ * Gets the [SoundFieldAttributes] of the provided [AudioTrack].
+ *
+ * @param session The current SceneCore [Session] instance.
+ * @param track The [AudioTrack] from which to get the [SoundFieldAttributes].
+ * @return The [SoundFieldAttributes] of the provided track, null if not set.
+ */
+ @JvmStatic
+ public fun getSoundFieldAttributes(
+ session: Session,
+ track: AudioTrack
+ ): SoundFieldAttributes? {
+ val rtAttributes =
+ session.runtime.audioTrackExtensionsWrapper.getSoundFieldAttributes(track)
+ return rtAttributes?.toSoundFieldAttributes()
+ }
+ }
+}
+
+/** Provides spatial audio extensions on the platform [AudioTrack.Builder] class. */
+@Suppress("ClassShouldBeObject", "MissingBuildMethod", "TopLevelBuilder")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SpatialAudioTrackBuilder private constructor() {
+
+ public companion object {
+ /**
+ * Sets the [PointSourceAttributes] on the provided [AudioTrack.Builder].
+ *
+ * @param session The current SceneCore [Session] instance.
+ * @param builder The Builder on which to set the attributes.
+ * @param attributes The source attributes to be set.
+ * @return The same [AudioTrack.Builder] instance provided.
+ */
+ @Suppress("SetterReturnsThis")
+ @JvmStatic
+ public fun setPointSourceAttributes(
+ session: Session,
+ builder: AudioTrack.Builder,
+ attributes: PointSourceAttributes,
+ ): AudioTrack.Builder {
+
+ return session.runtime.audioTrackExtensionsWrapper.setPointSourceAttributes(
+ builder,
+ attributes.rtPointSourceAttributes,
+ )
+ }
+
+ /**
+ * Sets the [SoundFieldAttributes] on the provided [AudioTrack.Builder].
+ *
+ * @param session The current SceneCore [Session] instance.
+ * @param builder The Builder on which to set the attributes.
+ * @param attributes The source attributes to be set.
+ * @return The same [AudioTrack.Builder] instance provided.
+ */
+ @Suppress("SetterReturnsThis")
+ @JvmStatic
+ public fun setSoundFieldAttributes(
+ session: Session,
+ builder: AudioTrack.Builder,
+ attributes: SoundFieldAttributes,
+ ): AudioTrack.Builder {
+ return session.runtime.audioTrackExtensionsWrapper.setSoundFieldAttributes(
+ builder,
+ attributes.rtSoundFieldAttributes,
+ )
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatialCapabilities.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatialCapabilities.kt
new file mode 100644
index 0000000..642e41f
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatialCapabilities.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+
+/** Representation of the spatial capabilities of the current session. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SpatialCapabilities(@SpatialCapability private val capabilities: Int) {
+
+ public companion object {
+ /** The activity can spatialize itself by e.g. adding a spatial panel. */
+ public const val SPATIAL_CAPABILITY_UI: Int = 1 shl 0
+
+ /** The activity can create 3D contents. */
+ public const val SPATIAL_CAPABILITY_3D_CONTENT: Int = 1 shl 1
+
+ /** The activity can enable or disable passthrough. */
+ public const val SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL: Int = 1 shl 2
+
+ /** The activity can set its own environment. */
+ public const val SPATIAL_CAPABILITY_APP_ENVIRONMENT: Int = 1 shl 3
+
+ /** The activity can use spatial audio. */
+ public const val SPATIAL_CAPABILITY_SPATIAL_AUDIO: Int = 1 shl 4
+
+ /** The activity can spatially embed another activity. */
+ public const val SPATIAL_CAPABILITY_EMBED_ACTIVITY: Int = 1 shl 5
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(
+ flag = true,
+ value =
+ [
+ SPATIAL_CAPABILITY_UI,
+ SPATIAL_CAPABILITY_3D_CONTENT,
+ SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL,
+ SPATIAL_CAPABILITY_APP_ENVIRONMENT,
+ SPATIAL_CAPABILITY_SPATIAL_AUDIO,
+ SPATIAL_CAPABILITY_EMBED_ACTIVITY,
+ ],
+ )
+ internal annotation class SpatialCapability
+
+ public fun hasCapability(@SpatialCapability capability: Int): Boolean =
+ (capabilities and capability) != 0
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is SpatialCapabilities) return false
+
+ if (capabilities != other.capabilities) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return capabilities
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatialEnvironment.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatialEnvironment.kt
new file mode 100644
index 0000000..b6fd2d7
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatialEnvironment.kt
@@ -0,0 +1,454 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+@file:Suppress("JVM_FIELD", "Deprecation")
+
+package androidx.xr.scenecore
+
+import android.util.Log
+import androidx.annotation.RestrictTo
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatialEnvironment.SetPassthroughOpacityPreferenceResult as RtSetPassthroughOpacityPreferenceResult
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatialEnvironment.SetSpatialEnvironmentPreferenceResult as RtSetSpatialEnvironmentPreferenceResult
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatialEnvironment.SpatialEnvironmentPreference as RtSpatialEnvironmentPreference
+import com.google.errorprone.annotations.CanIgnoreReturnValue
+import java.util.concurrent.atomic.AtomicReference
+import java.util.function.Consumer
+
+// TODO: Support a nullable Runtime in the ctor. This should allow the Runtime to "show up" later.
+
+/**
+ * The SpatialEnvironment is used to manage the XR background and passthrough. There is a single
+ * instance of this class managed by each SceneCore Session (which is bound to an [Activity].)
+ *
+ * The SpatialEnvironment is a composite of a stand-alone skybox, and glTF-specified geometry. A
+ * single skybox and a single glTF can be set at the same time. Applications are encouraged to
+ * supply glTFs for ground and horizon visibility.
+ *
+ * The XR background can be set to display one of three configurations:
+ * 1) A combination of a skybox and glTF geometry.
+ * 2) A Passthrough surface, where the XR background is a live feed from the device's outward facing
+ * cameras. At full opacity, this surface completely occludes the skybox and geometry.
+ * 3) A mixed configuration where the passthrough surface is not at full opacity nor is it at zero
+ * opacity. The passthrough surface becomes semi-transparent and alpha blends with the skybox and
+ * geometry behind it.
+ *
+ * Note that methods in this class do not necessarily take effect immediately. Rather, they set a
+ * preference that will be applied when the device enters a state where the XR background can be
+ * changed.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SpatialEnvironment(runtime: JxrPlatformAdapter) {
+
+ private val TAG = "SpatialEnvironment"
+
+ private val rtEnvironment: JxrPlatformAdapter.SpatialEnvironment = runtime.spatialEnvironment
+
+ // These two fields are only used by the deprecated setSkybox() and setGeometry() methods.
+ // TODO: b/370015943 - Remove after clients migrate to the SpatialEnvironmentPreference APIs.
+ private val deprecatedSkybox = AtomicReference<ExrImage?>()
+ private val deprecatedGeometry = AtomicReference<GltfModel?>()
+
+ // TODO: b/370484799 - Remove this once all clients move away from it.
+ /** Describes if/how the User can view their real-world physical environment. */
+ @Deprecated(message = "Use isSpatialEnvironmentPreferenceActive() instead.")
+ public class PassthroughMode internal constructor(public val value: Int) {
+ public companion object {
+ /** The state at startup. The application cannot set this state. No longer used. */
+ @JvmField public val Uninitialized: PassthroughMode = PassthroughMode(0)
+ /**
+ * The user's passthrough is not composed into their view. Environment skyboxes and
+ * geometry are only visible in this state.
+ */
+ @JvmField public val Disabled: PassthroughMode = PassthroughMode(1)
+ /** The user's passthrough is visible at full or partial opacity. */
+ @JvmField public val Enabled: PassthroughMode = PassthroughMode(2)
+ }
+ }
+
+ /**
+ * Represents the preferred spatial environment for the application.
+ *
+ * @param skybox The preferred skybox for the environment based on a pre-loaded EXR Image. If
+ * null, it will be all black.
+ * @param geometry The preferred geometry for the environment based on a pre-loaded [GltfModel].
+ * If null, there will be no geometry.
+ */
+ public class SpatialEnvironmentPreference(
+ public val skybox: ExrImage?,
+ public val geometry: GltfModel?,
+ ) {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as SpatialEnvironmentPreference
+
+ if (skybox != other.skybox) return false
+ if (geometry != other.geometry) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = skybox?.hashCode() ?: 0
+ result = 31 * result + (geometry?.hashCode() ?: 0)
+ return result
+ }
+ }
+
+ // TODO: b/370484799 - Remove this once all clients migrate to the opacity Preference APIs.
+ /**
+ * Sets the preference for passthrough.
+ *
+ * Calling with DISABLED is equivalent to calling setPassthroughOpacityPreference(0.0f) and
+ * calling with ENABLED is equivalent to calling setPassthroughOpacityPreference(1.0f). Calling
+ * with UNINITIALIZED is ignored. See [setPassthroughOpacityPreference] for more details.
+ */
+ @Deprecated(message = "Use setPassthroughOpacityPreference instead.")
+ public fun setPassthrough(passthroughMode: PassthroughMode) {
+ when (passthroughMode) {
+ PassthroughMode.Uninitialized -> return // Do nothing. This isn't allowed.
+ PassthroughMode.Disabled -> setPassthroughOpacityPreference(0.0f)
+ PassthroughMode.Enabled -> setPassthroughOpacityPreference(1.0f)
+ }
+ }
+
+ // TODO: b/370484799 - Remove this once all clients migrate to the opacity Preference APIs.
+ /**
+ * Sets the preference for passthrough. This is equivalent to calling
+ * [setPassthroughOpacityPreference] with the given opacity value.
+ */
+ @Deprecated(message = "Use setPassthroughOpacityPreference instead.")
+ public fun setPassthroughOpacity(passthroughOpacity: Float) {
+ setPassthroughOpacityPreference(passthroughOpacity)
+ }
+
+ // TODO: b/370484799 - Remove this once all clients migrate to the opacity Preference APIs.
+ /** Gets the current preference for passthrough mode. */
+ @Deprecated(message = "Use getCurrentPassthroughOpacity instead.")
+ public fun getPassthroughMode(): PassthroughMode {
+ if (getCurrentPassthroughOpacity() > 0.0f) {
+ return PassthroughMode.Enabled
+ } else {
+ return PassthroughMode.Disabled
+ }
+ }
+
+ // TODO: b/370484799 - Remove this once all clients migrate to the opacity Preference APIs.
+ /**
+ * Gets the current passthrough opacity. This may be different than the passthrough opacity
+ * preference.
+ */
+ @Deprecated(message = "Use getCurrentPassthroughOpacity instead.")
+ public fun getPassthroughOpacity(): Float {
+ return getCurrentPassthroughOpacity()
+ }
+
+ /**
+ * Sets the application's preferred passthrough opacity between 0.0f and 1.0f. Upon
+ * construction, the default value is null, which means "no application preference".
+ *
+ * Setting the application preference through this method does not guarantee that the value will
+ * be immediately applied and visible to the user. The actual passthrough opacity value is
+ * controlled by the system in response to a combination of the application's preference and
+ * user actions outside the application. Generally, the application's preference will be shown
+ * to the user when the application has the
+ * [SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL] capability. The current value
+ * visible to the user can be observed by calling [getCurrentPassthroughOpacity] or by
+ * registering a listener with [addOnPassthroughOpacityChangedListener].
+ *
+ * @param passthroughOpacityPreference The application's passthrough opacity preference between
+ * 0.0f (disabled with no passthrough) and 1.0f (fully enabled passthrough hides the spatial
+ * environment). Values within 0.01f of 0.0 or 1.0 will be snapped to those values. Other
+ * values result in semi-transparent passthrough alpha blended with the spatial environment.
+ * Values outside [0.0f, 1.0f] are clamped. If null, the system will manage the passthrough
+ * opacity.
+ * @return The result of the call to set the passthrough opacity preference. If the preference
+ * was successfully set and applied, the result will be
+ * [SetPassthroughOpacityPreferenceChangeApplied]. If the preference was set, but it cannot be
+ * currently applied, the result will be [SetPassthroughOpacityPreferenceChangePending].
+ */
+ @CanIgnoreReturnValue
+ public fun setPassthroughOpacityPreference(
+ @SuppressWarnings("AutoBoxing") passthroughOpacityPreference: Float?
+ ): SetPassthroughOpacityPreferenceResult {
+ return rtEnvironment
+ .setPassthroughOpacityPreference(passthroughOpacityPreference)
+ .toSetPassthroughOpacityPreferenceResult()
+ }
+
+ /**
+ * Gets the current passthrough opacity value visible to the user.
+ *
+ * Unlike the application's opacity preference returned by [getPassthroughOpacityPreference],
+ * this value can be overwritten by the system, and is not directly under the application's
+ * control.
+ *
+ * @return The current passthrough opacity value between 0.0f and 1.0f. A value of 0.0f means no
+ * passthrough is shown, and a value of 1.0f means the passthrough completely obscures the
+ * spatial environment geometry and skybox.
+ */
+ public fun getCurrentPassthroughOpacity(): Float {
+ return rtEnvironment.currentPassthroughOpacity
+ }
+
+ /**
+ * Gets the current passthrough opacity preference set through
+ * [setPassthroughOpacityPreference]. Defaults to null if [setPassthroughOpacityPreference] has
+ * not been called.
+ *
+ * This value only reflects the application's preference and does not necessarily reflect what
+ * the system is currently showing the user. See [getCurrentPassthroughOpacity] to get the
+ * actual visible opacity value.
+ *
+ * @return The last passthrough opacity value between 0.0f and 1.0f requested through
+ * [setPassthroughOpacityPreference]. If null, no application preference is set and the
+ * passthrough opacity will be fully managed through the system.
+ */
+ @SuppressWarnings("AutoBoxing")
+ public fun getPassthroughOpacityPreference(): Float? {
+ return rtEnvironment.passthroughOpacityPreference
+ }
+
+ /**
+ * Notifies an application when the user visible passthrough state changes, such as when the
+ * application enters or exits passthrough or when the passthrough opacity changes.
+ *
+ * This [listener] will be called on the Application's main thread.
+ *
+ * @param listener The [Consumer<Float>] to be added to listen for passthrough opacity changes.
+ */
+ public fun addOnPassthroughOpacityChangedListener(listener: Consumer<Float>) {
+ rtEnvironment.addOnPassthroughOpacityChangedListener(listener)
+ }
+
+ /**
+ * Remove a listener previously added by [addOnPassthroughOpacityChangedListener].
+ *
+ * @param listener The previously-added [Consumer<Float>] listener to be removed.
+ */
+ public fun removeOnPassthroughOpacityChangedListener(listener: Consumer<Float>) {
+ rtEnvironment.removeOnPassthroughOpacityChangedListener(listener)
+ }
+
+ // TODO: b/370015943 - Remove this once all clients migrate to the SpatialEnvironment APIs.
+ /**
+ * Sets the preferred environmental skybox based on a pre-loaded EXR Image.
+ *
+ * Note that this method does not necessarily cause an immediate change, it only sets a
+ * preference. Once the device enters a state where the XR background can be changed, the
+ * preference will be applied.
+ *
+ * Setting the skybox to null will disable the skybox.
+ */
+ @Deprecated(message = "Use setSpatialEnvironmentPreference() instead.")
+ public fun setSkybox(exrImage: ExrImage?) {
+ Log.w(TAG, "setGeometry() is deprecated. Use setSpatialEnvironmentPreference() instead.")
+ deprecatedSkybox.updateAndGet {
+ setSpatialEnvironmentPreference(
+ SpatialEnvironmentPreference(exrImage, deprecatedGeometry.get())
+ )
+ exrImage
+ }
+ }
+
+ // TODO: b/370015943 - Remove this once all clients migrate to the SpatialEnvironment APIs.
+ /**
+ * Sets the preferred environmental geometry based on a pre-loaded [GltfModel].
+ *
+ * Note that this method does not necessarily cause an immediate change, it only sets a
+ * preference. Once the device enters a state where the XR background can be changed, the
+ * preference will be applied.
+ *
+ * Setting the geometry to null will disable the geometry.
+ */
+ @Deprecated(message = "Use setSpatialEnvironmentPreference() instead.")
+ public fun setGeometry(gltfModel: GltfModel?) {
+ Log.w(TAG, "setGeometry() is deprecated. Use setSpatialEnvironmentPreference() instead.")
+ deprecatedGeometry.updateAndGet {
+ setSpatialEnvironmentPreference(
+ SpatialEnvironmentPreference(deprecatedSkybox.get(), gltfModel)
+ )
+ gltfModel
+ }
+ }
+
+ /**
+ * Returns true if the environment set by [setSpatialEnvironmentPreference] is active.
+ *
+ * Spatial environment preference set through [setSpatialEnvironmentPreference] are shown when
+ * this is true, but passthrough or other objects in the scene could partially or totally
+ * occlude them. When this is false, the default system environment will be active instead.
+ *
+ * @return True if the environment set by [setSpatialEnvironmentPreference] is active.
+ */
+ public fun isSpatialEnvironmentPreferenceActive(): Boolean {
+ return rtEnvironment.isSpatialEnvironmentPreferenceActive
+ }
+
+ /**
+ * Gets the preferred spatial environment for the application.
+ *
+ * The returned value is always what was most recently supplied to
+ * [setSpatialEnvironmentPreference], or null if no preference has been set.
+ *
+ * See [isSpatialEnvironmentPreferenceActive] or the [addOnSpatialEnvironmentChangedListener]
+ * listeners to know when this preference becomes active.
+ *
+ * @return The most recent spatial environment preference supplied to
+ * [setSpatialEnvironmentPreference]. If null, the default system environment will be
+ * displayed instead.
+ */
+ public fun getSpatialEnvironmentPreference(): SpatialEnvironmentPreference? {
+ return rtEnvironment.spatialEnvironmentPreference?.toSpatialEnvironmentPreference()
+ }
+
+ /**
+ * Sets the preferred spatial environment for the application.
+ *
+ * Note that this method only sets a preference and does not cause an immediate change unless
+ * [isSpatialEnvironmentPreferenceActive] is already true. Once the device enters a state where
+ * the XR background can be changed and the
+ * [SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENTS] capability is available, the
+ * preferred spatial environment for the application will be automatically displayed.
+ *
+ * Setting the preference to null will disable the preferred spatial environment for the
+ * application, meaning the default system environment will be displayed instead.
+ *
+ * If the given [SpatialEnvironmentPreference] is not null, but all of its properties are null,
+ * then the spatial environment will consist of a black skybox and no geometry
+ * [isSpatialEnvironmentPreferenceActive] is true.
+ *
+ * Changes to the Environment state will be notified via listeners added with
+ * [addOnSpatialEnvironmentChangedListener].
+ *
+ * @param environmentPreference The preferred spatial environment for the application. If null,
+ * then there is no preference, and the default system environment will be displayed instead.
+ * @return The result of the call to set the spatial environment preference. If the preference
+ * was successfully set and applied, the result will be
+ * [SetSpatialEnvironmentPreferenceChangeApplied]. If the preference was set, but it cannot be
+ * currently applied, the result will be [SetSpatialEnvironmentPreferenceChangePending].
+ */
+ @CanIgnoreReturnValue
+ public fun setSpatialEnvironmentPreference(
+ environmentPreference: SpatialEnvironmentPreference?
+ ): SetSpatialEnvironmentPreferenceResult {
+ return rtEnvironment
+ .setSpatialEnvironmentPreference(
+ environmentPreference?.toRtSpatialEnvironmentPreference()
+ )
+ .toSetSpatialEnvironmentPreferenceResult()
+ }
+
+ // TODO: b/370957362 - Add overloads for the add...Listener methods to take in an executor
+ /**
+ * Notifies an application whether or not the preferred spatial environment for the application
+ * is active.
+ *
+ * The environment will try to transition to the application environment when a non-null
+ * preference is set through [setSpatialEnvironmentPreference] and the application has the
+ * [SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENTS] capability. The environment
+ * preferences will otherwise not be active.
+ *
+ * The listener consumes a boolean value that is true if the environment preference is active
+ * when the listener is notified.
+ *
+ * This listener will be invoked on the Application's main thread.
+ *
+ * @param listener The [Consumer<Boolean>] to be added to listen for spatial environment
+ * changes.
+ */
+ public fun addOnSpatialEnvironmentChangedListener(listener: Consumer<Boolean>) {
+ rtEnvironment.addOnSpatialEnvironmentChangedListener(listener)
+ }
+
+ /**
+ * Remove a listener previously added by [addOnSpatialEnvironmentChangedListener].
+ *
+ * @param listener The previously-added [Consumer<Boolean>] listener to be removed.
+ */
+ public fun removeOnSpatialEnvironmentChangedListener(listener: Consumer<Boolean>) {
+ rtEnvironment.removeOnSpatialEnvironmentChangedListener(listener)
+ }
+
+ /** Result values for calls to [setPassthroughOpacityPreference] */
+ public sealed class SetPassthroughOpacityPreferenceResult()
+
+ /** The call to [setPassthroughOpacityPreference] succeeded and should now be visible. */
+ public class SetPassthroughOpacityPreferenceChangeApplied :
+ SetPassthroughOpacityPreferenceResult()
+
+ /**
+ * The call to [setPassthroughOpacityPreference] successfully applied the preference, but it is
+ * not immediately visible due to requesting a state change while the activity does not have the
+ * [SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL] capability to control the app
+ * passthrough state. The preference was still set and will be applied when the capability is
+ * gained.
+ */
+ public class SetPassthroughOpacityPreferenceChangePending :
+ SetPassthroughOpacityPreferenceResult()
+
+ /** Result values for calls to SpatialEnvironment.setSpatialEnvironmentPreference */
+ public sealed class SetSpatialEnvironmentPreferenceResult()
+
+ /** The call to [setSpatialEnvironmentPreference] succeeded and should now be visible. */
+ public class SetSpatialEnvironmentPreferenceChangeApplied :
+ SetSpatialEnvironmentPreferenceResult()
+
+ /**
+ * The call to [setSpatialEnvironmentPreference] successfully applied the preference, but it is
+ * not immediately visible due to requesting a state change while the activity does not have the
+ * [SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENTS] capability to control the app
+ * environment state. The preference was still set and will be applied when the capability is
+ * gained.
+ */
+ public class SetSpatialEnvironmentPreferenceChangePending :
+ SetSpatialEnvironmentPreferenceResult()
+}
+
+internal fun SpatialEnvironment.SpatialEnvironmentPreference.toRtSpatialEnvironmentPreference():
+ RtSpatialEnvironmentPreference {
+ return RtSpatialEnvironmentPreference(skybox?.image, geometry?.model)
+}
+
+internal fun RtSpatialEnvironmentPreference.toSpatialEnvironmentPreference():
+ SpatialEnvironment.SpatialEnvironmentPreference {
+ return SpatialEnvironment.SpatialEnvironmentPreference(
+ skybox?.let { ExrImage(it) },
+ geometry?.let { GltfModel(it) },
+ )
+}
+
+internal fun RtSetSpatialEnvironmentPreferenceResult.toSetSpatialEnvironmentPreferenceResult():
+ SpatialEnvironment.SetSpatialEnvironmentPreferenceResult {
+ return when (this) {
+ RtSetSpatialEnvironmentPreferenceResult.CHANGE_APPLIED ->
+ SpatialEnvironment.SetSpatialEnvironmentPreferenceChangeApplied()
+ RtSetSpatialEnvironmentPreferenceResult.CHANGE_PENDING ->
+ SpatialEnvironment.SetSpatialEnvironmentPreferenceChangePending()
+ }
+}
+
+internal fun RtSetPassthroughOpacityPreferenceResult.toSetPassthroughOpacityPreferenceResult():
+ SpatialEnvironment.SetPassthroughOpacityPreferenceResult {
+ return when (this) {
+ RtSetPassthroughOpacityPreferenceResult.CHANGE_APPLIED ->
+ SpatialEnvironment.SetPassthroughOpacityPreferenceChangeApplied()
+ RtSetPassthroughOpacityPreferenceResult.CHANGE_PENDING ->
+ SpatialEnvironment.SetPassthroughOpacityPreferenceChangePending()
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatialMediaPlayer.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatialMediaPlayer.kt
new file mode 100644
index 0000000..66d3432
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatialMediaPlayer.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import android.media.MediaPlayer
+import androidx.annotation.RestrictTo
+
+@Suppress("ClassShouldBeObject")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SpatialMediaPlayer {
+
+ public companion object {
+ /**
+ * Sets a [PointSourceAttributes] on a [MediaPlayer] instance.
+ *
+ * Must be called before prepare(), not compatible with instances created by
+ * MediaPlayer#create(). Only the attributes from the most recent call to
+ * setPointSourceAttributes or [setSoundFieldAttributes] will apply.
+ *
+ * @param session The current SceneCore [Session] instance.
+ * @param mediaPlayer The [MediaPlayer] instance on which to set the attributes
+ * @param attributes The source attributes to be set.
+ */
+ @JvmStatic
+ public fun setPointSourceAttributes(
+ session: Session,
+ mediaPlayer: MediaPlayer,
+ attributes: PointSourceAttributes,
+ ) {
+ session.runtime.mediaPlayerExtensionsWrapper.setPointSourceAttributes(
+ mediaPlayer,
+ attributes.rtPointSourceAttributes,
+ )
+ }
+
+ /**
+ * Sets a [SoundFieldAttributes] on a [MediaPlayer] instance.
+ *
+ * Must be called before prepare(), not compatible with instances created by
+ * MediaPlayer#create(). Only the attributes from the most recent call to
+ * setSoundFieldAttributes or [setPointSourceAttributes] will apply.
+ *
+ * @param session The current SceneCore [Session] instance.
+ * @param mediaPlayer The [MediaPlayer] instance on which to set the attributes
+ * @param attributes The source attributes to be set.
+ */
+ @JvmStatic
+ public fun setSoundFieldAttributes(
+ session: Session,
+ mediaPlayer: MediaPlayer,
+ attributes: SoundFieldAttributes,
+ ) {
+ session.runtime.mediaPlayerExtensionsWrapper.setSoundFieldAttributes(
+ mediaPlayer,
+ attributes.rtSoundFieldAttributes,
+ )
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatialSoundPool.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatialSoundPool.kt
new file mode 100644
index 0000000..f0d4de4
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatialSoundPool.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+@file:Suppress("UNUSED_PARAMETER")
+
+package androidx.xr.scenecore
+
+import android.media.SoundPool
+import androidx.annotation.RestrictTo
+
+/** Provides spatial audio extensions on the framework [SoundPool] class. */
+@Suppress("ClassShouldBeObject")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SpatialSoundPool private constructor() {
+
+ public companion object {
+ /**
+ * Plays a spatialized sound effect emitted relative [Node] in the [PointSourceAttributes].
+ *
+ * @param session The current SceneCore [Session] instance.
+ * @param soundPool The [SoundPool] to use to the play the sound.
+ * @param soundID a soundId returned by the load() function.
+ * @param attributes attributes to specify sound source. [PointSourceAttributes]
+ * @param volume value (range = 0.0 to 1.0)
+ * @param priority stream priority (0 = lowest priority)
+ * @param loop loop mode (0 = no loop, -1 = loop forever, N = loop N times)
+ * @param rate playback rate (1.0 = normal playback, range 0.5 to 2.0)
+ * @return non-zero streamID if successful, zero if failed
+ */
+ @JvmStatic
+ public fun play(
+ session: Session,
+ soundPool: SoundPool,
+ soundID: Int,
+ attributes: PointSourceAttributes,
+ volume: Float,
+ priority: Int,
+ loop: Int,
+ rate: Float,
+ ): Int {
+
+ return session.runtime.soundPoolExtensionsWrapper.play(
+ soundPool,
+ soundID,
+ attributes.rtPointSourceAttributes,
+ volume,
+ priority,
+ loop,
+ rate,
+ )
+ }
+
+ /**
+ * Plays a spatialized sound effect as a sound field.
+ *
+ * @param session The current SceneCore [Session] instance.
+ * @param soundPool The [SoundPool] to use to the play the sound.
+ * @param soundID a soundId returned by the load() function.
+ * @param attributes attributes to specify sound source. [SoundFieldAttributes]
+ * @param volume value (range = 0.0 to 1.0)
+ * @param priority stream priority (0 = lowest priority)
+ * @param loop loop mode (0 = no loop, -1 = loop forever, N = loop N times)
+ * @param rate playback rate (1.0 = normal playback, range 0.5 to 2.0)
+ * @return non-zero streamID if successful, zero if failed
+ */
+ @JvmStatic
+ public fun play(
+ session: Session,
+ soundPool: SoundPool,
+ soundID: Int,
+ attributes: SoundFieldAttributes,
+ volume: Float,
+ priority: Int,
+ loop: Int,
+ rate: Float,
+ ): Int {
+
+ return session.runtime.soundPoolExtensionsWrapper.play(
+ soundPool,
+ soundID,
+ attributes.rtSoundFieldAttributes,
+ volume,
+ priority,
+ loop,
+ rate,
+ )
+ }
+
+ /**
+ * @param session The current SceneCore [Session] instance.
+ * @param soundPool The [SoundPool] to use to get its SourceType.
+ * @param streamId a streamID returned by the play(), playAsPointSource(), or
+ * playAsSoundField().
+ * @return The [SpatializerConstants.SourceType] for the given streamID.
+ */
+ @JvmStatic
+ @SpatializerConstants.SourceType
+ public fun getSpatialSourceType(
+ session: Session,
+ soundPool: SoundPool,
+ streamId: Int
+ ): Int {
+ return session.runtime.soundPoolExtensionsWrapper
+ .getSpatialSourceType(soundPool, streamId)
+ .sourceTypeToJxr()
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatialUser.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatialUser.kt
new file mode 100644
index 0000000..414cae6
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatialUser.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import androidx.annotation.RestrictTo
+
+/**
+ * The User object is used to retrieve information about the user. This includes the Head and The
+ * CameraViews.
+ *
+ * @param runtime The JxrPlatformAdapter for the Session.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SpatialUser(private val runtime: JxrPlatformAdapter) {
+ private var cachedLeftCamera: CameraView? = null
+ private var cachedRightCamera: CameraView? = null
+ private var cachedHead: Head? = null
+
+ private var leftCamera: CameraView? = null
+ get() {
+ if (cachedLeftCamera == null) {
+ cachedLeftCamera = CameraView.createLeft(runtime)
+ }
+ return cachedLeftCamera
+ }
+
+ private var rightCamera: CameraView? = null
+ get() {
+ if (cachedRightCamera == null) {
+ cachedRightCamera = CameraView.createRight(runtime)
+ }
+ return cachedRightCamera
+ }
+
+ internal companion object {
+ /** Factory function for creating [SpatialUser] instance. */
+ internal fun create(runtime: JxrPlatformAdapter): SpatialUser {
+ return SpatialUser(runtime)
+ }
+ }
+
+ /** Returns a Head for the SpatialUser or null if it is not yet available. */
+ public var head: Head? = null
+ get() {
+ if (cachedHead == null) {
+ cachedHead = Head.create(runtime)
+ }
+ return cachedHead
+ }
+
+ /**
+ * Returns a list of CameraViews that the user is using. The length of the list is dependent on
+ * the device type. The list will be empty if the cameras are not yet available.
+ */
+ public fun getCameraViews(): List<CameraView> {
+ return listOfNotNull<CameraView>(leftCamera, rightCamera)
+ }
+
+ /** Returns a CameraView for the specified CameraType or null if it is not available. */
+ public fun getCameraView(cameraType: CameraView.CameraType): CameraView? {
+ if (cameraType == CameraView.CameraType.LEFT_EYE) {
+ return leftCamera
+ } else if (cameraType == CameraView.CameraType.RIGHT_EYE) {
+ return rightCamera
+ }
+ return null
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatializerConstants.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatializerConstants.kt
new file mode 100644
index 0000000..35cda9c
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SpatializerConstants.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+
+/** Constants for spatialized audio. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface SpatializerConstants {
+
+ public companion object {
+ /** Specifies spatial rendering using First Order Ambisonics */
+ public const val AMBISONICS_ORDER_FIRST_ORDER: Int = 0
+ /** Specifies spatial rendering using Second Order Ambisonics */
+ public const val AMBISONICS_ORDER_SECOND_ORDER: Int = 1
+ /** Specifies spatial rendering using Third Order Ambisonics */
+ public const val AMBISONICS_ORDER_THIRD_ORDER: Int = 2
+
+ /** The sound source has not been spatialized with the Spatial Audio SDK. */
+ public const val SOURCE_TYPE_BYPASS: Int = 0
+ /** The sound source has been spatialized as a 3D point source. */
+ public const val SOURCE_TYPE_POINT_SOURCE: Int = 1
+ /** The sound source is an ambisonics sound field. */
+ public const val SOURCE_TYPE_SOUND_FIELD: Int = 2
+ }
+
+ /** Used to set the Ambisonics order of a [SoundFieldAttributes] */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(
+ value =
+ [
+ AMBISONICS_ORDER_FIRST_ORDER,
+ AMBISONICS_ORDER_SECOND_ORDER,
+ AMBISONICS_ORDER_THIRD_ORDER
+ ]
+ )
+ public annotation class AmbisonicsOrder
+
+ /** Represents the type of spatialization for an audio source. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(value = [SOURCE_TYPE_BYPASS, SOURCE_TYPE_POINT_SOURCE, SOURCE_TYPE_SOUND_FIELD])
+ public annotation class SourceType
+}
+
+/** Converts the [JxrPlatformAdapter] SourceType IntDef to the SceneCore API. */
[email protected]
+internal fun @receiver:JxrPlatformAdapter.SpatializerConstants.SourceType Int.sourceTypeToJxr():
+ Int {
+ return when (this) {
+ JxrPlatformAdapter.SpatializerConstants.SOURCE_TYPE_BYPASS ->
+ SpatializerConstants.SOURCE_TYPE_BYPASS
+ JxrPlatformAdapter.SpatializerConstants.SOURCE_TYPE_POINT_SOURCE ->
+ SpatializerConstants.SOURCE_TYPE_POINT_SOURCE
+ JxrPlatformAdapter.SpatializerConstants.SOURCE_TYPE_SOUND_FIELD ->
+ SpatializerConstants.SOURCE_TYPE_SOUND_FIELD
+ else -> {
+ // Unknown source type, returning bypass.
+ SpatializerConstants.SOURCE_TYPE_BYPASS
+ }
+ }
+}
+
+/** Converts the [JxrPlatformAdapter] SourceType IntDef to the SceneCore API. */
[email protected]
+internal fun @receiver:JxrPlatformAdapter.SpatializerConstants.AmbisonicsOrder
+Int.ambisonicsOrderToJxr(): Int {
+ return when (this) {
+ JxrPlatformAdapter.SpatializerConstants.AMBISONICS_ORDER_FIRST_ORDER ->
+ SpatializerConstants.AMBISONICS_ORDER_FIRST_ORDER
+ JxrPlatformAdapter.SpatializerConstants.AMBISONICS_ORDER_SECOND_ORDER ->
+ SpatializerConstants.AMBISONICS_ORDER_SECOND_ORDER
+ JxrPlatformAdapter.SpatializerConstants.AMBISONICS_ORDER_THIRD_ORDER ->
+ SpatializerConstants.AMBISONICS_ORDER_THIRD_ORDER
+ else -> {
+ // Unknown order, returning first order
+ SpatializerConstants.AMBISONICS_ORDER_FIRST_ORDER
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/Types.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/Types.kt
new file mode 100644
index 0000000..78fa4c6
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/Types.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+
+/**
+ * Dimensions of a 3D object.
+ *
+ * @param width Width.
+ * @param height Height.
+ * @param depth Depth.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public data class Dimensions(
+ public val width: Float = 0f,
+ public val height: Float = 0f,
+ public val depth: Float = 0f,
+) {
+ override fun toString(): String {
+ return super.toString() + ": w $width x h $height x d $depth"
+ }
+}
+
+/**
+ * Dimensions of a 2D surface in Pixels.
+ *
+ * @param width Integer Width.
+ * @param height Integer Height.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public data class PixelDimensions(public val width: Int = 0, public val height: Int = 0) {
+ override fun toString(): String {
+ return super.toString() + ": w $width x h $height"
+ }
+}
+
+/**
+ * The angles (in radians) representing the sides of the view frustum. These are not expected to
+ * change over the lifetime of the session but in rare cases may change due to updated camera
+ * settings.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public data class Fov(
+ public val angleLeft: Float,
+ public val angleRight: Float,
+ public val angleUp: Float,
+ public val angleDown: Float,
+)
+
+/** Type of plane based on orientation i.e. Horizontal or Vertical. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public object PlaneType {
+ public const val HORIZONTAL: Int = 0
+ public const val VERTICAL: Int = 1
+ public const val ANY: Int = 2
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@Retention(AnnotationRetention.SOURCE)
+@IntDef(PlaneType.HORIZONTAL, PlaneType.VERTICAL, PlaneType.ANY)
+@Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER)
+internal annotation class PlaneTypeValue
+
+/** Semantic plane types. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public object PlaneSemantic {
+ public const val WALL: Int = 0
+ public const val FLOOR: Int = 1
+ public const val CEILING: Int = 2
+ public const val TABLE: Int = 3
+ public const val ANY: Int = 4
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@Retention(AnnotationRetention.SOURCE)
+@IntDef(
+ PlaneSemantic.WALL,
+ PlaneSemantic.FLOOR,
+ PlaneSemantic.CEILING,
+ PlaneSemantic.TABLE,
+ PlaneSemantic.ANY,
+)
+@Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER)
+internal annotation class PlaneSemanticValue
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/Utils.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/Utils.kt
new file mode 100644
index 0000000..efe9b46
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/Utils.kt
@@ -0,0 +1,226 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import android.os.Handler
+import android.os.Looper
+import androidx.xr.runtime.math.Ray
+import androidx.xr.scenecore.InputEvent.HitInfo
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions as RtDimensions
+import androidx.xr.scenecore.JxrPlatformAdapter.InputEvent as RtInputEvent
+import androidx.xr.scenecore.JxrPlatformAdapter.InputEvent.HitInfo as RtHitInfo
+import androidx.xr.scenecore.JxrPlatformAdapter.MoveEvent as RtMoveEvent
+import androidx.xr.scenecore.JxrPlatformAdapter.PixelDimensions as RtPixelDimensions
+import androidx.xr.scenecore.JxrPlatformAdapter.ResizeEvent as RtResizeEvent
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatialCapabilities as RtSpatialCapabilities
+import java.util.concurrent.Executor
+
+internal class HandlerExecutor(val handler: Handler) : Executor {
+ override fun execute(command: Runnable) {
+ handler.post(command)
+ }
+
+ companion object {
+ val mainThreadExecutor: Executor = HandlerExecutor(Handler(Looper.getMainLooper()))
+ }
+}
+
+/** Extension function that converts a [Dimensions] to [RtDimensions]. */
+internal fun Dimensions.toRtDimensions(): RtDimensions {
+ return RtDimensions(width, height, depth)
+}
+
+/** Extension function that converts a [RtDimensions] to [Dimensions]. */
+internal fun RtDimensions.toDimensions(): Dimensions {
+ return Dimensions(width, height, depth)
+}
+
+/** Extension function that converts a [PixelDimensions] to [RtPixelDimensions]. */
+internal fun PixelDimensions.toRtPixelDimensions(): RtPixelDimensions {
+ return RtPixelDimensions(width, height)
+}
+
+/** Extension function that converts a [RtPixelDimensions] to [PixelDimensions]. */
+internal fun RtPixelDimensions.toPixelDimensions(): PixelDimensions {
+ return PixelDimensions(width, height)
+}
+
+/** Extension function that converts [Int] to [JxrPlatformAdapter.PlaneType]. */
+internal fun Int.toRtPlaneType(): JxrPlatformAdapter.PlaneType {
+ return when (this) {
+ PlaneType.HORIZONTAL -> JxrPlatformAdapter.PlaneType.HORIZONTAL
+ PlaneType.VERTICAL -> JxrPlatformAdapter.PlaneType.VERTICAL
+ PlaneType.ANY -> JxrPlatformAdapter.PlaneType.ANY
+ else -> error("Unknown Plane Type: $PlaneType")
+ }
+}
+
+/** Extension function that converts [Int] to [JxrPlatformAdapter.PlaneSemantic]. */
+internal fun Int.toRtPlaneSemantic(): JxrPlatformAdapter.PlaneSemantic {
+ return when (this) {
+ PlaneSemantic.WALL -> JxrPlatformAdapter.PlaneSemantic.WALL
+ PlaneSemantic.FLOOR -> JxrPlatformAdapter.PlaneSemantic.FLOOR
+ PlaneSemantic.CEILING -> JxrPlatformAdapter.PlaneSemantic.CEILING
+ PlaneSemantic.TABLE -> JxrPlatformAdapter.PlaneSemantic.TABLE
+ PlaneSemantic.ANY -> JxrPlatformAdapter.PlaneSemantic.ANY
+ else -> error("Unknown Plane Semantic: $PlaneSemantic")
+ }
+}
+
+/** Extension function that converts a [RtMoveEvent] to a [MoveEvent]. */
+internal fun RtMoveEvent.toMoveEvent(entityManager: EntityManager): MoveEvent {
+
+ disposedEntity?.let { entityManager.removeEntity(it) }
+ return MoveEvent(
+ moveState.toMoveState(),
+ Ray(initialInputRay.origin, initialInputRay.direction),
+ Ray(currentInputRay.origin, currentInputRay.direction),
+ previousPose,
+ currentPose,
+ previousScale.x,
+ currentScale.x,
+ entityManager.getEntityForRtEntity(initialParent)!!,
+ updatedParent?.let {
+ entityManager.getEntityForRtEntity(it)
+ ?: AnchorEntity.create(it as JxrPlatformAdapter.AnchorEntity, entityManager)
+ },
+ )
+}
+
+/** Extension function that converts a [RtHitInfo] to a [HitInfo]. */
+internal fun RtHitInfo.toHitInfo(entityManager: EntityManager): HitInfo? {
+ // TODO: b/377541143 - Replace instance equality check in EntityManager.
+ val hitEntity = inputEntity?.let { entityManager.getEntityForRtEntity(it) }
+ return if (hitEntity == null) {
+ null
+ } else {
+ HitInfo(inputEntity = hitEntity, hitPosition = hitPosition, transform = transform)
+ }
+}
+
+/** Extension function that converts a [RtInputEvent] to a [InputEvent]. */
+internal fun RtInputEvent.toInputEvent(entityManager: EntityManager): InputEvent {
+ return InputEvent(
+ source.toInputEventSource(),
+ pointerType.toInputEventPointerType(),
+ timestamp,
+ origin,
+ direction,
+ action.toInputEventAction(),
+ hitInfo?.toHitInfo(entityManager),
+ secondaryHitInfo?.toHitInfo(entityManager),
+ )
+}
+
+/** Extension function that converts a [RtSpatialCapabilities] to a [SpatialCapabilities]. */
+internal fun RtSpatialCapabilities.toSpatialCapabilities(): SpatialCapabilities {
+ return SpatialCapabilities(capabilities.toSpatialCapability())
+}
+
+/** Extension function that converts a [RtResizeEvent] to a [ResizeEvent]. */
+internal fun RtResizeEvent.toResizeEvent(): ResizeEvent {
+ return ResizeEvent(resizeState.toResizeState(), newSize.toDimensions())
+}
+
+/**
+ * Extension function that converts a [Set] of [AnchorPlacement] to a [Set] of
+ * [JxrPlatformAdapter.AnchorPlacement].
+ */
+internal fun Set<AnchorPlacement>.toRtAnchorPlacement(
+ runtime: JxrPlatformAdapter
+): Set<JxrPlatformAdapter.AnchorPlacement> {
+ val rtAnchorPlacementSet = HashSet<JxrPlatformAdapter.AnchorPlacement>()
+ for (placement in this) {
+ val planeTypeFilter = placement.planeTypeFilter.map { it.toRtPlaneType() }.toMutableSet()
+ val planeSemanticFilter =
+ placement.planeSemanticFilter.map { it.toRtPlaneSemantic() }.toMutableSet()
+
+ val rtAnchorPlacement =
+ runtime.createAnchorPlacementForPlanes(planeTypeFilter, planeSemanticFilter)
+ rtAnchorPlacementSet.add(rtAnchorPlacement)
+ }
+ return rtAnchorPlacementSet
+}
+
+/** Extension function that converts a [Int] to [MoveEvent.MoveState]. */
[email protected]
+internal fun Int.toMoveState(): Int {
+ return when (this) {
+ JxrPlatformAdapter.MoveEvent.MOVE_STATE_START -> MoveEvent.MOVE_STATE_START
+ JxrPlatformAdapter.MoveEvent.MOVE_STATE_ONGOING -> MoveEvent.MOVE_STATE_ONGOING
+ JxrPlatformAdapter.MoveEvent.MOVE_STATE_END -> MoveEvent.MOVE_STATE_END
+ else -> error("Unknown Move State: $this")
+ }
+}
+
+/** Extension function that converts a [Int] to [ResizeEvent.ResizeState]. */
[email protected]
+internal fun Int.toResizeState(): Int {
+ return when (this) {
+ JxrPlatformAdapter.ResizeEvent.RESIZE_STATE_UNKNOWN -> ResizeEvent.RESIZE_STATE_UNKNOWN
+ JxrPlatformAdapter.ResizeEvent.RESIZE_STATE_START -> ResizeEvent.RESIZE_STATE_START
+ JxrPlatformAdapter.ResizeEvent.RESIZE_STATE_ONGOING -> ResizeEvent.RESIZE_STATE_ONGOING
+ JxrPlatformAdapter.ResizeEvent.RESIZE_STATE_END -> ResizeEvent.RESIZE_STATE_END
+ else -> error("Unknown Resize State: $this")
+ }
+}
+
+/** Extension function that converts a [Int] to [InputEvent.Source]. */
[email protected]
+internal fun Int.toInputEventSource(): Int {
+ return when (this) {
+ JxrPlatformAdapter.InputEvent.SOURCE_UNKNOWN -> InputEvent.SOURCE_UNKNOWN
+ JxrPlatformAdapter.InputEvent.SOURCE_HEAD -> InputEvent.SOURCE_HEAD
+ JxrPlatformAdapter.InputEvent.SOURCE_CONTROLLER -> InputEvent.SOURCE_CONTROLLER
+ JxrPlatformAdapter.InputEvent.SOURCE_HANDS -> InputEvent.SOURCE_HANDS
+ JxrPlatformAdapter.InputEvent.SOURCE_MOUSE -> InputEvent.SOURCE_MOUSE
+ JxrPlatformAdapter.InputEvent.SOURCE_GAZE_AND_GESTURE -> InputEvent.SOURCE_GAZE_AND_GESTURE
+ else -> error("Unknown Input Event Source: $this")
+ }
+}
+
+/** Extension function that converts a [Int] to [InputEvent.PointerType]. */
[email protected]
+internal fun Int.toInputEventPointerType(): Int {
+ return when (this) {
+ JxrPlatformAdapter.InputEvent.POINTER_TYPE_DEFAULT -> InputEvent.POINTER_TYPE_DEFAULT
+ JxrPlatformAdapter.InputEvent.POINTER_TYPE_LEFT -> InputEvent.POINTER_TYPE_LEFT
+ JxrPlatformAdapter.InputEvent.POINTER_TYPE_RIGHT -> InputEvent.POINTER_TYPE_RIGHT
+ else -> error("Unknown Input Event Pointer Type: $this")
+ }
+}
+
+/** Extension function that converts a [Int] to [SpatialCapabilities.SpatialCapability]. */
[email protected]
+internal fun Int.toSpatialCapability(): Int {
+ return this
+}
+
+/** Extension function that converts a [Int] to [InputEvent.Action]. */
[email protected]
+internal fun Int.toInputEventAction(): Int {
+ return when (this) {
+ JxrPlatformAdapter.InputEvent.ACTION_DOWN -> InputEvent.ACTION_DOWN
+ JxrPlatformAdapter.InputEvent.ACTION_UP -> InputEvent.ACTION_UP
+ JxrPlatformAdapter.InputEvent.ACTION_MOVE -> InputEvent.ACTION_MOVE
+ JxrPlatformAdapter.InputEvent.ACTION_CANCEL -> InputEvent.ACTION_CANCEL
+ JxrPlatformAdapter.InputEvent.ACTION_HOVER_MOVE -> InputEvent.ACTION_HOVER_MOVE
+ JxrPlatformAdapter.InputEvent.ACTION_HOVER_ENTER -> InputEvent.ACTION_HOVER_ENTER
+ JxrPlatformAdapter.InputEvent.ACTION_HOVER_EXIT -> InputEvent.ACTION_HOVER_EXIT
+ else -> error("Unknown Input Event Action: $this")
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/common/BaseActivityPose.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/common/BaseActivityPose.java
new file mode 100644
index 0000000..050df03
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/common/BaseActivityPose.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.common;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter.ActivityPose;
+
+/**
+ * Base implementation of JXRCore ActivityPose.
+ *
+ * <p>A ActivityPose is an object that has a pose in the world space.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public abstract class BaseActivityPose implements ActivityPose {
+ @Override
+ @NonNull
+ public Pose getActivitySpacePose() {
+ throw new UnsupportedOperationException(
+ "getActivitySpacePose is not implemented for this ActivityPose.");
+ }
+
+ /** Returns the pose for this entity, relative to the activity space root. */
+ @NonNull
+ public Pose getPoseInActivitySpace() {
+ throw new UnsupportedOperationException(
+ "getPoseInActivitySpace is not implemented for this ActivityPose.");
+ }
+
+ @Override
+ @NonNull
+ public Vector3 getWorldSpaceScale() {
+ return new Vector3(1f, 1f, 1f);
+ }
+
+ @Override
+ @NonNull
+ public Vector3 getActivitySpaceScale() {
+ throw new UnsupportedOperationException(
+ "getActivitySpaceScale is not implemented for this ActivityPose.");
+ }
+
+ @Override
+ @NonNull
+ public Pose transformPoseTo(@NonNull Pose pose, @NonNull ActivityPose destination) {
+
+ // TODO: b/355680575 - Revisit if we need to account for parent rotation when calculating
+ // the
+ // scale. This code might produce unexpected results when non-uniform scale is involved in
+ // the
+ // parent-child entity hierarchy.
+
+ // Compute the inverse scale of the destination entity in the activity space.
+
+ // We assume that the world space root is the activity space root.
+ BaseActivityPose baseDestination = (BaseActivityPose) destination;
+ Vector3 destinationScale = baseDestination.getWorldSpaceScale();
+
+ Vector3 inverseDestinationScale =
+ new Vector3(
+ 1f / destinationScale.getX(),
+ 1f / destinationScale.getY(),
+ 1f / destinationScale.getZ());
+
+ // Compute the transformation to the destination entity from this local entity.
+ Pose activityToLocal = this.getPoseInActivitySpace();
+ Pose activityToDestination = baseDestination.getPoseInActivitySpace();
+ Pose destinationToActivity =
+ new Pose(
+ activityToDestination
+ .getTranslation()
+ .times(inverseDestinationScale),
+ activityToDestination.getRotation())
+ .getInverse();
+ Pose destinationToLocal =
+ destinationToActivity.compose(
+ new Pose(
+ activityToLocal.getTranslation().times(inverseDestinationScale),
+ activityToLocal.getRotation()));
+
+ // Apply the transformation to the destination entity, from this entity, on the local pose.
+ return destinationToLocal.compose(
+ new Pose(
+ pose.getTranslation()
+ .times(this.getWorldSpaceScale().times(inverseDestinationScale)),
+ pose.getRotation()));
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/common/BaseEntity.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/common/BaseEntity.java
new file mode 100644
index 0000000..c2584bc
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/common/BaseEntity.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.common;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter.Component;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Implementation of a subset of core RealityCore Entity functionality. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public abstract class BaseEntity extends BaseActivityPose implements Entity {
+ private final List<Entity> children = new ArrayList<>();
+ private final List<Component> componentList = new ArrayList<>();
+ private BaseEntity parent;
+ private Pose pose = new Pose();
+ private Vector3 scale = new Vector3(1.0f, 1.0f, 1.0f);
+ private float alpha = 1.0f;
+ private boolean hidden = false;
+
+ protected void addChildInternal(@NonNull Entity child) {
+ if (children.contains(child)) {
+ Log.w("RealityCoreRuntime", "Trying to add child who is already a child.");
+ }
+ children.add(child);
+ }
+
+ protected void removeChildInternal(@NonNull Entity child) {
+ if (!children.contains(child)) {
+ Log.w("RealityCoreRuntime", "Trying to remove child who is not a child.");
+ return;
+ }
+ children.remove(child);
+ }
+
+ @Override
+ public void addChild(@NonNull Entity child) {
+ child.setParent(this);
+ }
+
+ @Override
+ public void addChildren(@NonNull List<Entity> children) {
+ for (Entity child : children) {
+ child.setParent(this);
+ }
+ }
+
+ @Override
+ @Nullable
+ public Entity getParent() {
+ return parent;
+ }
+
+ @Override
+ public void setParent(@Nullable Entity parent) {
+ if ((parent != null) && !(parent instanceof BaseEntity)) {
+ Log.e("RealityCoreRuntime", "Cannot set non-BaseEntity as a parent of a BaseEntity");
+ return;
+ }
+ if (this.parent != null) {
+ this.parent.removeChildInternal(this);
+ }
+ this.parent = (BaseEntity) parent;
+ if (this.parent != null) {
+ this.parent.addChildInternal(this);
+ }
+ }
+
+ @Override
+ @NonNull
+ public List<Entity> getChildren() {
+ return children;
+ }
+
+ @Override
+ public void setContentDescription(@NonNull String text) {
+ // TODO(b/320202321): Finish A11y Text integration.
+ Log.i("BaseEntity", "setContentDescription: " + text);
+ }
+
+ @Override
+ @NonNull
+ public Pose getPose() {
+ return pose;
+ }
+
+ @Override
+ public void setPose(@NonNull Pose pose) {
+ this.pose = pose;
+ }
+
+ @Override
+ @NonNull
+ public Pose getActivitySpacePose() {
+ // Any parentless "space" entities (such as the root and anchor entities) are expected to
+ // override this method non-recursively so that this error is never thrown.
+ if (parent == null) {
+ throw new IllegalStateException("Cannot get pose in ActivitySpace with a null parent");
+ }
+
+ return parent.getActivitySpacePose()
+ .compose(
+ new Pose(
+ this.pose.getTranslation().times(parent.getWorldSpaceScale()),
+ this.pose.getRotation()));
+ }
+
+ @Override
+ @NonNull
+ public Vector3 getScale() {
+ return scale;
+ }
+
+ @Override
+ public void setScale(@NonNull Vector3 scale) {
+ this.scale = scale;
+ }
+
+ // Purely sets the value of the scale.
+ protected final void setScaleInternal(@NonNull Vector3 scale) {
+ this.scale = scale;
+ }
+
+ @Override
+ public float getAlpha() {
+ return alpha;
+ }
+
+ @Override
+ public void setAlpha(float alpha) {
+ this.alpha = alpha;
+ }
+
+ @Override
+ public float getActivitySpaceAlpha() {
+ if (parent == null) {
+ return alpha;
+ }
+ return parent.getActivitySpaceAlpha() * alpha;
+ }
+
+ @Override
+ @NonNull
+ public Vector3 getWorldSpaceScale() {
+ if (parent == null) {
+ throw new IllegalStateException("Cannot get scale in WorldSpace with a null parent");
+ }
+ return parent.getWorldSpaceScale().times(this.scale);
+ }
+
+ @Override
+ @NonNull
+ public Vector3 getActivitySpaceScale() {
+ if (parent == null) {
+ throw new IllegalStateException("Cannot get scale in ActivitySpace with a null parent");
+ }
+ return parent.getActivitySpaceScale().times(this.scale);
+ }
+
+ @Override
+ public boolean isHidden(boolean includeParents) {
+ if (!includeParents || parent == null) {
+ return hidden;
+ }
+ return hidden || parent.isHidden(true);
+ }
+
+ @Override
+ public void setHidden(boolean hidden) {
+ this.hidden = hidden;
+ }
+
+ @Override
+ public void dispose() {
+ // Create a copy to avoid concurrent modification issues since the children detach
+ // themselves
+ // from their parents as they are disposed.
+ List<Entity> childrenToDispose = new ArrayList<>(children);
+ for (Entity child : childrenToDispose) {
+ child.dispose();
+ }
+ }
+
+ @Override
+ public boolean addComponent(@NonNull Component component) {
+ if (component.onAttach(this)) {
+ componentList.add(component);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void removeComponent(@NonNull Component component) {
+ if (componentList.contains(component)) {
+ component.onDetach(this);
+ componentList.remove(component);
+ }
+ }
+
+ @Override
+ public void removeAllComponents() {
+ for (Component component : componentList) {
+ component.onDetach(this);
+ }
+ componentList.clear();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/ActivityPanelEntityImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/ActivityPanelEntityImpl.java
new file mode 100644
index 0000000..3da6432
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/ActivityPanelEntityImpl.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.xr.extensions.XrExtensions;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.extensions.space.ActivityPanel;
+import androidx.xr.scenecore.JxrPlatformAdapter.ActivityPanelEntity;
+import androidx.xr.scenecore.JxrPlatformAdapter.PixelDimensions;
+
+import java.util.Objects;
+import java.util.concurrent.ScheduledExecutorService;
+
+/** Implementation of {@link ActivityPanelEntity}. */
+class ActivityPanelEntityImpl extends BasePanelEntity implements ActivityPanelEntity {
+ private static final String TAG = ActivityPanelEntityImpl.class.getSimpleName();
+ private final ActivityPanel activityPanel;
+
+ // TODO(b/352630140): Add a static factory method and remove the business logic from
+ // JxrPlatformAdapterAxr.
+
+ ActivityPanelEntityImpl(
+ Node node,
+ XrExtensions extensions,
+ EntityManager entityManager,
+ ActivityPanel activityPanel,
+ PixelDimensions windowBoundsPx,
+ ScheduledExecutorService executor) {
+ super(node, extensions, entityManager, executor);
+ // We need to notify our base class of the pixelDimensions, even though the Extensions are
+ // initialized in the factory method. (ext.ActivityPanel.setWindowBounds, etc)
+ super.setPixelDimensions(windowBoundsPx);
+ this.activityPanel = activityPanel;
+ }
+
+ @Override
+ public void launchActivity(Intent intent, @Nullable Bundle bundle) {
+ // Note that launching an Activity into the Panel doesn't actually update the size. The
+ // application is expected to set the size of the ActivityPanel at construction time, before
+ // launching an Activity into it. The Activity will then render into the size the
+ // application
+ // specified, and the system will apply letterboxing if necessary.
+ activityPanel.launchActivity(intent, bundle);
+ }
+
+ @Override
+ public void moveActivity(Activity activity) {
+ // Note that moving an Activity into the Panel doesn't actually update the size. The
+ // application
+ // should explicitly call setPixelDimensions() to update the size of an ActivityPanel.
+ activityPanel.moveActivity(activity);
+ }
+
+ @Override
+ public void setPixelDimensions(PixelDimensions dimensions) {
+ PixelDimensions oldDimensions = this.pixelDimensions;
+ super.setPixelDimensions(dimensions);
+
+ // Avoid updating the bounds if we were called with the same values.
+ if (Objects.equals(oldDimensions, dimensions)) {
+ Log.i(TAG, "setPixelDimensions called with same dimensions - " + dimensions);
+ return;
+ }
+
+ activityPanel.setWindowBounds(new Rect(0, 0, dimensions.width, dimensions.height));
+ }
+
+ /**
+ * Disposes the ActivityPanelEntity.
+ *
+ * <p>This will delete the ActivityPanel and destroy the embedded activity.
+ */
+ @Override
+ public void dispose() {
+ activityPanel.delete();
+ super.dispose();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/ActivitySpaceImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/ActivitySpaceImpl.java
new file mode 100644
index 0000000..a959d70
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/ActivitySpaceImpl.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.xr.extensions.XrExtensions;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.extensions.space.Bounds;
+import androidx.xr.extensions.space.SpatialState;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter.ActivitySpace;
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+
+/**
+ * Implementation of a RealityCore ActivitySpaceImpl.
+ *
+ * <p>This is used to create an entity that contains the task node.
+ */
+@SuppressWarnings({"deprecation", "UnnecessarilyFullyQualified"}) // TODO(b/373435470): Remove
+final class ActivitySpaceImpl extends SystemSpaceEntityImpl implements ActivitySpace {
+
+ private static final String TAG = "ActivitySpaceImpl";
+
+ private final Set<OnBoundsChangedListener> boundsListeners =
+ Collections.synchronizedSet(new HashSet<>());
+
+ private final Supplier<SpatialState> spatialStateProvider;
+ private final AtomicReference<Dimensions> bounds = new AtomicReference<>();
+
+ public ActivitySpaceImpl(
+ Node taskNode,
+ XrExtensions extensions,
+ EntityManager entityManager,
+ Supplier<SpatialState> spatialStateProvider,
+ ScheduledExecutorService executor) {
+ super(taskNode, extensions, entityManager, executor);
+
+ this.spatialStateProvider = spatialStateProvider;
+ }
+
+ /** Returns the identity pose since this entity defines the origin of the activity space. */
+ @Override
+ public Pose getPoseInActivitySpace() {
+ return new Pose();
+ }
+
+ /** Returns the identity pose since we assume the activity space is the world space root. */
+ @Override
+ public Pose getActivitySpacePose() {
+
+ return new Pose();
+ }
+
+ @Override
+ public Vector3 getActivitySpaceScale() {
+ return new Vector3(1.0f, 1.0f, 1.0f);
+ }
+
+ @Override
+ public void setParent(Entity parent) {
+ Log.e(TAG, "Cannot set parent for the ActivitySpace.");
+ }
+
+ @Override
+ public void setScale(Vector3 scale) {
+ // TODO(b/349391097): make this behavior consistent with AnchorEntityImpl
+ Log.e(TAG, "Cannot set scale for the ActivitySpace.");
+ }
+
+ @SuppressWarnings("ObjectToString")
+ @Override
+ public void dispose() {
+ Log.i(TAG, "Disposing " + this);
+ super.dispose();
+ }
+
+ @Override
+ public Dimensions getBounds() {
+ // The bounds are kept in sync with the Extensions in the onBoundsChangedEvent callback. We
+ // only
+ // invoke getSpatialState if they've never been set.
+ return bounds.updateAndGet(
+ oldBounds -> {
+ if (oldBounds == null) {
+ Bounds bounds = spatialStateProvider.get().getBounds();
+ return new Dimensions(bounds.width, bounds.height, bounds.depth);
+ }
+ return oldBounds;
+ });
+ }
+
+ @Override
+ public void addOnBoundsChangedListener(@NonNull OnBoundsChangedListener listener) {
+ this.boundsListeners.add(listener);
+ }
+
+ @Override
+ public void removeOnBoundsChangedListener(@NonNull OnBoundsChangedListener listener) {
+ this.boundsListeners.remove(listener);
+ }
+
+ /**
+ * This method is called by the Runtime when the bounds of the Activity change. We dispatch the
+ * event upwards to the JXRCoreSession via ActivitySpace.
+ *
+ * <p>Note that this call happens on the Activity's UI thread, so we should be careful not to
+ * block it.
+ */
+ public void onBoundsChanged(Bounds newBounds) {
+ Dimensions newDimensions =
+ bounds.updateAndGet(
+ oldBounds ->
+ new Dimensions(newBounds.width, newBounds.height, newBounds.depth));
+ for (OnBoundsChangedListener listener : boundsListeners) {
+ listener.onBoundsChanged(newDimensions);
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/AnchorEntityImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/AnchorEntityImpl.java
new file mode 100644
index 0000000..8ab6970
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/AnchorEntityImpl.java
@@ -0,0 +1,652 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.xr.extensions.XrExtensions;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.extensions.node.NodeTransaction;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.runtime.openxr.ExportableAnchor;
+import androidx.xr.scenecore.JxrPlatformAdapter.ActivitySpace;
+import androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity;
+import androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.OnStateChangedListener;
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.PlaneSemantic;
+import androidx.xr.scenecore.JxrPlatformAdapter.PlaneType;
+import androidx.xr.scenecore.impl.perception.Anchor;
+import androidx.xr.scenecore.impl.perception.PerceptionLibrary;
+import androidx.xr.scenecore.impl.perception.Plane;
+import androidx.xr.scenecore.impl.perception.Plane.PlaneData;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Duration;
+import java.util.UUID;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+
+/**
+ * Implementation of AnchorEntity.
+ *
+ * <p>This entity creates trackable anchors in space.
+ */
+@SuppressWarnings("BanSynchronizedMethods")
+class AnchorEntityImpl extends SystemSpaceEntityImpl implements AnchorEntity {
+ public static final Duration ANCHOR_SEARCH_DELAY = Duration.ofMillis(500);
+ public static final Duration PERSIST_STATE_CHECK_DELAY = Duration.ofMillis(500);
+ public static final String ANCHOR_NODE_NAME = "AnchorNode";
+ private static final String TAG = "AnchorEntityImpl";
+ private final ActivitySpaceImpl activitySpace;
+ private final AndroidXrEntity activitySpaceRoot;
+ private final PerceptionLibrary perceptionLibrary;
+ private OnStateChangedListener onStateChangedListener;
+ private State state = State.UNANCHORED;
+ private PersistState persistState = PersistState.PERSIST_NOT_REQUESTED;
+ private Anchor anchor;
+ private UUID uuid = null;
+ private PersistStateChangeListener persistStateChangeListener;
+
+ private static class AnchorCreationData {
+
+ static final int ANCHOR_CREATION_SEMANTIC = 1;
+ static final int ANCHOR_CREATION_PERSISTED = 2;
+ static final int ANCHOR_CREATION_PLANE = 3;
+ static final int ANCHOR_CREATION_PERCEPTION_ANCHOR = 4;
+
+ @AnchorCreationType int anchorCreationType;
+
+ /** IntDef for Anchor creation types. */
+ @IntDef({
+ ANCHOR_CREATION_SEMANTIC,
+ ANCHOR_CREATION_PERSISTED,
+ ANCHOR_CREATION_PLANE,
+ ANCHOR_CREATION_PERCEPTION_ANCHOR,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface AnchorCreationType {}
+
+ // Anchor that is already created via Perception API.
+ androidx.xr.arcore.Anchor perceptionAnchor;
+
+ // Anchor search deadline for semantic and persisted anchors.
+ Long anchorSearchDeadline;
+
+ // Fields exclusively for semantic anchors.
+ Dimensions dimensions;
+ PlaneType planeType;
+ PlaneSemantic planeSemantic;
+
+ // Fields exclusively for persisted anchors.
+ UUID uuid = null;
+
+ // Fields exclusively for plane anchors.
+ Plane plane;
+ Pose planeOffsetPose;
+ Long planeDataTimeNs;
+ }
+
+ static AnchorEntityImpl createSemanticAnchor(
+ Node node,
+ Dimensions dimensions,
+ PlaneType planeType,
+ PlaneSemantic planeSemantic,
+ Duration anchorSearchTimeout,
+ ActivitySpace activitySpace,
+ Entity activitySpaceRoot,
+ XrExtensions extensions,
+ EntityManager entityManager,
+ ScheduledExecutorService executor,
+ PerceptionLibrary perceptionLibrary) {
+ AnchorCreationData anchorCreationData = new AnchorCreationData();
+ anchorCreationData.anchorCreationType = AnchorCreationData.ANCHOR_CREATION_SEMANTIC;
+ anchorCreationData.dimensions = dimensions;
+ anchorCreationData.planeType = planeType;
+ anchorCreationData.planeSemantic = planeSemantic;
+ anchorCreationData.anchorSearchDeadline = getAnchorDeadline(anchorSearchTimeout);
+ return new AnchorEntityImpl(
+ node,
+ anchorCreationData,
+ activitySpace,
+ activitySpaceRoot,
+ extensions,
+ entityManager,
+ executor,
+ perceptionLibrary);
+ }
+
+ static AnchorEntityImpl createPersistedAnchor(
+ Node node,
+ UUID uuid,
+ Duration anchorSearchTimeout,
+ ActivitySpace activitySpace,
+ Entity activitySpaceRoot,
+ XrExtensions extensions,
+ EntityManager entityManager,
+ ScheduledExecutorService executor,
+ PerceptionLibrary perceptionLibrary) {
+ AnchorCreationData anchorCreationData = new AnchorCreationData();
+ anchorCreationData.anchorCreationType = AnchorCreationData.ANCHOR_CREATION_PERSISTED;
+ anchorCreationData.uuid = uuid;
+ anchorCreationData.anchorSearchDeadline = getAnchorDeadline(anchorSearchTimeout);
+ return new AnchorEntityImpl(
+ node,
+ anchorCreationData,
+ activitySpace,
+ activitySpaceRoot,
+ extensions,
+ entityManager,
+ executor,
+ perceptionLibrary);
+ }
+
+ static AnchorEntityImpl createAnchorFromPlane(
+ Node node,
+ Plane plane,
+ Pose planeOffsetPose,
+ @Nullable Long planeDataTimeNs,
+ ActivitySpace activitySpace,
+ Entity activitySpaceRoot,
+ XrExtensions extensions,
+ EntityManager entityManager,
+ ScheduledExecutorService executor,
+ PerceptionLibrary perceptionLibrary) {
+ AnchorCreationData anchorCreationData = new AnchorCreationData();
+ anchorCreationData.anchorCreationType = AnchorCreationData.ANCHOR_CREATION_PLANE;
+ anchorCreationData.plane = plane;
+ anchorCreationData.planeOffsetPose = planeOffsetPose;
+ anchorCreationData.planeDataTimeNs = planeDataTimeNs;
+ return new AnchorEntityImpl(
+ node,
+ anchorCreationData,
+ activitySpace,
+ activitySpaceRoot,
+ extensions,
+ entityManager,
+ executor,
+ perceptionLibrary);
+ }
+
+ static AnchorEntityImpl createAnchorFromPerceptionAnchor(
+ Node node,
+ androidx.xr.arcore.Anchor anchor,
+ ActivitySpace activitySpace,
+ Entity activitySpaceRoot,
+ XrExtensions extensions,
+ EntityManager entityManager,
+ ScheduledExecutorService executor,
+ PerceptionLibrary perceptionLibrary) {
+ AnchorCreationData anchorCreationData = new AnchorCreationData();
+ anchorCreationData.anchorCreationType =
+ AnchorCreationData.ANCHOR_CREATION_PERCEPTION_ANCHOR;
+ anchorCreationData.perceptionAnchor = anchor;
+ return new AnchorEntityImpl(
+ node,
+ anchorCreationData,
+ activitySpace,
+ activitySpaceRoot,
+ extensions,
+ entityManager,
+ executor,
+ perceptionLibrary);
+ }
+
+ protected AnchorEntityImpl(
+ Node node,
+ AnchorCreationData anchorCreationData,
+ ActivitySpace activitySpace,
+ Entity activitySpaceRoot,
+ XrExtensions extensions,
+ EntityManager entityManager,
+ ScheduledExecutorService executor,
+ PerceptionLibrary perceptionLibrary) {
+ super(node, extensions, entityManager, executor);
+ this.perceptionLibrary = perceptionLibrary;
+
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction.setName(node, ANCHOR_NODE_NAME).apply();
+ }
+
+ if (activitySpace instanceof ActivitySpaceImpl) {
+ this.activitySpace = (ActivitySpaceImpl) activitySpace;
+ } else {
+ Log.e(
+ TAG,
+ "ActivitySpace is not an instance of ActivitySpaceImpl.Anchor is in Error"
+ + " state.");
+ this.state = State.ERROR;
+ this.activitySpace = null;
+ }
+
+ if (activitySpaceRoot instanceof AndroidXrEntity) {
+ this.activitySpaceRoot = (AndroidXrEntity) activitySpaceRoot;
+ } else {
+ Log.e(
+ TAG,
+ "ActivitySpaceRoot is not an instance of AndroidXrEntity. Anchor is in Error"
+ + " state.");
+ this.state = State.ERROR;
+ this.activitySpaceRoot = null;
+ }
+
+ // Return early if the state is already in an error state.
+ if (this.state == State.ERROR) {
+ return;
+ }
+
+ // If we are creating a semantic or persisted anchor then we need to search for the anchor
+ // asynchronously. Otherwise we can create the anchor on the plane.
+ if (anchorCreationData.anchorCreationType
+ == AnchorCreationData.ANCHOR_CREATION_PERCEPTION_ANCHOR) {
+ tryConvertAnchor(anchorCreationData.perceptionAnchor);
+ } else if (anchorCreationData.anchorCreationType
+ == AnchorCreationData.ANCHOR_CREATION_SEMANTIC
+ || anchorCreationData.anchorCreationType
+ == AnchorCreationData.ANCHOR_CREATION_PERSISTED) {
+ tryFindAnchor(anchorCreationData);
+ } else if (anchorCreationData.anchorCreationType
+ == AnchorCreationData.ANCHOR_CREATION_PLANE) {
+ tryCreateAnchorForPlane(anchorCreationData);
+ }
+ }
+
+ @Nullable
+ private static Long getAnchorDeadline(Duration anchorSearchTimeout) {
+ // If the timeout is zero or null then we return null here and the anchor search will
+ // continue
+ // indefinitely.
+ if (anchorSearchTimeout == null || anchorSearchTimeout.isZero()) {
+ return null;
+ }
+ return SystemClock.uptimeMillis() + anchorSearchTimeout.toMillis();
+ }
+
+ // Converts a perception anchor to JXRCore runtime anchor.
+ private void tryConvertAnchor(androidx.xr.arcore.Anchor perceptionAnchor) {
+ ExportableAnchor exportableAnchor = (ExportableAnchor) perceptionAnchor.getRuntimeAnchor();
+ this.anchor =
+ new Anchor(exportableAnchor.getNativePointer(), exportableAnchor.getAnchorToken());
+ if (anchor.getAnchorToken() == null) {
+ updateState(State.ERROR);
+ return;
+ }
+ updateState(State.ANCHORED);
+ }
+
+ // Creates an anchor on the provided plane.
+ private void tryCreateAnchorForPlane(AnchorCreationData anchorCreationData) {
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ RuntimeUtils.poseToPerceptionPose(anchorCreationData.planeOffsetPose);
+ anchor =
+ anchorCreationData.plane.createAnchor(
+ perceptionPose, anchorCreationData.planeDataTimeNs);
+ if (anchor == null || anchor.getAnchorToken() == null) {
+ updateState(State.ERROR);
+ return;
+ }
+ updateState(State.ANCHORED);
+ }
+
+ // Schedules a search for the anchor.
+ private void scheduleTryFindAnchor(AnchorCreationData anchorCreationData) {
+ ScheduledFuture<?> unusedAnchorFuture =
+ executor.schedule(
+ () -> tryFindAnchor(anchorCreationData),
+ ANCHOR_SEARCH_DELAY.toMillis(),
+ MILLISECONDS);
+ }
+
+ // Checks if the anchor search has exceeded the deadline.
+ private boolean searchDeadlineExceeded(Long anchorSearchDeadline) {
+ // If the system is paused it will continue to count after it wakes up.
+ return anchorSearchDeadline != null && SystemClock.uptimeMillis() > anchorSearchDeadline;
+ }
+
+ private synchronized void cancelAnchorSearch() {
+ if (state == State.UNANCHORED) {
+ Log.i(TAG, "Stopping search for anchor, reached timeout.");
+ updateState(State.TIMED_OUT);
+ }
+ }
+
+ // Searches for the anchor and updates the state based on the result. If the anchor wasn't found
+ // then the search is scheduled again if the deadline has not been exceeded.
+ private void tryFindAnchor(AnchorCreationData anchorCreationData) {
+ if (this.activitySpace == null) {
+ Log.e(TAG, "Skipping search for anchor there is no valid parent.");
+ return;
+ }
+ synchronized (this) {
+ if (state != State.UNANCHORED) {
+ // This should only be searching for an anchor if the state is UNANCHORED. If the
+ // state is
+ // ANCHORED then the anchor was already found, if it is ERROR then the entity no
+ // longer can
+ // use the anchor. Return here to stop the search.
+ Log.i(TAG, "Stopping search for anchor, the state is: " + state);
+ return;
+ }
+ }
+ // Check if we are passed the deadline if so, cancel the search.
+ if (searchDeadlineExceeded(anchorCreationData.anchorSearchDeadline)) {
+ cancelAnchorSearch();
+ return;
+ }
+
+ if (perceptionLibrary.getSession() == null) {
+ scheduleTryFindAnchor(anchorCreationData);
+ return;
+ }
+
+ if (anchorCreationData.anchorCreationType == AnchorCreationData.ANCHOR_CREATION_SEMANTIC) {
+ anchor = findPlaneAnchor(anchorCreationData);
+ } else if (anchorCreationData.anchorCreationType
+ == AnchorCreationData.ANCHOR_CREATION_PERSISTED) {
+ anchor = perceptionLibrary.getSession().createAnchorFromUuid(anchorCreationData.uuid);
+ } else {
+ Log.e(
+ TAG,
+ "Searching for anchor creation type is not supported: "
+ + anchorCreationData.anchorCreationType);
+ }
+
+ if (anchor == null || anchor.getAnchorToken() == null) {
+ scheduleTryFindAnchor(anchorCreationData);
+ return;
+ }
+ Log.i(TAG, "Received anchor: " + anchor.getAnchorToken());
+ // TODO: b/330933143 - Handle Additional anchor states (e.g. Error/ Becoming unanchored)
+ synchronized (this) {
+ // Make sure that we are still looking for the anchor before updating the state. The
+ // application might have closed or disposed of the AnchorEntity while the search was
+ // still
+ // active on another thread.
+ if (state != State.UNANCHORED
+ || searchDeadlineExceeded(anchorCreationData.anchorSearchDeadline)) {
+ Log.i(TAG, "Found anchor but no longer searching.");
+ if (searchDeadlineExceeded(anchorCreationData.anchorSearchDeadline)) {
+ cancelAnchorSearch();
+ }
+ // Detach the found anchor since it is no longer needed.
+ if (!anchor.detach()) {
+ Log.e(TAG, "Error when detaching anchor.");
+ }
+ return;
+ }
+ updateState(State.ANCHORED);
+ if (anchorCreationData.anchorCreationType
+ == AnchorCreationData.ANCHOR_CREATION_PERSISTED) {
+ this.uuid = anchorCreationData.uuid;
+ updatePersistState(PersistState.PERSISTED);
+ }
+ }
+ }
+
+ // Tries to find a plane that matches the semantic anchor requirements. This creates an anchor
+ // on
+ // the plane if found.
+ @Nullable
+ private Anchor findPlaneAnchor(AnchorCreationData anchorCreationData) {
+ for (Plane plane : perceptionLibrary.getSession().getAllPlanes()) {
+ long timeNow = SystemClock.uptimeMillis() * 1000000;
+ PlaneData planeData = plane.getData(timeNow);
+ if (planeData == null) {
+ Log.e(TAG, "Plane data is null for plane");
+ continue;
+ }
+ Log.i(
+ TAG,
+ "Found a matching plane with Extent Width: "
+ + planeData.extentWidth
+ + ", Extent Height: "
+ + planeData.extentHeight
+ + ", Type: "
+ + planeData.type
+ + ", Label: "
+ + planeData.label);
+ Plane.Type perceptionType = RuntimeUtils.getPlaneType(anchorCreationData.planeType);
+ Plane.Label perceptionLabel =
+ RuntimeUtils.getPlaneLabel(anchorCreationData.planeSemantic);
+ if (anchorCreationData.dimensions.width <= planeData.extentWidth
+ && anchorCreationData.dimensions.height <= planeData.extentHeight
+ && (planeData.type == perceptionType || perceptionType == Plane.Type.ARBITRARY)
+ && (planeData.label == perceptionLabel
+ || perceptionLabel == Plane.Label.UNKNOWN)) {
+ return plane.createAnchor(
+ androidx.xr.scenecore.impl.perception.Pose.identity(), timeNow);
+ }
+ }
+ return null;
+ }
+
+ private synchronized void updateState(State newState) {
+ if (state == newState) {
+ return;
+ }
+ state = newState;
+ if (state == State.ANCHORED) {
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ // Attach to the root CPM node. This will enable the anchored content to be visible.
+ // Note
+ // that the parent of the Entity is null, but the CPM Node is still attached.
+ transaction
+ .setParent(node, activitySpace.getNode())
+ .setAnchorId(node, anchor.getAnchorToken())
+ .apply();
+ }
+ }
+ if (onStateChangedListener != null) {
+ onStateChangedListener.onStateChanged(state);
+ }
+ }
+
+ @Override
+ public State getState() {
+ return state;
+ }
+
+ @Override
+ public void setOnStateChangedListener(OnStateChangedListener onStateChangedListener) {
+ this.onStateChangedListener = onStateChangedListener;
+ }
+
+ @Override
+ @Nullable
+ public UUID persist() {
+ if (uuid != null) {
+ return uuid;
+ }
+ if (state != State.ANCHORED) {
+ Log.e(TAG, "Cannot persist an anchor that is not in the ANCHORED state.");
+ return null;
+ }
+ uuid = anchor.persist();
+ if (uuid == null) {
+ Log.e(TAG, "Failed to get a UUID for the anchor.");
+ return null;
+ }
+ updatePersistState(PersistState.PERSIST_PENDING);
+ schedulePersistStateCheck();
+ return uuid;
+ }
+
+ private void schedulePersistStateCheck() {
+ ScheduledFuture<?> unusedPersistStateFuture =
+ executor.schedule(
+ this::checkPersistState,
+ PERSIST_STATE_CHECK_DELAY.toMillis(),
+ MILLISECONDS);
+ }
+
+ private void checkPersistState() {
+ synchronized (this) {
+ if (anchor == null) {
+ Log.i(
+ TAG,
+ "Anchor is disposed before becoming persisted, stop checking its persist"
+ + " state.");
+ return;
+ }
+ if (anchor.getPersistState() == Anchor.PersistState.PERSISTED) {
+ updatePersistState(PersistState.PERSISTED);
+ Log.i(TAG, "Anchor is persisted.");
+ return;
+ }
+ }
+ schedulePersistStateCheck();
+ }
+
+ @Override
+ public void registerPersistStateChangeListener(
+ PersistStateChangeListener persistStateChangeListener) {
+ this.persistStateChangeListener = persistStateChangeListener;
+ }
+
+ private synchronized void updatePersistState(PersistState newPersistState) {
+ if (persistState == newPersistState) {
+ return;
+ }
+ persistState = newPersistState;
+ if (persistStateChangeListener != null) {
+ persistStateChangeListener.onPersistStateChanged(newPersistState);
+ }
+ }
+
+ @Override
+ public PersistState getPersistState() {
+ return persistState;
+ }
+
+ @Override
+ public long nativePointer() {
+ return anchor.getAnchorId();
+ }
+
+ @Override
+ public Pose getPose() {
+ throw new UnsupportedOperationException("Cannot get 'pose' on an AnchorEntity.");
+ }
+
+ @Override
+ public void setPose(Pose pose) {
+ throw new UnsupportedOperationException("Cannot set 'pose' on an AnchorEntity.");
+ }
+
+ @Override
+ public void setScale(Vector3 scale) {
+ // TODO(b/349391097): make this behavior consistent with ActivitySpaceImpl
+ throw new UnsupportedOperationException("Cannot set 'scale' on an AnchorEntity.");
+ }
+
+ // TODO: b/360168321 Use the OpenXrPosableHelper when retrieving the pose in activity space.
+ @Override
+ public Pose getPoseInActivitySpace() {
+ synchronized (this) {
+ if (activitySpace == null) {
+ throw new IllegalStateException(
+ "Cannot get pose in Activity Space with a null Activity Space.");
+ }
+
+ if (state != State.ANCHORED) {
+ Log.w(
+ TAG,
+ "Cannot retrieve pose in underlying space. Ensure that the anchor is"
+ + " anchored before calling this method. Returning identity pose.");
+ return new Pose();
+ }
+
+ // ActivitySpace and the anchor have unit scale and the anchor has no direct parent so
+ // we can
+ // just compose the two poses without scaling.
+ final Pose openXrToAnchor = this.getPoseInOpenXrReferenceSpace();
+ final Pose openXrToActivitySpace = activitySpace.getPoseInOpenXrReferenceSpace();
+ if (openXrToActivitySpace == null || openXrToAnchor == null) {
+ Log.e(
+ TAG,
+ "Cannot retrieve pose in underlying space despite anchor being anchored."
+ + " Returning identity pose.");
+ return new Pose();
+ }
+
+ final Pose activitySpaceToOpenXr = openXrToActivitySpace.getInverse();
+ return activitySpaceToOpenXr.compose(openXrToAnchor);
+ }
+ }
+
+ // TODO: b/360168321 Use the OpenXrPosableHelper when retrieving the pose in world space.
+ @Override
+ public Pose getActivitySpacePose() {
+ if (activitySpaceRoot == null) {
+ throw new IllegalStateException(
+ "Cannot get pose in World Space Pose with a null World Space Entity.");
+ }
+
+ // ActivitySpace and the anchor have unit scale and the anchor has no direct parent so we
+ // can
+ // just
+ // compose the two poses without scaling.
+ final Pose activitySpaceToAnchor = this.getPoseInActivitySpace();
+ final Pose worldSpaceToActivitySpace =
+ activitySpaceRoot.getPoseInActivitySpace().getInverse();
+ return worldSpaceToActivitySpace.compose(activitySpaceToAnchor);
+ }
+
+ @Override
+ public Vector3 getActivitySpaceScale() {
+ return getWorldSpaceScale().div(activitySpace.getWorldSpaceScale());
+ }
+
+ @Override
+ public void setParent(Entity parent) {
+ throw new UnsupportedOperationException("Cannot set 'parent' on an AnchorEntity.");
+ }
+
+ @SuppressWarnings("ObjectToString")
+ @Override
+ public void dispose() {
+ Log.i(TAG, "Disposing " + this);
+
+ synchronized (this) {
+ // Return early if it is already in the error state.
+ if (state == State.ERROR) {
+ return;
+ }
+ updateState(State.ERROR);
+ if (anchor != null && !anchor.detach()) {
+ Log.e(TAG, "Error when detaching anchor.");
+ }
+ anchor = null;
+ }
+
+ // Set the parent of the CPM node to null; to hide the anchored content.The parent of the
+ // entity
+ // was always null so does not need to be reset.
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction.setAnchorId(node, null);
+ transaction.setParent(node, null).apply();
+ }
+ super.dispose();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/AnchorPlacementImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/AnchorPlacementImpl.java
new file mode 100644
index 0000000..65f75a5
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/AnchorPlacementImpl.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import androidx.xr.scenecore.JxrPlatformAdapter.AnchorPlacement;
+import androidx.xr.scenecore.JxrPlatformAdapter.PlaneSemantic;
+import androidx.xr.scenecore.JxrPlatformAdapter.PlaneType;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Constructor for an AnchorPlacement.
+ *
+ * <p>Setting a [PlaneType] or [PlaneSemantic] anchor placement means that the [Entity] with a
+ * [MovableComponent] will be anchored to a plane of that [PlaneType] or [PlaneSemantic] if it is
+ * released while nearby after being moved. If no [PlaneType] or [PlaneSemantic] is set the [Entity]
+ * will not be anchored.
+ */
+class AnchorPlacementImpl implements AnchorPlacement {
+ protected Set<PlaneType> planeTypeFilter = new HashSet<>();
+ protected Set<PlaneSemantic> planeSemanticFilter = new HashSet<>();
+
+ AnchorPlacementImpl() {}
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/AndroidXrEntity.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/AndroidXrEntity.java
new file mode 100644
index 0000000..1bbb7d8
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/AndroidXrEntity.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.util.Log;
+
+import androidx.xr.extensions.Consumer;
+import androidx.xr.extensions.XrExtensions;
+import androidx.xr.extensions.node.InputEvent;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.extensions.node.NodeTransaction;
+import androidx.xr.extensions.node.ReformEvent;
+import androidx.xr.extensions.node.ReformOptions;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Quaternion;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.InputEventListener;
+import androidx.xr.scenecore.JxrPlatformAdapter.PointerCaptureComponent;
+import androidx.xr.scenecore.common.BaseEntity;
+
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * Implementation of a RealityCore Entity that wraps an android XR extension Node.
+ *
+ * <p>This should not be created on its own but should be inherited by objects that need to wrap an
+ * Android extension node.
+ */
+@SuppressWarnings({"BanSynchronizedMethods", "BanConcurrentHashMap"})
+abstract class AndroidXrEntity extends BaseEntity implements Entity {
+
+ protected final Node node;
+ protected final XrExtensions extensions;
+ protected final ScheduledExecutorService executor;
+ // Visible for testing
+ final ConcurrentHashMap<InputEventListener, Executor> inputEventListenerMap =
+ new ConcurrentHashMap<>();
+ Optional<InputEventListener> pointerCaptureInputEventListener = Optional.empty();
+ Optional<Executor> pointerCaptureExecutor = Optional.empty();
+ final ConcurrentHashMap<Consumer<ReformEvent>, Executor> reformEventConsumerMap =
+ new ConcurrentHashMap<>();
+ private final EntityManager entityManager;
+ private ReformOptions reformOptions;
+
+ AndroidXrEntity(
+ Node node,
+ XrExtensions extensions,
+ EntityManager entityManager,
+ ScheduledExecutorService executor) {
+ this.node = node;
+ this.extensions = extensions;
+ this.entityManager = entityManager;
+ this.executor = executor;
+ entityManager.setEntityForNode(node, this);
+ }
+
+ @Override
+ public void setPose(Pose pose) {
+ // TODO: b/321268237 - Minimize the number of node transactions
+ super.setPose(pose);
+
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction
+ .setPosition(
+ node,
+ pose.getTranslation().getX(),
+ pose.getTranslation().getY(),
+ pose.getTranslation().getZ())
+ .setOrientation(
+ node,
+ pose.getRotation().getX(),
+ pose.getRotation().getY(),
+ pose.getRotation().getZ(),
+ pose.getRotation().getW())
+ .apply();
+ }
+ }
+
+ @Override
+ public void setScale(Vector3 scale) {
+ super.setScale(scale);
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction.setScale(node, scale.getX(), scale.getY(), scale.getZ()).apply();
+ }
+ }
+
+ /** Returns the pose for this entity, relative to the activity space root. */
+ @Override
+ public Pose getPoseInActivitySpace() {
+ // TODO: b/355680575 - Revisit if we need to account for parent rotation when calculating
+ // the
+ // scale. This code might produce unexpected results when non-uniform scale is involved in
+ // the
+ // parent-child entity hierarchy.
+
+ // Any parentless "space" entities (such as the root and anchor entities) are expected to
+ // override this method non-recursively so that this error is never thrown.
+ if (!(getParent() instanceof AndroidXrEntity)) {
+ throw new IllegalStateException(
+ "Cannot get pose in Activity Space with a non-AndroidXrEntity parent");
+ }
+ AndroidXrEntity xrParent = (AndroidXrEntity) getParent();
+ return xrParent.getPoseInActivitySpace()
+ .compose(
+ new Pose(
+ getPose().getTranslation().times(xrParent.getWorldSpaceScale()),
+ getPose().getRotation()));
+ }
+
+ /**
+ * This method should be called when the ActivitySpace's underlying base space has been updated.
+ * For example, this method should be called when the task node moves in XrExtensions.
+ */
+ public void onActivitySpaceUpdated() {
+ // Defaults to a no-op.
+ }
+
+ // Returns the underlying extension Node for the Entity.
+ public Node getNode() {
+ return node;
+ }
+
+ @Override
+ public void setParent(Entity parent) {
+ if ((parent != null) && !(parent instanceof AndroidXrEntity)) {
+ Log.e(
+ "RealityCoreRuntime",
+ "Cannot set non-AndroidXrEntity as a parent of a AndroidXrEntity");
+ return;
+ }
+ super.setParent(parent);
+
+ AndroidXrEntity xrParent = (AndroidXrEntity) parent;
+
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ if (xrParent == null) {
+ transaction.setVisibility(node, false).setParent(node, null);
+ } else {
+ transaction.setParent(node, xrParent.getNode());
+ }
+ transaction.apply();
+ }
+ }
+
+ @Override
+ public void setSize(Dimensions dimensions) {
+ // TODO: b/326479171: Uncomment when extensions implement setSize.
+ // try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ // transaction.setSize(node, dimensions.width, dimensions.height,
+ // dimensions.depth).apply();
+ // }
+ }
+
+ @Override
+ public void setAlpha(float alpha) {
+ super.setAlpha(alpha);
+
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction.setAlpha(node, alpha).apply();
+ }
+ }
+
+ @Override
+ public void setHidden(boolean hidden) {
+ super.setHidden(hidden);
+
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ if (reformOptions != null) {
+ if (hidden) {
+ // Since this entity is being hidden, disable reform and the highlights around
+ // the node.
+ transaction.disableReform(node);
+ } else {
+ // Enables reform and the highlights around the node.
+ transaction.enableReform(node, reformOptions);
+ }
+ }
+ transaction.setVisibility(node, !hidden).apply();
+ }
+ }
+
+ @Override
+ public void addInputEventListener(Executor executor, InputEventListener eventListener) {
+ maybeSetupInputListeners();
+ inputEventListenerMap.put(eventListener, executor == null ? this.executor : executor);
+ }
+
+ /**
+ * Request pointer capture for this Entity, using the given interfaces to propagate state and
+ * captured input.
+ *
+ * <p>Returns true if a new pointer capture session was requested. Returns false if there is
+ * already a previously existing pointer capture session as only one can be supported at a given
+ * time.
+ */
+ public boolean requestPointerCapture(
+ Executor executor,
+ InputEventListener eventListener,
+ PointerCaptureComponent.StateListener stateListener) {
+ if (pointerCaptureInputEventListener.isPresent()) {
+ return false;
+ }
+ getNode()
+ .requestPointerCapture(
+ (pcState) -> {
+ if (pcState == Node.POINTER_CAPTURE_STATE_PAUSED) {
+ stateListener.onStateChanged(
+ PointerCaptureComponent.POINTER_CAPTURE_STATE_PAUSED);
+ } else if (pcState == Node.POINTER_CAPTURE_STATE_ACTIVE) {
+ stateListener.onStateChanged(
+ PointerCaptureComponent.POINTER_CAPTURE_STATE_ACTIVE);
+ } else if (pcState == Node.POINTER_CAPTURE_STATE_STOPPED) {
+ stateListener.onStateChanged(
+ PointerCaptureComponent.POINTER_CAPTURE_STATE_STOPPED);
+ } else {
+ Log.e("Runtime", "Invalid state received for pointer capture");
+ }
+ },
+ executor);
+
+ addPointerCaptureInputListener(executor, eventListener);
+ return true;
+ }
+
+ private void addPointerCaptureInputListener(
+ Executor executor, InputEventListener eventListener) {
+ maybeSetupInputListeners();
+ pointerCaptureInputEventListener = Optional.of(eventListener);
+ pointerCaptureExecutor = Optional.ofNullable(executor);
+ }
+
+ private void maybeSetupInputListeners() {
+ if (inputEventListenerMap.isEmpty() && pointerCaptureInputEventListener.isEmpty()) {
+ node.listenForInput(
+ (xrInputEvent) -> {
+ if (xrInputEvent.getDispatchFlags()
+ == InputEvent.DISPATCH_FLAG_CAPTURED_POINTER) {
+ pointerCaptureInputEventListener.ifPresent(
+ (listener) ->
+ pointerCaptureExecutor
+ .orElse(this.executor)
+ .execute(
+ () ->
+ listener.onInputEvent(
+ RuntimeUtils
+ .getInputEvent(
+ xrInputEvent,
+ entityManager))));
+ } else {
+ inputEventListenerMap.forEach(
+ (inputEventListener, listenerExecutor) ->
+ listenerExecutor.execute(
+ () ->
+ inputEventListener.onInputEvent(
+ RuntimeUtils.getInputEvent(
+ xrInputEvent,
+ entityManager))));
+ }
+ },
+ this.executor);
+ }
+ }
+
+ @Override
+ public void removeInputEventListener(InputEventListener consumer) {
+ inputEventListenerMap.remove(consumer);
+ maybeStopListeningForInput();
+ }
+
+ /** Stop any pointer capture requests on this Entity. */
+ public void stopPointerCapture() {
+ getNode().stopPointerCapture();
+ pointerCaptureInputEventListener = Optional.empty();
+ pointerCaptureExecutor = Optional.empty();
+ maybeStopListeningForInput();
+ }
+
+ private void maybeStopListeningForInput() {
+ if (inputEventListenerMap.isEmpty() && pointerCaptureInputEventListener.isEmpty()) {
+ node.stopListeningForInput();
+ }
+ }
+
+ @Override
+ public void dispose() {
+ inputEventListenerMap.clear();
+ node.stopListeningForInput();
+ reformEventConsumerMap.clear();
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction.disableReform(node);
+ }
+
+ // SystemSpaceEntityImpls (Anchors, ActivitySpace, etc) should have null parents.
+ if (getParent() != null) {
+ setParent(null);
+ }
+ entityManager.removeEntityForNode(node);
+ super.dispose();
+ }
+
+ /**
+ * Gets the reform options for this entity.
+ *
+ * @return The reform options for this entity.
+ */
+ public ReformOptions getReformOptions() {
+ if (reformOptions == null) {
+ Consumer<ReformEvent> reformEventConsumer =
+ reformEvent -> {
+ if ((reformOptions.getEnabledReform() & ReformOptions.ALLOW_MOVE) != 0
+ && (reformOptions.getFlags()
+ & ReformOptions.FLAG_ALLOW_SYSTEM_MOVEMENT)
+ != 0) {
+ // Update the cached pose of the entity.
+ super.setPose(
+ new Pose(
+ new Vector3(
+ reformEvent.getProposedPosition().x,
+ reformEvent.getProposedPosition().y,
+ reformEvent.getProposedPosition().z),
+ new Quaternion(
+ reformEvent.getProposedOrientation().x,
+ reformEvent.getProposedOrientation().y,
+ reformEvent.getProposedOrientation().z,
+ reformEvent.getProposedOrientation().w)));
+ // Update the cached scale of the entity.
+ super.setScaleInternal(
+ new Vector3(
+ reformEvent.getProposedScale().x,
+ reformEvent.getProposedScale().y,
+ reformEvent.getProposedScale().z));
+ }
+ reformEventConsumerMap.forEach(
+ (eventConsumer, consumerExecutor) ->
+ consumerExecutor.execute(
+ () -> eventConsumer.accept(reformEvent)));
+ };
+ reformOptions = extensions.createReformOptions(reformEventConsumer, executor);
+ }
+ return reformOptions;
+ }
+
+ /**
+ * Updates the reform options for this entity. Uses the same instance of [ReformOptions]
+ * provided by {@link #getReformOptions()}.
+ */
+ public synchronized void updateReformOptions() {
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ if (reformOptions.getEnabledReform() == 0) {
+ // Disables reform and the highlights around the node.
+ transaction.disableReform(node);
+ } else {
+ // Enables reform and the highlights around the node.
+ transaction.enableReform(node, reformOptions);
+ }
+ transaction.apply();
+ }
+ }
+
+ public void addReformEventConsumer(
+ Consumer<ReformEvent> reformEventConsumer, Executor executor) {
+ executor = (executor == null) ? this.executor : executor;
+ reformEventConsumerMap.put(reformEventConsumer, executor);
+ }
+
+ public void removeReformEventConsumer(Consumer<ReformEvent> reformEventConsumer) {
+ reformEventConsumerMap.remove(reformEventConsumer);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/AudioTrackExtensionsWrapperImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/AudioTrackExtensionsWrapperImpl.java
new file mode 100644
index 0000000..6e07e54
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/AudioTrackExtensionsWrapperImpl.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.media.AudioTrack;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.xr.extensions.media.AudioTrackExtensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.AudioTrackExtensionsWrapper;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.PointSourceAttributes;
+import androidx.xr.scenecore.JxrPlatformAdapter.SoundFieldAttributes;
+
+/** Implementation of the {@link AudioTrackExtensionsWrapper} */
+final class AudioTrackExtensionsWrapperImpl implements AudioTrackExtensionsWrapper {
+
+ private final AudioTrackExtensions extensions;
+
+ private final EntityManager entityManager;
+
+ AudioTrackExtensionsWrapperImpl(AudioTrackExtensions extensions, EntityManager entityManager) {
+ this.extensions = extensions;
+ this.entityManager = entityManager;
+ }
+
+ @Nullable
+ @Override
+ public PointSourceAttributes getPointSourceAttributes(@NonNull AudioTrack audioTrack) {
+ androidx.xr.extensions.media.PointSourceAttributes extAttributes =
+ extensions.getPointSourceAttributes(audioTrack);
+
+ if (extAttributes == null) {
+ return null;
+ }
+
+ Entity entity = entityManager.getEntityForNode(extAttributes.getNode());
+
+ if (entity == null) {
+ return null;
+ }
+
+ return new PointSourceAttributes(entity);
+ }
+
+ @Nullable
+ @Override
+ public SoundFieldAttributes getSoundFieldAttributes(@NonNull AudioTrack audioTrack) {
+ androidx.xr.extensions.media.SoundFieldAttributes extAttributes =
+ extensions.getSoundFieldAttributes(audioTrack);
+
+ if (extAttributes == null) {
+ return null;
+ }
+
+ return new SoundFieldAttributes(extAttributes.getAmbisonicsOrder());
+ }
+
+ @Override
+ public int getSpatialSourceType(@NonNull AudioTrack audioTrack) {
+ return extensions.getSpatialSourceType(audioTrack);
+ }
+
+ @Override
+ @NonNull
+ public AudioTrack.Builder setPointSourceAttributes(
+ @NonNull AudioTrack.Builder builder, @NonNull PointSourceAttributes attributes) {
+ androidx.xr.extensions.media.PointSourceAttributes extAttributes =
+ MediaUtils.convertPointSourceAttributesToExtensions(attributes);
+
+ return extensions.setPointSourceAttributes(builder, extAttributes);
+ }
+
+ @Override
+ @NonNull
+ public AudioTrack.Builder setSoundFieldAttributes(
+ @NonNull AudioTrack.Builder builder, @NonNull SoundFieldAttributes attributes) {
+ androidx.xr.extensions.media.SoundFieldAttributes extAttributes =
+ MediaUtils.convertSoundFieldAttributesToExtensions(attributes);
+
+ return extensions.setSoundFieldAttributes(builder, extAttributes);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/BasePanelEntity.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/BasePanelEntity.java
new file mode 100644
index 0000000..38da5e0
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/BasePanelEntity.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.content.res.Resources;
+
+import androidx.xr.extensions.XrExtensions;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.extensions.node.NodeTransaction;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.PanelEntity;
+import androidx.xr.scenecore.JxrPlatformAdapter.PixelDimensions;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+/** BasePanelEntity provides implementations of capabilities common to PanelEntities. */
+@SuppressWarnings("deprecation") // TODO(b/373435470): Remove
+abstract class BasePanelEntity extends AndroidXrEntity implements PanelEntity {
+ protected PixelDimensions pixelDimensions;
+ private float cornerRadius;
+
+ BasePanelEntity(
+ Node node,
+ XrExtensions extensions,
+ EntityManager entityManager,
+ ScheduledExecutorService executor) {
+ super(node, extensions, entityManager, executor);
+ }
+
+ private float getDefaultPixelDensity() {
+ return extensions
+ .getConfig()
+ .defaultPixelsPerMeter(Resources.getSystem().getDisplayMetrics().density);
+ }
+
+ @Override
+ public Vector3 getPixelDensity() {
+ Vector3 scale = getWorldSpaceScale();
+ float defaultPixelDensity = getDefaultPixelDensity();
+ return new Vector3(
+ defaultPixelDensity / scale.getX(),
+ defaultPixelDensity / scale.getY(),
+ defaultPixelDensity / scale.getZ());
+ }
+
+ @Override
+ public Dimensions getSize() {
+ Vector3 pixelDensity = getPixelDensity();
+ return new Dimensions(
+ pixelDimensions.width / pixelDensity.getX(),
+ pixelDimensions.height / pixelDensity.getY(),
+ 0);
+ }
+
+ @Override
+ public void setSize(Dimensions dimensions) {
+ // TODO(b/352630025): remove this method.
+ setPixelDimensions(new PixelDimensions((int) dimensions.width, (int) dimensions.height));
+ }
+
+ @Override
+ public PixelDimensions getPixelDimensions() {
+ return pixelDimensions;
+ }
+
+ @Override
+ public void setPixelDimensions(PixelDimensions dimensions) {
+ pixelDimensions = dimensions;
+ }
+
+ @Override
+ public void setCornerRadius(float value) {
+ if (value < 0.0f) {
+ throw new IllegalArgumentException("Corner radius can't be negative: " + value);
+ }
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction.setCornerRadius(node, value).apply();
+ cornerRadius = value;
+ }
+ }
+
+ @Override
+ public float getCornerRadius() {
+ return cornerRadius;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/CameraViewActivityPoseImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/CameraViewActivityPoseImpl.java
new file mode 100644
index 0000000..cabab63
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/CameraViewActivityPoseImpl.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter.CameraViewActivityPose;
+import androidx.xr.scenecore.common.BaseActivityPose;
+import androidx.xr.scenecore.impl.perception.PerceptionLibrary;
+import androidx.xr.scenecore.impl.perception.Session;
+import androidx.xr.scenecore.impl.perception.ViewProjection;
+import androidx.xr.scenecore.impl.perception.ViewProjections;
+
+/**
+ * A ActivityPose representing a user's camera. This can be used to determine the location and field
+ * of view of the camera.
+ */
+final class CameraViewActivityPoseImpl extends BaseActivityPose implements CameraViewActivityPose {
+ private static final String TAG = "CameraViewActivityPose";
+ private final PerceptionLibrary perceptionLibrary;
+ @CameraType private final int cameraType;
+ private final OpenXrActivityPoseHelper openXrActivityPoseHelper;
+ // Default the pose to null. A null pose indicates that the camera is not ready yet.
+ private Pose lastOpenXrPose = null;
+
+ public CameraViewActivityPoseImpl(
+ @CameraType int cameraType,
+ ActivitySpaceImpl activitySpace,
+ AndroidXrEntity activitySpaceRoot,
+ PerceptionLibrary perceptionLibrary) {
+ this.cameraType = cameraType;
+ this.perceptionLibrary = perceptionLibrary;
+ this.openXrActivityPoseHelper =
+ new OpenXrActivityPoseHelper(activitySpace, activitySpaceRoot);
+ }
+
+ @Override
+ public Pose getPoseInActivitySpace() {
+ return openXrActivityPoseHelper.getPoseInActivitySpace(getPoseInOpenXrReferenceSpace());
+ }
+
+ @Override
+ public Pose getActivitySpacePose() {
+ return openXrActivityPoseHelper.getActivitySpacePose(getPoseInOpenXrReferenceSpace());
+ }
+
+ @Override
+ public Vector3 getActivitySpaceScale() {
+ // This WorldPose is assumed to always have a scale of 1.0f in the OpenXR reference space.
+ return openXrActivityPoseHelper.getActivitySpaceScale(new Vector3(1f, 1f, 1f));
+ }
+
+ @Nullable
+ private ViewProjection getViewProjection() {
+ final Session session = perceptionLibrary.getSession();
+ if (session == null) {
+ Log.w(TAG, "Cannot retrieve the camera pose with a null perception session.");
+ return null;
+ }
+ ViewProjections perceptionViews = session.getStereoViews();
+ if (perceptionViews == null) {
+ Log.e(TAG, "Error retrieving the camera.");
+ return null;
+ }
+ if (cameraType == CameraViewActivityPose.CAMERA_TYPE_LEFT_EYE) {
+ return perceptionViews.getLeftEye();
+ } else if (cameraType == CameraViewActivityPose.CAMERA_TYPE_RIGHT_EYE) {
+ return perceptionViews.getRightEye();
+ } else {
+ Log.w(TAG, "Unsupported camera type: " + cameraType);
+ return null;
+ }
+ }
+
+ /** Gets the pose in the OpenXR reference space. Can be null if it is not yet ready. */
+ @Nullable
+ public Pose getPoseInOpenXrReferenceSpace() {
+ ViewProjection viewProjection = getViewProjection();
+ if (viewProjection != null) {
+ lastOpenXrPose = RuntimeUtils.fromPerceptionPose(viewProjection.getPose());
+ }
+ return lastOpenXrPose;
+ }
+
+ @Override
+ @CameraType
+ public int getCameraType() {
+ return cameraType;
+ }
+
+ @Override
+ public Fov getFov() {
+ ViewProjection viewProjection = getViewProjection();
+ if (viewProjection == null) {
+ return new Fov(0, 0, 0, 0);
+ }
+ return RuntimeUtils.fovFromPerceptionFov(viewProjection.getFov());
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/EntityManager.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/EntityManager.java
new file mode 100644
index 0000000..4bb5465
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/EntityManager.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static java.util.stream.Collectors.toCollection;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Manages the mapping between {@link Node} and {@link Entity} for a given {@link
+ * JxrPlatformAdapterAxr}.
+ */
+@SuppressWarnings("BanConcurrentHashMap")
+final class EntityManager {
+ private final Map<Node, Entity> nodeEntityMap = new ConcurrentHashMap<>();
+
+ /**
+ * Returns the {@link Entity} associated with the given {@link Node}.
+ *
+ * @param node the {@link Node} to get the associated {@link Entity} for.
+ * @return the {@link Entity} associated with the given {@link Node}, or null if no such {@link
+ * Entity} exists.
+ */
+ @Nullable
+ Entity getEntityForNode(@NonNull Node node) {
+ return nodeEntityMap.get(node);
+ }
+
+ /**
+ * Sets the {@link Entity} associated with the given {@link Node}.
+ *
+ * @param node the {@link Node} to set the associated {@link Entity} for.
+ * @param entity the {@link Entity} to associate with the given {@link Node}.
+ */
+ void setEntityForNode(@NonNull Node node, @NonNull Entity entity) {
+ nodeEntityMap.put(node, entity);
+ }
+
+ /**
+ * Returns a list of all {@link Entity}s of type {@code T} (including subtypes of {@code T}).
+ *
+ * @param entityClass the type of {@link Entity} to return.
+ * @return a list of all {@link Entity}s of type {@code T} (including subtypes of {@code T}).
+ */
+ <T extends Entity> List<T> getEntitiesOfType(@NonNull Class<T> entityClass) {
+ return nodeEntityMap.values().stream()
+ .filter(entityClass::isInstance)
+ .map(entityClass::cast)
+ .collect(toCollection(ArrayList::new));
+ }
+
+ /** Returns a collection of all {@link Entity}s. */
+ Collection<Entity> getAllEntities() {
+ return nodeEntityMap.values();
+ }
+
+ /** Removes the given {@link Node} from the map. */
+ void removeEntityForNode(@NonNull Node node) {
+ nodeEntityMap.remove(node);
+ }
+
+ /** Clears the EntityManager. */
+ void clear() {
+ nodeEntityMap.clear();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/ExrImageResourceImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/ExrImageResourceImpl.java
new file mode 100644
index 0000000..c011afa
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/ExrImageResourceImpl.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import androidx.xr.scenecore.JxrPlatformAdapter.ExrImageResource;
+
+/**
+ * Implementation of a RealityCore ExrImageResource.
+ *
+ * <p>EXR Images are high dynamic range images that can be used as environmental skyboxes, and can
+ * be used for Image Based Lighting.
+ */
+@SuppressWarnings({"deprecation", "UnnecessarilyFullyQualified"}) // TODO(b/373435470): Remove
+final class ExrImageResourceImpl implements ExrImageResource {
+ // Note: right now the "environment" format accessible through the XRExtensions layer is .EXR
+ private final androidx.xr.extensions.asset.EnvironmentToken token;
+
+ public ExrImageResourceImpl(androidx.xr.extensions.asset.EnvironmentToken token) {
+ this.token = token;
+ }
+
+ public androidx.xr.extensions.asset.EnvironmentToken getToken() {
+ return token;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/GltfEntityImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/GltfEntityImpl.java
new file mode 100644
index 0000000..622b2e7
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/GltfEntityImpl.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.xr.extensions.XrExtensions;
+import androidx.xr.extensions.node.NodeTransaction;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.GltfEntity;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * Implementation of a RealityCore GltfEntity.
+ *
+ * <p>This is used to create an entity that contains a glTF object.
+ */
+// TODO: b/321782625 - Add tests when the Extensions can be faked.
+@SuppressWarnings("deprecation") // TODO(b/373435470): Remove
+final class GltfEntityImpl extends AndroidXrEntity implements GltfEntity {
+ public GltfEntityImpl(
+ GltfModelResourceImpl gltfModelResource,
+ Entity parentEntity,
+ XrExtensions extensions,
+ EntityManager entityManager,
+ ScheduledExecutorService executor) {
+ super(extensions.createNode(), extensions, entityManager, executor);
+ setParent(parentEntity);
+
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction.setGltfModel(node, gltfModelResource.getExtensionModelToken()).apply();
+ }
+ }
+
+ @Override
+ public void startAnimation(boolean looping, @Nullable String animationName) {
+ // Implement this for the non-Split Engine path or ignore until the Split
+ // Engine path becomes the default.
+ Log.w("GltfEntityImpl: ", "GLTF Animation is only supported when using SplitEngine.");
+ }
+
+ @Override
+ public void stopAnimation() {
+ // Implement this for the non-Split Engine path or ignore until the Split
+ // Engine path becomes the default.
+ Log.w("GltfEntityImpl: ", "GLTF Animation is only supported when using SplitEngine.");
+ }
+
+ @Override
+ @AnimationState
+ public int getAnimationState() {
+ // Implement this for the non-Split Engine path or ignore until the Split
+ // Engine path becomes the default.
+ Log.w("GltfEntityImpl: ", "GLTF Animation is only supported when using SplitEngine.");
+ return AnimationState.STOPPED;
+ }
+
+ @SuppressWarnings("ObjectToString")
+ @Override
+ public void dispose() {
+ Log.i("GltfEntityImpl", "Disposing " + this);
+ super.dispose();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/GltfEntityImplSplitEngine.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/GltfEntityImplSplitEngine.java
new file mode 100644
index 0000000..242c3c7
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/GltfEntityImplSplitEngine.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.xr.extensions.XrExtensions;
+import androidx.xr.extensions.node.NodeTransaction;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.GltfEntity;
+
+import com.google.androidxr.splitengine.SplitEngineSubspaceManager;
+import com.google.androidxr.splitengine.SubspaceNode;
+import com.google.ar.imp.apibindings.ImpressApi;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * Implementation of a RealityCore GltfEntitySplitEngine.
+ *
+ * <p>This is used to create an entity that contains a glTF object using the Split Engine route.
+ */
+// TODO: b/375520647 - Add unit tests for this class.
+class GltfEntityImplSplitEngine extends AndroidXrEntity implements GltfEntity {
+ private final ImpressApi impressApi;
+ private final SplitEngineSubspaceManager splitEngineSubspaceManager;
+ private final SubspaceNode subspace;
+ private final int modelImpressNode;
+ private final int subspaceImpressNode;
+ @AnimationState private int animationState = AnimationState.STOPPED;
+
+ public GltfEntityImplSplitEngine(
+ GltfModelResourceImplSplitEngine gltfModelResource,
+ Entity parentEntity,
+ ImpressApi impressApi,
+ SplitEngineSubspaceManager splitEngineSubspaceManager,
+ XrExtensions extensions,
+ EntityManager entityManager,
+ ScheduledExecutorService executor) {
+ super(extensions.createNode(), extensions, entityManager, executor);
+ this.impressApi = impressApi;
+ this.splitEngineSubspaceManager = splitEngineSubspaceManager;
+ setParent(parentEntity);
+
+ // TODO(b/377907379): - Punt this logic to the UI thread, so that applications can create
+ // Gltf entities from any thread.
+
+ // System will only render Impress nodes that are parented by this subspace node.
+ this.subspaceImpressNode = impressApi.createImpressNode();
+ String subspaceName = "gltf_entity_subspace_" + subspaceImpressNode;
+
+ this.subspace =
+ splitEngineSubspaceManager.createSubspace(subspaceName, subspaceImpressNode);
+
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ // Make the Entity node a parent of the subspace node.
+ transaction.setParent(subspace.subspaceNode, this.node).apply();
+ }
+ this.modelImpressNode =
+ impressApi.instanceGltfModel(gltfModelResource.getExtensionModelToken());
+ impressApi.setImpressNodeParent(modelImpressNode, subspaceImpressNode);
+ // The Impress node hierarchy is: Subspace Impress node --- parent of ---> model Impress
+ // node.
+ // The CPM node hierarchy is: Entity CPM node --- parent of ---> Subspace CPM node.
+ }
+
+ @Override
+ public void startAnimation(boolean looping, @Nullable String animationName) {
+ // TODO: b/362826747 - Add a listener interface so that the application can be
+ // notified that the animation has stopped, been cancelled (by starting another animation)
+ // and / or shown an error state if something went wrong.
+
+ // TODO(b/377907379): - Punt this logic to the UI thread.
+
+ // Note that at the moment this future will be garbage collected, since we don't return it
+ // from
+ // this method.
+ ListenableFuture<Void> future =
+ impressApi.animateGltfModel(modelImpressNode, animationName, looping);
+ animationState = AnimationState.PLAYING;
+
+ // At the moment, we don't do anything interesting on failure except for logging. If we
+ // didn't
+ // care about logging the failure, we could just not register a listener at all if the
+ // animation
+ // is looping, since it will never terminate normally.
+ future.addListener(
+ () -> {
+ try {
+ future.get();
+ // The animation played to completion and has stopped
+ animationState = AnimationState.STOPPED;
+ } catch (Exception e) {
+ if (e instanceof InterruptedException) {
+ // If this happened, then it's likely Impress is shutting down and we
+ // need to
+ // shut down as well.
+ Thread.currentThread().interrupt();
+ } else {
+ // Some other error happened. Log it and stop the animation.
+ Log.e("GltfEntityImpl", "Could not start animation: " + e);
+ animationState = AnimationState.STOPPED;
+ }
+ }
+ },
+ this.executor);
+ }
+
+ @Override
+ public void stopAnimation() {
+ // TODO(b/377907379): - Punt this logic to the UI thread.
+ impressApi.stopGltfModelAnimation(modelImpressNode);
+ animationState = AnimationState.STOPPED;
+ }
+
+ @Override
+ @AnimationState
+ public int getAnimationState() {
+ return animationState;
+ }
+
+ @SuppressWarnings("ObjectToString")
+ @Override
+ public void dispose() {
+ // TODO(b/377907379): - Punt this logic to the UI thread.
+ splitEngineSubspaceManager.deleteSubspace(subspace.subspaceId);
+ impressApi.destroyImpressNode(modelImpressNode);
+ impressApi.destroyImpressNode(subspaceImpressNode);
+ super.dispose();
+ }
+
+ public void setColliderEnabled(boolean enableCollider) {
+ // TODO(b/377907379): - Punt this logic to the UI thread
+ impressApi.setGltfModelColliderEnabled(modelImpressNode, enableCollider);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/GltfModelResourceImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/GltfModelResourceImpl.java
new file mode 100644
index 0000000..2dca9f2
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/GltfModelResourceImpl.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import androidx.xr.scenecore.JxrPlatformAdapter.GltfModelResource;
+
+/**
+ * Implementation of a RealityCore GltfModelResource.
+ *
+ * <p>This is used to create to load a glTF that can later be used when creating a GltfEntity.
+ */
+@SuppressWarnings({"deprecation", "UnnecessarilyFullyQualified"}) // TODO(b/373435470): Remove
+final class GltfModelResourceImpl implements GltfModelResource {
+ private final androidx.xr.extensions.asset.GltfModelToken token;
+
+ public GltfModelResourceImpl(androidx.xr.extensions.asset.GltfModelToken token) {
+ this.token = token;
+ }
+
+ public androidx.xr.extensions.asset.GltfModelToken getExtensionModelToken() {
+ return token;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/GltfModelResourceImplSplitEngine.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/GltfModelResourceImplSplitEngine.java
new file mode 100644
index 0000000..13681ef
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/GltfModelResourceImplSplitEngine.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import androidx.xr.scenecore.JxrPlatformAdapter.GltfModelResource;
+
+/**
+ * Implementation of a RealityCore GltfModelResource for the Split Engine.
+ *
+ * <p>This is used to create to load a glTF that can later be used when creating a
+ * GltfEntitySplitEngine.
+ */
+// TODO: b/362368652 - Add an interface which returns an integer animation IDX given a string
+// animation name for a loaded GLTF.
+final class GltfModelResourceImplSplitEngine implements GltfModelResource {
+ private final long token;
+
+ public GltfModelResourceImplSplitEngine(long token) {
+ this.token = token;
+ }
+
+ public long getExtensionModelToken() {
+ return token;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/HeadActivityPoseImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/HeadActivityPoseImpl.java
new file mode 100644
index 0000000..d1b9b59
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/HeadActivityPoseImpl.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import androidx.annotation.Nullable;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter.HeadActivityPose;
+import androidx.xr.scenecore.common.BaseActivityPose;
+import androidx.xr.scenecore.impl.perception.PerceptionLibrary;
+import androidx.xr.scenecore.impl.perception.Session;
+
+/**
+ * An ActivityPose representing the head of the user. This can be used to determine the location of
+ * the user's head.
+ */
+class HeadActivityPoseImpl extends BaseActivityPose implements HeadActivityPose {
+ private final PerceptionLibrary perceptionLibrary;
+ private final OpenXrActivityPoseHelper openXrActivityPoseHelper;
+ // Default the pose to null. A null pose indicates that the head is not ready yet.
+ private Pose lastOpenXrPose = null;
+
+ public HeadActivityPoseImpl(
+ ActivitySpaceImpl activitySpace,
+ AndroidXrEntity activitySpaceRoot,
+ PerceptionLibrary perceptionLibrary) {
+ this.perceptionLibrary = perceptionLibrary;
+ this.openXrActivityPoseHelper =
+ new OpenXrActivityPoseHelper(activitySpace, activitySpaceRoot);
+ }
+
+ @Override
+ public Pose getPoseInActivitySpace() {
+ return openXrActivityPoseHelper.getPoseInActivitySpace(getPoseInOpenXrReferenceSpace());
+ }
+
+ @Override
+ public Pose getActivitySpacePose() {
+ return openXrActivityPoseHelper.getActivitySpacePose(getPoseInOpenXrReferenceSpace());
+ }
+
+ @Override
+ public Vector3 getActivitySpaceScale() {
+ // This WorldPose is assumed to always have a scale of 1.0f in the OpenXR reference space.
+ return openXrActivityPoseHelper.getActivitySpaceScale(new Vector3(1f, 1f, 1f));
+ }
+
+ /** Gets the pose in the OpenXR reference space. Can be null if it is not yet ready. */
+ @Nullable
+ public Pose getPoseInOpenXrReferenceSpace() {
+ final Session session = perceptionLibrary.getSession();
+ if (session == null) {
+ return lastOpenXrPose;
+ }
+ androidx.xr.scenecore.impl.perception.Pose perceptionHeadPose = session.getHeadPose();
+ if (perceptionHeadPose != null) {
+ lastOpenXrPose = RuntimeUtils.fromPerceptionPose(perceptionHeadPose);
+ }
+ return lastOpenXrPose;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/InteractableComponentImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/InteractableComponentImpl.java
new file mode 100644
index 0000000..f2d5118
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/InteractableComponentImpl.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.util.Log;
+
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.InputEventListener;
+import androidx.xr.scenecore.JxrPlatformAdapter.InteractableComponent;
+
+import java.util.concurrent.Executor;
+
+/** Implementation of [JxrPlatformAdapter.InteractableComponent]. */
+class InteractableComponentImpl implements InteractableComponent {
+ final InputEventListener consumer;
+ final Executor executor;
+ Entity entity;
+
+ InteractableComponentImpl(Executor executor, InputEventListener consumer) {
+ this.consumer = consumer;
+ this.executor = executor;
+ }
+
+ @Override
+ public boolean onAttach(Entity entity) {
+ if (this.entity != null) {
+ Log.e("Runtime", "Already attached to entity " + this.entity);
+ return false;
+ }
+ this.entity = entity;
+ if (entity instanceof GltfEntityImplSplitEngine) {
+ ((GltfEntityImplSplitEngine) entity).setColliderEnabled(true);
+ }
+ // InputEvent type translation happens here.
+ entity.addInputEventListener(executor, consumer);
+ return true;
+ }
+
+ @Override
+ public void onDetach(Entity entity) {
+ if (entity instanceof GltfEntityImplSplitEngine) {
+ ((GltfEntityImplSplitEngine) entity).setColliderEnabled(false);
+ }
+ entity.removeInputEventListener(consumer);
+ this.entity = null;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxr.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxr.java
new file mode 100644
index 0000000..47da4bd
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxr.java
@@ -0,0 +1,1174 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.SurfaceControlViewHost;
+import android.view.SurfaceControlViewHost.SurfacePackage;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.xr.arcore.Anchor;
+import androidx.xr.extensions.XrExtensions;
+import androidx.xr.extensions.XrExtensionsProvider;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.extensions.node.NodeTransaction;
+import androidx.xr.extensions.space.ActivityPanel;
+import androidx.xr.extensions.space.ActivityPanelLaunchParameters;
+import androidx.xr.extensions.space.SpatialState;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.scenecore.JxrPlatformAdapter;
+import androidx.xr.scenecore.impl.perception.PerceptionLibrary;
+import androidx.xr.scenecore.impl.perception.Session;
+import androidx.xr.scenecore.impl.perception.ViewProjections;
+
+import com.google.androidxr.splitengine.SplitEngineSubspaceManager;
+import com.google.ar.imp.apibindings.ImpressApi;
+import com.google.ar.imp.apibindings.ImpressApiImpl;
+import com.google.ar.imp.view.splitengine.ImpSplitEngine;
+import com.google.ar.imp.view.splitengine.ImpSplitEngineRenderer;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.InputStream;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/** Implementation of JxrPlatformAdapter for AndroidXR. */
+// TODO: b/322550407 - Use the Android Fluent Logger
+// TODO(b/373435470): Remove "deprecation" and "UnnecessarilyFullyQualified"
+@SuppressWarnings({
+ "deprecation",
+ "UnnecessarilyFullyQualified",
+ "BanSynchronizedMethods",
+ "BanConcurrentHashMap",
+})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class JxrPlatformAdapterAxr implements JxrPlatformAdapter {
+ private static final String TAG = "JxrPlatformAdapterAxr";
+ private static final String SPLIT_ENGINE_LIBRARY_NAME = "impress_api_jni";
+
+ private final ActivitySpaceImpl activitySpace;
+ private final HeadActivityPoseImpl headActivityPose;
+ private final PerceptionSpaceActivityPoseImpl perceptionSpaceActivityPose;
+ private final List<CameraViewActivityPoseImpl> cameraActivityPoses = new ArrayList<>();
+ private final ScheduledExecutorService executor;
+ private final XrExtensions extensions;
+
+ private final SoundPoolExtensionsWrapper soundPoolExtensionsWrapper;
+ private final AudioTrackExtensionsWrapper audioTrackExtensionsWrapper;
+ private final MediaPlayerExtensionsWrapper mediaPlayerExtensionsWrapper;
+
+ private final PerceptionLibrary perceptionLibrary;
+ private final SpatialEnvironmentImpl environment;
+ private final boolean useSplitEngine;
+ private final Node taskWindowLeashNode;
+ private final int openXrReferenceSpaceType;
+ private final EntityManager entityManager;
+ private final PanelEntity mainPanelEntity;
+ private final ImpressApi impressApi;
+ private final Map<Consumer<SpatialCapabilities>, Executor> spatialCapabilitiesChangedListeners =
+ new ConcurrentHashMap<>();
+
+ @Nullable private Activity activity;
+ private SplitEngineSubspaceManager splitEngineSubspaceManager;
+ private ImpSplitEngineRenderer splitEngineRenderer;
+ private boolean frameLoopStarted;
+
+ // TODO b/373481538: remove lazy initialization once XR Extensions bug is fixed. This will allow
+ // us to remove the lazySpatialStateProvider instance and pass the spatialState directly.
+ private final AtomicReference<SpatialState> spatialState = new AtomicReference<>(null);
+
+ // Returns the currently-known spatial state, or fetches it from the extensions if it has never
+ // been set. The spatial state is kept updated in the SpatialStateCallback.
+ private final Supplier<SpatialState> lazySpatialStateProvider;
+
+ private JxrPlatformAdapterAxr(
+ Activity activity,
+ ScheduledExecutorService executor,
+ XrExtensions extensions,
+ @Nullable ImpressApi impressApi,
+ EntityManager entityManager,
+ PerceptionLibrary perceptionLibrary,
+ @Nullable SplitEngineSubspaceManager subspaceManager,
+ @Nullable ImpSplitEngineRenderer renderer,
+ Node rootSceneNode,
+ Node taskWindowLeashNode,
+ boolean useSplitEngine) {
+ this.activity = activity;
+ this.executor = executor;
+ this.extensions = extensions;
+
+ this.lazySpatialStateProvider =
+ () ->
+ spatialState.updateAndGet(
+ oldSpatialState -> {
+ if (oldSpatialState == null) {
+ oldSpatialState = this.extensions.getSpatialState(activity);
+ }
+ return oldSpatialState;
+ });
+ setSpatialStateCallback();
+
+ soundPoolExtensionsWrapper =
+ new SoundPoolExtensionsWrapperImpl(
+ extensions.getXrSpatialAudioExtensions().getSoundPoolExtensions());
+ audioTrackExtensionsWrapper =
+ new AudioTrackExtensionsWrapperImpl(
+ extensions.getXrSpatialAudioExtensions().getAudioTrackExtensions(),
+ entityManager);
+ mediaPlayerExtensionsWrapper =
+ new MediaPlayerExtensionsWrapperImpl(
+ extensions.getXrSpatialAudioExtensions().getMediaPlayerExtensions());
+ this.entityManager = entityManager;
+ this.perceptionLibrary = perceptionLibrary;
+ this.taskWindowLeashNode = taskWindowLeashNode;
+ this.environment =
+ new SpatialEnvironmentImpl(
+ activity,
+ extensions,
+ rootSceneNode,
+ lazySpatialStateProvider,
+ useSplitEngine);
+ this.activitySpace =
+ new ActivitySpaceImpl(
+ rootSceneNode,
+ extensions,
+ entityManager,
+ lazySpatialStateProvider,
+ executor);
+ this.headActivityPose =
+ new HeadActivityPoseImpl(
+ activitySpace,
+ (AndroidXrEntity) getActivitySpaceRootImpl(),
+ perceptionLibrary);
+ this.perceptionSpaceActivityPose =
+ new PerceptionSpaceActivityPoseImpl(
+ activitySpace, (AndroidXrEntity) getActivitySpaceRootImpl());
+ this.cameraActivityPoses.add(
+ new CameraViewActivityPoseImpl(
+ CameraViewActivityPose.CAMERA_TYPE_LEFT_EYE,
+ activitySpace,
+ (AndroidXrEntity) getActivitySpaceRootImpl(),
+ perceptionLibrary));
+ this.cameraActivityPoses.add(
+ new CameraViewActivityPoseImpl(
+ CameraViewActivityPose.CAMERA_TYPE_RIGHT_EYE,
+ activitySpace,
+ (AndroidXrEntity) getActivitySpaceRootImpl(),
+ perceptionLibrary));
+ this.useSplitEngine = useSplitEngine;
+ this.openXrReferenceSpaceType = extensions.getOpenXrWorldSpaceType();
+
+ this.mainPanelEntity =
+ new MainPanelEntityImpl(
+ activity, taskWindowLeashNode, extensions, entityManager, executor);
+ this.mainPanelEntity.setParent(activitySpace);
+
+ // TODO:b/377918731 - Move this logic into factories and inject SE into the constructor
+ if (impressApi == null) {
+ // TODO: b/370116937) - Check against useSplitEngine as well and don't load this if
+ // SplitEngine is disabled.
+ this.impressApi = new ImpressApiImpl();
+ } else {
+ this.impressApi = impressApi;
+ }
+
+ if (useSplitEngine && subspaceManager == null && renderer == null) {
+ ImpSplitEngine.SplitEngineSetupParams impApiSetupParams =
+ new ImpSplitEngine.SplitEngineSetupParams();
+ impApiSetupParams.jniLibraryName = SPLIT_ENGINE_LIBRARY_NAME;
+ this.splitEngineRenderer = ImpSplitEngineRenderer.create(activity, impApiSetupParams);
+ startRenderer();
+ this.splitEngineSubspaceManager =
+ new SplitEngineSubspaceManager(
+ this.splitEngineRenderer,
+ rootSceneNode,
+ taskWindowLeashNode,
+ SPLIT_ENGINE_LIBRARY_NAME);
+ this.impressApi.setup(splitEngineRenderer.getView());
+ environment.onSplitEngineReady(this.splitEngineSubspaceManager, this.impressApi);
+ }
+ }
+
+ @NonNull
+ public static JxrPlatformAdapterAxr create(
+ @NonNull Activity activity, @NonNull ScheduledExecutorService executor) {
+ return create(
+ activity,
+ executor,
+ XrExtensionsProvider.getXrExtensions(),
+ null,
+ new EntityManager(),
+ new PerceptionLibrary(),
+ null,
+ null,
+ /* useSplitEngine= */ true);
+ }
+
+ @NonNull
+ public static JxrPlatformAdapterAxr create(
+ @NonNull Activity activity,
+ @NonNull ScheduledExecutorService executor,
+ boolean useSplitEngine) {
+ return create(
+ activity,
+ executor,
+ XrExtensionsProvider.getXrExtensions(),
+ null,
+ new EntityManager(),
+ new PerceptionLibrary(),
+ null,
+ null,
+ useSplitEngine);
+ }
+
+ @NonNull
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public static JxrPlatformAdapterAxr create(
+ @NonNull Activity activity,
+ @NonNull ScheduledExecutorService executor,
+ @NonNull Node rootSceneNode,
+ @NonNull Node taskWindowLeashNode) {
+ return create(
+ activity,
+ executor,
+ XrExtensionsProvider.getXrExtensions(),
+ null,
+ new EntityManager(),
+ new PerceptionLibrary(),
+ null,
+ null,
+ rootSceneNode,
+ taskWindowLeashNode,
+ /* useSplitEngine= */ false);
+ }
+
+ @NonNull
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public static JxrPlatformAdapterAxr create(
+ @NonNull Activity activity,
+ @NonNull ScheduledExecutorService executor,
+ @NonNull XrExtensions extensions,
+ @Nullable ImpressApi impressApi,
+ @NonNull PerceptionLibrary perceptionLibrary,
+ @Nullable SplitEngineSubspaceManager splitEngineSubspaceManager,
+ @Nullable ImpSplitEngineRenderer splitEngineRenderer) {
+ return create(
+ activity,
+ executor,
+ extensions,
+ impressApi,
+ new EntityManager(),
+ perceptionLibrary,
+ splitEngineSubspaceManager,
+ splitEngineRenderer,
+ /* useSplitEngine= */ false);
+ }
+
+ static JxrPlatformAdapterAxr create(
+ Activity activity,
+ ScheduledExecutorService executor,
+ XrExtensions extensions,
+ ImpressApi impressApi,
+ EntityManager entityManager,
+ PerceptionLibrary perceptionLibrary,
+ SplitEngineSubspaceManager splitEngineSubspaceManager,
+ ImpSplitEngineRenderer splitEngineRenderer,
+ boolean useSplitEngine) {
+ Node rootSceneNode = extensions.createNode();
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction.setName(rootSceneNode, "RootSceneNode").apply();
+ }
+ Log.i(TAG, "Impl Node for task $activity.taskId is root scene node: " + rootSceneNode);
+ Node taskWindowLeashNode = extensions.createNode();
+ // TODO: b/376934871 - Check async results.
+ extensions.attachSpatialScene(
+ activity, rootSceneNode, taskWindowLeashNode, (result) -> {}, Runnable::run);
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction
+ .setParent(taskWindowLeashNode, rootSceneNode)
+ .setName(taskWindowLeashNode, "TaskWindowLeashNode")
+ .apply();
+ }
+ return create(
+ activity,
+ executor,
+ extensions,
+ impressApi,
+ entityManager,
+ perceptionLibrary,
+ splitEngineSubspaceManager,
+ splitEngineRenderer,
+ rootSceneNode,
+ taskWindowLeashNode,
+ useSplitEngine);
+ }
+
+ static JxrPlatformAdapterAxr create(
+ @NonNull Activity activity,
+ @NonNull ScheduledExecutorService executor,
+ @NonNull XrExtensions extensions,
+ @Nullable ImpressApi impressApi,
+ @NonNull EntityManager entityManager,
+ @NonNull PerceptionLibrary perceptionLibrary,
+ @Nullable SplitEngineSubspaceManager splitEngineSubspaceManager,
+ @Nullable ImpSplitEngineRenderer splitEngineRenderer,
+ @NonNull Node rootSceneNode,
+ @NonNull Node taskWindowLeashNode,
+ boolean useSplitEngine) {
+ JxrPlatformAdapterAxr runtime =
+ new JxrPlatformAdapterAxr(
+ activity,
+ executor,
+ extensions,
+ impressApi,
+ entityManager,
+ perceptionLibrary,
+ splitEngineSubspaceManager,
+ splitEngineRenderer,
+ rootSceneNode,
+ taskWindowLeashNode,
+ useSplitEngine);
+
+ Log.i(TAG, "Initing perception library soon");
+ runtime.initPerceptionLibrary();
+ return runtime;
+ }
+
+ private static GltfModelResourceImpl getModelResourceFromToken(
+ androidx.xr.extensions.asset.GltfModelToken token) {
+ return new GltfModelResourceImpl(token);
+ }
+
+ private static GltfModelResourceImplSplitEngine getModelResourceFromTokenSplitEngine(
+ long token) {
+ return new GltfModelResourceImplSplitEngine(token);
+ }
+
+ private static ExrImageResourceImpl getExrImageResourceFromToken(
+ androidx.xr.extensions.asset.EnvironmentToken token) {
+ return new ExrImageResourceImpl(token);
+ }
+
+ // Note that this is called on the Activity's UI thread so we should be careful to not block
+ // it.
+ // It is synchronized because we assume this.spatialState cannot be updated elsewhere during the
+ // execution of this method.
+ @VisibleForTesting
+ synchronized void onSpatialStateChanged(
+ @NonNull androidx.xr.extensions.space.SpatialState newSpatialState) {
+ SpatialState previousSpatialState = this.spatialState.getAndSet(newSpatialState);
+
+ boolean spatialCapabilitiesChanged =
+ previousSpatialState == null
+ || !newSpatialState
+ .getSpatialCapabilities()
+ .equals(previousSpatialState.getSpatialCapabilities());
+
+ boolean hasBoundsChanged =
+ previousSpatialState == null
+ || !newSpatialState.getBounds().equals(previousSpatialState.getBounds());
+
+ EnumSet<SpatialEnvironmentImpl.ChangedSpatialStates> changedSpatialStates =
+ environment.setSpatialState(newSpatialState);
+ boolean environmentVisibilityChanged =
+ changedSpatialStates.contains(
+ SpatialEnvironmentImpl.ChangedSpatialStates.ENVIRONMENT_CHANGED);
+ boolean passthroughVisibilityChanged =
+ changedSpatialStates.contains(
+ SpatialEnvironmentImpl.ChangedSpatialStates.PASSTHROUGH_CHANGED);
+
+ // Fire the state change events only after all the states have been updated.
+ if (environmentVisibilityChanged) {
+ environment.fireOnSpatialEnvironmentChangedEvent(
+ environment.isSpatialEnvironmentPreferenceActive());
+ }
+ if (passthroughVisibilityChanged) {
+ environment.firePassthroughOpacityChangedEvent(
+ environment.getCurrentPassthroughOpacity());
+ }
+
+ if (spatialCapabilitiesChanged) {
+ SpatialCapabilities spatialCapabilities =
+ RuntimeUtils.convertSpatialCapabilities(
+ newSpatialState.getSpatialCapabilities());
+
+ spatialCapabilitiesChangedListeners.forEach(
+ (listener, executor) ->
+ executor.execute(() -> listener.accept(spatialCapabilities)));
+ }
+
+ if (hasBoundsChanged) {
+ activitySpace.onBoundsChanged(newSpatialState.getBounds());
+ }
+ }
+
+ private void setSpatialStateCallback() {
+ Handler mainHandler = new Handler(Looper.getMainLooper());
+ extensions.registerSpatialStateCallback(
+ activity, this::onSpatialStateChanged, mainHandler::post);
+ }
+
+ private synchronized void initPerceptionLibrary() {
+ if (perceptionLibrary.getSession() != null) {
+ Log.w(TAG, "Cannot init perception session, already initialized.");
+ return;
+ }
+ ListenableFuture<Session> sessionFuture =
+ perceptionLibrary.initSession(activity, openXrReferenceSpaceType, executor);
+ Objects.requireNonNull(sessionFuture)
+ .addListener(
+ () -> {
+ try {
+ sessionFuture.get();
+ } catch (Exception e) {
+ if (e instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ Log.e(
+ TAG,
+ "Failed to init perception session with error: "
+ + e.getMessage());
+ }
+ },
+ executor);
+ }
+
+ @Override
+ @NonNull
+ public SpatialCapabilities getSpatialCapabilities() {
+ return RuntimeUtils.convertSpatialCapabilities(
+ lazySpatialStateProvider.get().getSpatialCapabilities());
+ }
+
+ @Override
+ public void addSpatialCapabilitiesChangedListener(
+ @NonNull Executor executor, @NonNull Consumer<SpatialCapabilities> listener) {
+ spatialCapabilitiesChangedListeners.put(listener, executor);
+ }
+
+ @Override
+ public void removeSpatialCapabilitiesChangedListener(
+ @NonNull Consumer<SpatialCapabilities> listener) {
+ spatialCapabilitiesChangedListeners.remove(listener);
+ }
+
+ @Override
+ @NonNull
+ public LoggingEntity createLoggingEntity(@NonNull Pose pose) {
+ LoggingEntityImpl entity = new LoggingEntityImpl();
+ entity.setPose(pose);
+ return entity;
+ }
+
+ @Override
+ @NonNull
+ public SpatialEnvironment getSpatialEnvironment() {
+ return environment;
+ }
+
+ @Override
+ @NonNull
+ public ActivitySpace getActivitySpace() {
+ return activitySpace;
+ }
+
+ @Override
+ @Nullable
+ public HeadActivityPose getHeadActivityPose() {
+ // If it is unable to retrieve a pose the head in not yet loaded in openXR so return null.
+ if (headActivityPose.getPoseInOpenXrReferenceSpace() == null) {
+ return null;
+ }
+ return headActivityPose;
+ }
+
+ @Override
+ @Nullable
+ public CameraViewActivityPose getCameraViewActivityPose(
+ @CameraViewActivityPose.CameraType int cameraType) {
+ CameraViewActivityPoseImpl cameraViewActivityPose = null;
+ if (cameraType == CameraViewActivityPose.CAMERA_TYPE_LEFT_EYE) {
+ cameraViewActivityPose = cameraActivityPoses.get(0);
+ } else if (cameraType == CameraViewActivityPose.CAMERA_TYPE_RIGHT_EYE) {
+ cameraViewActivityPose = cameraActivityPoses.get(1);
+ }
+ // If it is unable to retrieve a pose the camera in not yet loaded in openXR so return null.
+ if (cameraViewActivityPose == null
+ || cameraViewActivityPose.getPoseInOpenXrReferenceSpace() == null) {
+ return null;
+ }
+ return cameraViewActivityPose;
+ }
+
+ @Override
+ @NonNull
+ public PerceptionSpaceActivityPose getPerceptionSpaceActivityPose() {
+ return perceptionSpaceActivityPose;
+ }
+
+ // TODO(b/349180723): Refactor to a streaming based approach.
+ @Nullable
+ public Pose getHeadPoseInOpenXrUnboundedSpace() {
+ Session session = perceptionLibrary.getSession();
+ if (session == null) {
+ Log.w(TAG, "Perception session is uninitialized, returning null head pose.");
+ return null;
+ }
+ return RuntimeUtils.fromPerceptionPose(Objects.requireNonNull(session.getHeadPose()));
+ }
+
+ @Nullable
+ public ViewProjections getStereoViewsInOpenXrUnboundedSpace() {
+ Session session = perceptionLibrary.getSession();
+ if (session == null) {
+ Log.w(TAG, "Perception session is uninitialized, returning null head pose.");
+ return null;
+ }
+ return session.getStereoViews();
+ }
+
+ @Override
+ @NonNull
+ public Entity getActivitySpaceRootImpl() {
+ // Trivially returns the activity space for now, but it could be updated to return any other
+ // singleton space entity. That space entity will define the world space origin of the SDK
+ // and
+ // will be the default parent for new content entities.
+
+ return activitySpace;
+ }
+
+ @Override
+ public void requestFullSpaceMode() {
+ // TODO: b/376934871 - Check async results.
+ extensions.requestFullSpaceMode(
+ activity, /* requestEnter= */ true, (result) -> {}, Runnable::run);
+ }
+
+ @Override
+ public void requestHomeSpaceMode() {
+ // TODO: b/376934871 - Check async results.
+ extensions.requestFullSpaceMode(
+ activity, /* requestEnter= */ false, (result) -> {}, Runnable::run);
+ }
+
+ // TODO: b/374345896 - Delete this method once we've finalized the SplitEngine migration.
+ @SuppressWarnings({
+ "AndroidJdkLibsChecker",
+ "RestrictTo",
+ "FutureReturnValueIgnored",
+ "AsyncSuffixFuture"
+ })
+ @Override
+ @Nullable
+ public ListenableFuture<GltfModelResource> loadGltfByAssetName(@NonNull String assetName) {
+ ResolvableFuture<GltfModelResource> gltfModelResourceFuture = ResolvableFuture.create();
+ InputStream asset;
+ try {
+ asset = activity.getAssets().open(assetName);
+ } catch (Exception e) {
+ Log.w(TAG, "Could not open asset with error: " + e.getMessage());
+ return null;
+ }
+
+ CompletableFuture<androidx.xr.extensions.asset.GltfModelToken> tokenFuture;
+ try {
+ tokenFuture = extensions.loadGltfModel(asset, asset.available(), 0, assetName);
+ // Unfortunately, there is no way to avoid "leaking" this future, since we want to
+ // return a
+ // ListenableFuture. This should be a short lived problem since clients should be using
+ // loadGltfByAssetNameSplitEngine() if they have SplitEngine enabled.
+ tokenFuture.thenApply(
+ token -> gltfModelResourceFuture.set(getModelResourceFromToken(token)));
+ } catch (Exception e) {
+ Log.w(TAG, "Could not load glTF model with error: " + e.getMessage());
+ return null;
+ }
+
+ return gltfModelResourceFuture;
+ }
+
+ // ResolvableFuture is marked as RestrictTo(LIBRARY_GROUP_PREFIX), which is intended for classes
+ // within AndroidX. We're in the process of migrating to AndroidX. Without suppressing this
+ // warning, however, we get a build error - go/bugpattern/RestrictTo.
+ @SuppressWarnings({"RestrictTo", "AsyncSuffixFuture"})
+ @Override
+ @Nullable
+ public ListenableFuture<GltfModelResource> loadGltfByAssetNameSplitEngine(
+ @NonNull String name) {
+ ResolvableFuture<GltfModelResource> gltfModelResourceFuture = ResolvableFuture.create();
+ // TODO:b/374216912 - Consider calling setFuture() here to catch if the application calls
+ // cancel() on the return value from this function, so we can propagate the cancelation
+ // message
+ // to the Impress API.
+
+ if (!Looper.getMainLooper().isCurrentThread()) {
+ throw new IllegalStateException("This method must be called on the main thread.");
+ }
+
+ ListenableFuture<Long> gltfTokenFuture;
+ try {
+ gltfTokenFuture = impressApi.loadGltfModel(name);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Failed to load glTF model with error: " + e.getMessage());
+ // TODO:b/375070346 - make this method NonNull and set the gltfModelResourceFuture to an
+ // exception and return that.
+ return null;
+ }
+
+ gltfTokenFuture.addListener(
+ () -> {
+ try {
+ long gltfToken = gltfTokenFuture.get();
+ gltfModelResourceFuture.set(
+ getModelResourceFromTokenSplitEngine(gltfToken));
+ } catch (Exception e) {
+ if (e instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ Log.e(TAG, "Failed to load glTF model with error: " + e.getMessage());
+ gltfModelResourceFuture.setException(e);
+ }
+ },
+ // It's convenient for the main application for us to dispatch their listeners on
+ // the main
+ // thread, because they are required to call back to Impress from there, and it's
+ // likely
+ // that they will want to call back into the SDK to create entities from within a
+ // listener.
+ // We defensively post to the main thread here, but in practice this should not
+ // cause a
+ // thread hop because the Impress API already dispatches its callbacks to the main
+ // thread.
+ activity::runOnUiThread);
+ return gltfModelResourceFuture;
+ }
+
+ // TODO: b/376504646 - Delete this method once we've migrated to a SplitEngine backed skybox.
+ @SuppressWarnings({
+ "AndroidJdkLibsChecker",
+ "RestrictTo",
+ "FutureReturnValueIgnored",
+ "AsyncSuffixFuture"
+ })
+ @Override
+ @Nullable
+ public ListenableFuture<ExrImageResource> loadExrImageByAssetName(@NonNull String assetName) {
+ ResolvableFuture<ExrImageResource> exrImageResourceFuture = ResolvableFuture.create();
+ InputStream asset;
+ try {
+ // NOTE: extensions.loadEnvironment expects a .EXR file.
+ asset = activity.getAssets().open(assetName);
+ } catch (Exception e) {
+ Log.w(TAG, "Could not open asset with error: " + e.getMessage());
+ return null;
+ }
+
+ CompletableFuture<androidx.xr.extensions.asset.EnvironmentToken> tokenFuture;
+ try {
+ // NOTE: At the moment, extensions.loadEnvironment expects a .EXR file explicitly. This
+ // will need to be updated as support for GLTF environment geometry is added by
+ // the system.
+ tokenFuture = extensions.loadEnvironment(asset, asset.available(), 0, assetName);
+ // Unfortunately, there is no way to avoid "leaking" this future, since we want to
+ // return a
+ // ListenableFuture. This method should be deleted soon, once the SplitEngine backed
+ // skybox
+ // is ready.
+ tokenFuture.thenApply(
+ token -> exrImageResourceFuture.set(getExrImageResourceFromToken(token)));
+ } catch (Exception e) {
+ Log.i(TAG, "Could not load ExrImage with error: " + e.getMessage());
+ return null;
+ }
+ Log.w(TAG, "Loaded asset: " + assetName);
+
+ return exrImageResourceFuture;
+ }
+
+ @Override
+ @NonNull
+ public GltfEntity createGltfEntity(
+ @NonNull Pose pose, @NonNull GltfModelResource model, @Nullable Entity parentEntity) {
+ if (useSplitEngine && model instanceof GltfModelResourceImplSplitEngine) {
+ return createGltfEntitySplitEngine(pose, model, parentEntity);
+ }
+ if (parentEntity == null) {
+ throw new IllegalArgumentException("parentEntity cannot be null");
+ }
+ if (!(model instanceof GltfModelResourceImpl)) {
+ throw new IllegalArgumentException("GltfModelResource is not a GltfModelResourceImpl");
+ }
+ GltfEntity entity =
+ new GltfEntityImpl(
+ (GltfModelResourceImpl) model,
+ parentEntity,
+ extensions,
+ entityManager,
+ executor);
+ entity.setPose(pose);
+ return entity;
+ }
+
+ @Override
+ @NonNull
+ public StereoSurfaceEntity createStereoSurfaceEntity(
+ @StereoSurfaceEntity.StereoMode int stereoMode,
+ @NonNull Dimensions dimensions,
+ @NonNull Pose pose,
+ @NonNull Entity parentEntity) {
+ if (useSplitEngine) {
+ return createStereoSurfaceEntitySplitEngine(stereoMode, dimensions, pose, parentEntity);
+ } else {
+ // TODO: b/377983872 - Throw UnsupportedOperationException here.
+ Log.w(TAG, "stereo surface only works with split engine.");
+ return new StereoSurfaceEntityImpl(parentEntity, extensions, entityManager, executor);
+ }
+ }
+
+ @Override
+ @NonNull
+ public PanelEntity createPanelEntity(
+ @NonNull Pose pose,
+ @NonNull View view,
+ @NonNull PixelDimensions surfaceDimensionsPx,
+ @NonNull Dimensions dimensions,
+ @NonNull String name,
+ @SuppressWarnings("ContextFirst") @NonNull Context context,
+ @NonNull Entity parent) {
+
+ // TODO(b/352630140): Move this into a static factory method of PanelEntityImpl.
+
+ SurfaceControlViewHost surfaceControlViewHost =
+ new SurfaceControlViewHost(
+ context, Objects.requireNonNull(context.getDisplay()), new Binder());
+ surfaceControlViewHost.setView(view, surfaceDimensionsPx.width, surfaceDimensionsPx.height);
+ Node node = extensions.createNode();
+ SurfacePackage surfacePackage =
+ Objects.requireNonNull(surfaceControlViewHost.getSurfacePackage());
+
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction
+ .setName(node, name)
+ .setSurfacePackage(node, surfacePackage)
+ .setWindowBounds(
+ surfacePackage, surfaceDimensionsPx.width, surfaceDimensionsPx.height)
+ .setVisibility(node, true)
+ // Corner radius must be zeroed as the value is inherited from the parent node
+ // otherwise.
+ .setCornerRadius(node, 0f)
+ .apply();
+ }
+ surfacePackage.release();
+
+ PanelEntity panelEntity =
+ new PanelEntityImpl(
+ node,
+ extensions,
+ entityManager,
+ surfaceControlViewHost,
+ surfaceDimensionsPx,
+ executor);
+ panelEntity.setParent(parent);
+ panelEntity.setPose(pose);
+ return panelEntity;
+ }
+
+ @Override
+ @NonNull
+ public PanelEntity getMainPanelEntity() {
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction.setVisibility(taskWindowLeashNode, true).apply();
+ }
+ return mainPanelEntity;
+ }
+
+ @Override
+ @SuppressLint("ExecutorRegistration")
+ @NonNull
+ public InteractableComponent createInteractableComponent(
+ @NonNull Executor executor, @NonNull InputEventListener listener) {
+ return new InteractableComponentImpl(executor, listener);
+ }
+
+ @Override
+ @NonNull
+ public MovableComponent createMovableComponent(
+ boolean systemMovable,
+ boolean scaleInZ,
+ @NonNull Set<AnchorPlacement> anchorPlacement,
+ boolean shouldDisposeParentAnchor) {
+ return new MovableComponentImpl(
+ systemMovable,
+ scaleInZ,
+ anchorPlacement,
+ shouldDisposeParentAnchor,
+ perceptionLibrary,
+ extensions,
+ activitySpace,
+ (AndroidXrEntity) getActivitySpaceRootImpl(),
+ perceptionSpaceActivityPose,
+ entityManager,
+ new PanelShadowRenderer(
+ activitySpace, perceptionSpaceActivityPose, activity, extensions),
+ executor);
+ }
+
+ @Override
+ @NonNull
+ public AnchorPlacement createAnchorPlacementForPlanes(
+ @NonNull Set<PlaneType> planeTypeFilter,
+ @NonNull Set<PlaneSemantic> planeSemanticFilter) {
+ AnchorPlacementImpl anchorPlacement = new AnchorPlacementImpl();
+ anchorPlacement.planeTypeFilter.addAll(planeTypeFilter);
+ anchorPlacement.planeSemanticFilter.addAll(planeSemanticFilter);
+ return anchorPlacement;
+ }
+
+ @Override
+ @NonNull
+ public ResizableComponent createResizableComponent(
+ @NonNull Dimensions minimumSize, @NonNull Dimensions maximumSize) {
+ return new ResizableComponentImpl(executor, extensions, minimumSize, maximumSize);
+ }
+
+ @Override
+ @SuppressLint("ExecutorRegistration")
+ @SuppressWarnings("ExecutorRegistration")
+ @NonNull
+ public PointerCaptureComponent createPointerCaptureComponent(
+ @NonNull Executor executor,
+ @NonNull PointerCaptureComponent.StateListener stateListener,
+ @NonNull InputEventListener inputListener) {
+ return new PointerCaptureComponentImpl(executor, stateListener, inputListener);
+ }
+
+ @Override
+ @NonNull
+ public ActivityPanelEntity createActivityPanelEntity(
+ @NonNull Pose pose,
+ @NonNull PixelDimensions windowBoundsPx,
+ @NonNull String name,
+ @NonNull Activity hostActivity,
+ @NonNull Entity parent) {
+
+ // TODO(b/352630140): Move this into a static factory method of ActivityPanelEntityImpl.
+ Rect windowBoundsRect = new Rect(0, 0, windowBoundsPx.width, windowBoundsPx.height);
+ ActivityPanel activityPanel =
+ extensions.createActivityPanel(
+ hostActivity, new ActivityPanelLaunchParameters(windowBoundsRect));
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction
+ .setVisibility(activityPanel.getNode(), true)
+ .setName(activityPanel.getNode(), name)
+ // Corner radius must be zeroed as the value is inherited from the parent node
+ // otherwise.
+ .setCornerRadius(activityPanel.getNode(), 0f)
+ .apply();
+ }
+ activityPanel.setWindowBounds(windowBoundsRect);
+ ActivityPanelEntityImpl activityPanelEntity =
+ new ActivityPanelEntityImpl(
+ activityPanel.getNode(),
+ extensions,
+ entityManager,
+ activityPanel,
+ windowBoundsPx,
+ executor);
+ activityPanelEntity.setParent(parent);
+ activityPanelEntity.setPose(pose);
+ return activityPanelEntity;
+ }
+
+ @Override
+ @NonNull
+ public AnchorEntity createAnchorEntity(
+ @NonNull Dimensions bounds,
+ @NonNull PlaneType planeType,
+ @NonNull PlaneSemantic planeSemantic,
+ @NonNull Duration searchTimeout) {
+ Node node = extensions.createNode();
+ return AnchorEntityImpl.createSemanticAnchor(
+ node,
+ bounds,
+ planeType,
+ planeSemantic,
+ searchTimeout,
+ getActivitySpace(),
+ getActivitySpaceRootImpl(),
+ extensions,
+ entityManager,
+ executor,
+ perceptionLibrary);
+ }
+
+ @Override
+ @NonNull
+ public AnchorEntity createAnchorEntity(@NonNull Anchor anchor) {
+ Node node = extensions.createNode();
+ return AnchorEntityImpl.createAnchorFromPerceptionAnchor(
+ node,
+ anchor,
+ getActivitySpace(),
+ getActivitySpaceRootImpl(),
+ extensions,
+ entityManager,
+ executor,
+ perceptionLibrary);
+ }
+
+ @Override
+ @NonNull
+ public Entity createEntity(@NonNull Pose pose, @NonNull String name, @NonNull Entity parent) {
+ Node node = extensions.createNode();
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction.setName(node, name).apply();
+ }
+
+ // This entity is used to back JXR Core's ContentlessEntity.
+ Entity entity = new AndroidXrEntity(node, extensions, entityManager, executor) {};
+ entity.setParent(parent);
+ entity.setPose(pose);
+ return entity;
+ }
+
+ @Override
+ @NonNull
+ public AnchorEntity createPersistedAnchorEntity(
+ @NonNull UUID uuid, @NonNull Duration searchTimeout) {
+ Node node = extensions.createNode();
+ return AnchorEntityImpl.createPersistedAnchor(
+ node,
+ uuid,
+ searchTimeout,
+ getActivitySpace(),
+ getActivitySpaceRootImpl(),
+ extensions,
+ entityManager,
+ executor,
+ perceptionLibrary);
+ }
+
+ @Override
+ public boolean unpersistAnchor(@NonNull UUID uuid) {
+ Session session = perceptionLibrary.getSession();
+ if (session == null) {
+ Log.w(TAG, "Cannot unpersist anchor, perception session is not initialized.");
+ return false;
+ }
+ return session.unpersistAnchor(uuid);
+ }
+
+ @Override
+ @NonNull
+ public Bundle setFullSpaceMode(@NonNull Bundle bundle) {
+ return extensions.setFullSpaceMode(bundle);
+ }
+
+ @Override
+ @NonNull
+ public Bundle setFullSpaceModeWithEnvironmentInherited(@NonNull Bundle bundle) {
+ return extensions.setFullSpaceModeWithEnvironmentInherited(bundle);
+ }
+
+ @Override
+ public void setPreferredAspectRatio(@NonNull Activity activity, float preferredRatio) {
+ // TODO: b/376934871 - Check async results.
+ extensions.setPreferredAspectRatio(activity, preferredRatio, (result) -> {}, Runnable::run);
+ }
+
+ @Override
+ public void startRenderer() {
+ if (splitEngineRenderer == null || frameLoopStarted) {
+ return;
+ }
+ frameLoopStarted = true;
+ splitEngineRenderer.startFrameLoop();
+ }
+
+ @Override
+ public void stopRenderer() {
+ if (splitEngineRenderer == null || !frameLoopStarted) {
+ return;
+ }
+ frameLoopStarted = false;
+ splitEngineRenderer.stopFrameLoop();
+ }
+
+ @Override
+ public void dispose() {
+ Log.i(TAG, "Disposing resources");
+ environment.dispose();
+ extensions.clearSpatialStateCallback(activity);
+ // TODO: b/376934871 - Check async results.
+ extensions.detachSpatialScene(activity, (result) -> {}, Runnable::run);
+ activity = null;
+ entityManager.getAllEntities().forEach(Entity::dispose);
+ entityManager.clear();
+ if (splitEngineRenderer != null && splitEngineSubspaceManager != null) {
+ splitEngineRenderer.destroy();
+ splitEngineSubspaceManager.destroy();
+ }
+ }
+
+ public void setSplitEngineSubspaceManager(
+ @Nullable SplitEngineSubspaceManager splitEngineSubspaceManager) {
+ this.splitEngineSubspaceManager = splitEngineSubspaceManager;
+ }
+
+ @Override
+ @NonNull
+ public SoundPoolExtensionsWrapper getSoundPoolExtensionsWrapper() {
+ return soundPoolExtensionsWrapper;
+ }
+
+ @Override
+ @NonNull
+ public AudioTrackExtensionsWrapper getAudioTrackExtensionsWrapper() {
+ return audioTrackExtensionsWrapper;
+ }
+
+ @Override
+ @NonNull
+ public MediaPlayerExtensionsWrapper getMediaPlayerExtensionsWrapper() {
+ return mediaPlayerExtensionsWrapper;
+ }
+
+ /**
+ * Get the underlying OpenXR session that backs perception.
+ *
+ * <p>The OpenXR session is created on JXR's primary thread so this may return {@code
+ * XR_NULL_HANDLE} for a few frames at startup.
+ *
+ * @return the OpenXR XrSession, encoded in a jlong
+ */
+ public long getNativeSession() {
+ Session session = perceptionLibrary.getSession();
+ if (session == null) {
+ Log.w(TAG, "Perception session is uninitilazied, returning XR_NULL_HANDLE");
+ return Session.XR_NULL_HANDLE;
+ }
+
+ long nativeSession = session.getNativeSession();
+ if (nativeSession == Session.XR_NULL_HANDLE) {
+ Log.w(TAG, "Perception session initialized, but native session not yet created");
+ return Session.XR_NULL_HANDLE;
+ }
+
+ return nativeSession;
+ }
+
+ /**
+ * Get the underlying OpenXR instance that backs perception.
+ *
+ * <p>The OpenXR instance is created on JXR's primary thread so this may return {@code
+ * XR_NULL_HANDLE} for a few frames at startup.
+ *
+ * @return the OpenXR XrInstance, encoded in a jlong
+ */
+ public long getNativeInstance() {
+ Session session = perceptionLibrary.getSession();
+ if (session == null) {
+ Log.w(TAG, "Perception session is uninitilazied, returning XR_NULL_HANDLE");
+ return Session.XR_NULL_HANDLE;
+ }
+
+ long nativeInstance = session.getNativeInstance();
+ if (nativeInstance == Session.XR_NULL_HANDLE) {
+ Log.w(TAG, "Perception session initialized, but native instance not yet created");
+ return Session.XR_NULL_HANDLE;
+ }
+
+ return nativeInstance;
+ }
+
+ private StereoSurfaceEntity createStereoSurfaceEntitySplitEngine(
+ @StereoSurfaceEntity.StereoMode int stereoMode,
+ Dimensions dimensions,
+ Pose pose,
+ @NonNull Entity parentEntity) {
+
+ if (!Looper.getMainLooper().isCurrentThread()) {
+ throw new IllegalStateException("This method must be called on the main thread.");
+ }
+
+ StereoSurfaceEntity entity =
+ new StereoSurfaceEntitySplitEngineImpl(
+ parentEntity,
+ impressApi,
+ splitEngineSubspaceManager,
+ extensions,
+ entityManager,
+ executor,
+ stereoMode);
+ entity.setPose(pose);
+ entity.setDimensions(dimensions);
+ return entity;
+ }
+
+ private GltfEntity createGltfEntitySplitEngine(
+ Pose pose, GltfModelResource model, Entity parentEntity) {
+
+ if (!Looper.getMainLooper().isCurrentThread()) {
+ throw new IllegalStateException("This method must be called on the main thread.");
+ }
+ if (parentEntity == null) {
+ throw new IllegalArgumentException("parentEntity cannot be null");
+ }
+ if (!(model instanceof GltfModelResourceImplSplitEngine)) {
+ throw new IllegalArgumentException(
+ "GltfModelResource is not a GltfModelResourceImplSplitEngine");
+ }
+ GltfEntity entity =
+ new GltfEntityImplSplitEngine(
+ (GltfModelResourceImplSplitEngine) model,
+ parentEntity,
+ impressApi,
+ splitEngineSubspaceManager,
+ extensions,
+ entityManager,
+ executor);
+ entity.setPose(pose);
+ return entity;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/LoggingEntityImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/LoggingEntityImpl.java
new file mode 100644
index 0000000..2c1487b
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/LoggingEntityImpl.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.util.Log;
+
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.scenecore.JxrPlatformAdapter.ActivityPose;
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.InputEventListener;
+import androidx.xr.scenecore.JxrPlatformAdapter.LoggingEntity;
+import androidx.xr.scenecore.common.BaseEntity;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/** Implementation of a RealityCore Entity that logs its function calls. */
+class LoggingEntityImpl extends BaseEntity implements LoggingEntity {
+
+ private static final String TAG = "RealityCoreRuntime";
+
+ public LoggingEntityImpl() {
+ Log.i(TAG, "Creating LoggingEntity.");
+ }
+
+ @Override
+ public Pose getPose() {
+ Log.i(TAG, "Getting Logging Entity pose: " + super.getPose());
+ return super.getPose();
+ }
+
+ @Override
+ public void setPose(Pose pose) {
+ Log.i(TAG, "Setting Logging Entity pose to: " + pose);
+ super.setPose(pose);
+ }
+
+ @Override
+ public Pose getActivitySpacePose() {
+ Log.i(TAG, "Getting Logging Entity activitySpacePose.");
+ return new Pose();
+ }
+
+ @Override
+ public Pose transformPoseTo(Pose pose, ActivityPose destination) {
+ Log.i(
+ TAG,
+ "Transforming pose "
+ + pose
+ + " to be relative to the destination ActivityPose: "
+ + destination);
+ return new Pose();
+ }
+
+ @Override
+ public void addChild(Entity child) {
+ Log.i(TAG, "Adding child Entity: " + child);
+ super.addChild(child);
+ }
+
+ @Override
+ public void addChildren(List<Entity> children) {
+ Log.i(TAG, "Adding child Entities: " + children);
+ super.addChildren(children);
+ }
+
+ @Override
+ public Entity getParent() {
+ Log.i(TAG, "Getting Logging Entity parent: " + super.getParent());
+ return super.getParent();
+ }
+
+ @Override
+ public void setParent(Entity parent) {
+ if (!(parent instanceof LoggingEntityImpl)) {
+ Log.e(TAG, "Parent of a LoggingEntity must be a Logging entity");
+ return;
+ }
+ Log.i(TAG, "Setting Logging Entity parent to: " + parent);
+ super.setParent(parent);
+ }
+
+ @Override
+ public List<Entity> getChildren() {
+ Log.i(TAG, "Getting Logging Entity children: " + super.getChildren());
+ return super.getChildren();
+ }
+
+ @Override
+ public void setSize(Dimensions dimensions) {
+ Log.i(TAG, "Set size to " + dimensions);
+ }
+
+ @Override
+ public void addInputEventListener(Executor executor, InputEventListener consumer) {
+ Log.i(TAG, "Add input consumer " + consumer + " executor " + executor);
+ }
+
+ @Override
+ public void removeInputEventListener(InputEventListener consumer) {
+ Log.i(TAG, "Remove input consumer " + consumer);
+ }
+
+ @Override
+ public void dispose() {
+ Log.i(TAG, "dispose");
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/MainPanelEntityImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/MainPanelEntityImpl.java
new file mode 100644
index 0000000..3d4c32a
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/MainPanelEntityImpl.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.app.Activity;
+import android.graphics.Rect;
+
+import androidx.xr.extensions.XrExtensions;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.PanelEntity;
+import androidx.xr.scenecore.JxrPlatformAdapter.PixelDimensions;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * MainPanelEntity is a special instance of a PanelEntity that is backed by the WindowLeash CPM
+ * node. The content of this PanelEntity is assumed to have been previously defined and associated
+ * with the Window Leash Node.
+ */
+@SuppressWarnings("deprecation") // TODO(b/373435470): Remove
+final class MainPanelEntityImpl extends BasePanelEntity implements PanelEntity {
+ Activity runtimeActivity;
+
+ // Note that we expect the Node supplied here to be the WindowLeash node.
+ MainPanelEntityImpl(
+ Activity activity,
+ Node node,
+ XrExtensions extensions,
+ EntityManager entityManager,
+ ScheduledExecutorService executor) {
+ super(node, extensions, entityManager, executor);
+ runtimeActivity = activity;
+
+ // Read the Pixel dimensions for the primary panel off the Activity's WindowManager.
+ // Note that this requires MinAPI 30.
+ // TODO(b/352827267): Enforce minSDK API strategy - go/androidx-api-guidelines#compat-newapi
+ Rect bounds = getBoundsFromWindowManager();
+ super.setPixelDimensions(new PixelDimensions(bounds.width(), bounds.height()));
+ }
+
+ private Rect getBoundsFromWindowManager() {
+ return runtimeActivity.getWindowManager().getCurrentWindowMetrics().getBounds();
+ }
+
+ @Override
+ public Dimensions getSize() {
+ // The main panel bounds can change in HSM without JXRCore. Always read the bounds from the
+ // WindowManager.
+ Rect bounds = getBoundsFromWindowManager();
+ Vector3 pixelDensity = getPixelDensity();
+ return new Dimensions(
+ bounds.width() / pixelDensity.getX(), bounds.height() / pixelDensity.getY(), 0);
+ }
+
+ @Override
+ public PixelDimensions getPixelDimensions() {
+ // The main panel bounds can change in HSM without JXRCore. Always read the bounds from the
+ // WindowManager.
+ Rect bounds = getBoundsFromWindowManager();
+ return new PixelDimensions(bounds.width(), bounds.height());
+ }
+
+ @Override
+ public void setPixelDimensions(PixelDimensions dimensions) {
+ // TODO: b/376126162 - Consider calling setPixelDimensions() either when setMainWindowSize's
+ // callback is called, or when the next spatial state callback with the expected size is
+ // called.
+ super.setPixelDimensions(dimensions);
+ // TODO: b/376934871 - Check async results.
+ extensions.setMainWindowSize(
+ runtimeActivity,
+ dimensions.width,
+ dimensions.height,
+ (result) -> {},
+ Runnable::run);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/Matrix4Ext.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/Matrix4Ext.kt
new file mode 100644
index 0000000..af6ba58
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/Matrix4Ext.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+@file:JvmName("Matrix4Ext")
+
+package androidx.xr.scenecore.impl
+
+import androidx.annotation.RestrictTo
+import androidx.xr.runtime.math.Matrix4
+import androidx.xr.runtime.math.Vector3
+
+// TODO: b/377781580 - Consider removing this when rotation() can be used for scaled matrices.
+/** Returns the unscaled version of this matrix. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public fun Matrix4.getUnscaled(): Matrix4 {
+ // TODO: b/367780918 - Investigate why this.scale has negative values when inputs were positive.
+ // and allow negative scale values once this.scale reliably returns signed values.
+ val positiveScale = Vector3.abs(this.scale)
+ val scaleX = positiveScale.x
+ val scaleY = positiveScale.y
+ val scaleZ = positiveScale.z
+ return Matrix4(
+ floatArrayOf(
+ data[0] / scaleX,
+ data[1] / scaleX,
+ data[2] / scaleX,
+ data[3],
+ data[4] / scaleY,
+ data[5] / scaleY,
+ data[6] / scaleY,
+ data[7],
+ data[8] / scaleZ,
+ data[9] / scaleZ,
+ data[10] / scaleZ,
+ data[11],
+ data[12],
+ data[13],
+ data[14],
+ data[15],
+ )
+ )
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/MediaPlayerExtensionsWrapperImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/MediaPlayerExtensionsWrapperImpl.java
new file mode 100644
index 0000000..b741ef8
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/MediaPlayerExtensionsWrapperImpl.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.media.MediaPlayer;
+
+import androidx.annotation.NonNull;
+import androidx.xr.extensions.media.MediaPlayerExtensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.MediaPlayerExtensionsWrapper;
+import androidx.xr.scenecore.JxrPlatformAdapter.PointSourceAttributes;
+import androidx.xr.scenecore.JxrPlatformAdapter.SoundFieldAttributes;
+
+/** Implementation of the {@link MediaPlayerExtensionsWrapper}. */
+final class MediaPlayerExtensionsWrapperImpl implements MediaPlayerExtensionsWrapper {
+
+ private final MediaPlayerExtensions extensions;
+
+ public MediaPlayerExtensionsWrapperImpl(@NonNull MediaPlayerExtensions extensions) {
+ this.extensions = extensions;
+ }
+
+ @Override
+ public void setPointSourceAttributes(
+ @NonNull MediaPlayer mediaPlayer, @NonNull PointSourceAttributes attributes) {
+ androidx.xr.extensions.media.PointSourceAttributes extAttributes =
+ MediaUtils.convertPointSourceAttributesToExtensions(attributes);
+
+ extensions.setPointSourceAttributes(mediaPlayer, extAttributes);
+ }
+
+ @Override
+ public void setSoundFieldAttributes(
+ @NonNull MediaPlayer mediaPlayer, @NonNull SoundFieldAttributes attributes) {
+ androidx.xr.extensions.media.SoundFieldAttributes extAttributes =
+ MediaUtils.convertSoundFieldAttributesToExtensions(attributes);
+
+ extensions.setSoundFieldAttributes(mediaPlayer, extAttributes);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/MediaUtils.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/MediaUtils.java
new file mode 100644
index 0000000..52010dc
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/MediaUtils.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import androidx.annotation.RestrictTo;
+import androidx.xr.extensions.media.PointSourceAttributes;
+import androidx.xr.extensions.media.SoundFieldAttributes;
+import androidx.xr.extensions.media.SpatializerExtensions;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.scenecore.JxrPlatformAdapter;
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatializerConstants;
+
+/** Utils for the runtime media class conversions. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class MediaUtils {
+ private MediaUtils() {}
+
+ static PointSourceAttributes convertPointSourceAttributesToExtensions(
+ JxrPlatformAdapter.PointSourceAttributes attributes) {
+
+ Node node = ((AndroidXrEntity) attributes.getEntity()).getNode();
+
+ return new PointSourceAttributes.Builder().setNode(node).build();
+ }
+
+ static SoundFieldAttributes convertSoundFieldAttributesToExtensions(
+ JxrPlatformAdapter.SoundFieldAttributes attributes) {
+
+ return new SoundFieldAttributes.Builder()
+ .setAmbisonicsOrder(
+ convertAmbisonicsOrderToExtensions(attributes.getAmbisonicsOrder()))
+ .build();
+ }
+
+ @SpatializerExtensions.AmbisonicsOrder
+ static int convertAmbisonicsOrderToExtensions(
+ @SpatializerConstants.AmbisonicsOrder int ambisonicsOrder) {
+ switch (ambisonicsOrder) {
+ case SpatializerConstants.AMBISONICS_ORDER_FIRST_ORDER:
+ return SpatializerExtensions.AMBISONICS_ORDER_FIRST_ORDER;
+ case SpatializerConstants.AMBISONICS_ORDER_SECOND_ORDER:
+ return SpatializerExtensions.AMBISONICS_ORDER_SECOND_ORDER;
+ case SpatializerConstants.AMBISONICS_ORDER_THIRD_ORDER:
+ return SpatializerExtensions.AMBISONICS_ORDER_THIRD_ORDER;
+ default:
+ throw new IllegalArgumentException(
+ "Invalid Sound Field ambisonics order: " + ambisonicsOrder);
+ }
+ }
+
+ @SpatializerConstants.SourceType
+ static int convertExtensionsToSourceType(
+ @SpatializerExtensions.SourceType int extensionsSourceType) {
+ switch (extensionsSourceType) {
+ case SpatializerExtensions.SOURCE_TYPE_BYPASS:
+ return SpatializerConstants.SOURCE_TYPE_BYPASS;
+ case SpatializerExtensions.SOURCE_TYPE_POINT_SOURCE:
+ return SpatializerConstants.SOURCE_TYPE_POINT_SOURCE;
+ case SpatializerExtensions.SOURCE_TYPE_SOUND_FIELD:
+ return SpatializerConstants.SOURCE_TYPE_SOUND_FIELD;
+ default:
+ throw new IllegalArgumentException(
+ "Invalid Sound Spatializer source type: " + extensionsSourceType);
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/MovableComponentImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/MovableComponentImpl.java
new file mode 100644
index 0000000..035bc3e
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/MovableComponentImpl.java
@@ -0,0 +1,577 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static java.lang.Math.max;
+
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.Nullable;
+import androidx.xr.extensions.Consumer;
+import androidx.xr.extensions.XrExtensions;
+import androidx.xr.extensions.node.ReformEvent;
+import androidx.xr.extensions.node.ReformOptions;
+import androidx.xr.extensions.node.Vec3;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Quaternion;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity;
+import androidx.xr.scenecore.JxrPlatformAdapter.AnchorPlacement;
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.MovableComponent;
+import androidx.xr.scenecore.JxrPlatformAdapter.MoveEvent;
+import androidx.xr.scenecore.JxrPlatformAdapter.MoveEventListener;
+import androidx.xr.scenecore.JxrPlatformAdapter.PlaneSemantic;
+import androidx.xr.scenecore.JxrPlatformAdapter.PlaneType;
+import androidx.xr.scenecore.JxrPlatformAdapter.Ray;
+import androidx.xr.scenecore.impl.perception.PerceptionLibrary;
+import androidx.xr.scenecore.impl.perception.Plane;
+import androidx.xr.scenecore.impl.perception.Plane.PlaneData;
+import androidx.xr.scenecore.impl.perception.Session;
+
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
+
+/** Implementation of MovableComponent. */
+@SuppressWarnings("BanConcurrentHashMap")
+class MovableComponentImpl implements MovableComponent {
+ private static final String TAG = "MovableComponentImpl";
+ static final float MIN_PLANE_ANCHOR_DISTANCE = .2f;
+ private final boolean systemMovable;
+ private final boolean scaleInZ;
+ private final boolean shouldDisposeParentAnchor;
+ private final PerceptionLibrary perceptionLibrary;
+ private final XrExtensions extensions;
+ private final ActivitySpaceImpl activitySpaceImpl;
+ private final AndroidXrEntity activitySpaceEntity;
+ private final PerceptionSpaceActivityPoseImpl perceptionSpaceActivityPose;
+ private final EntityManager entityManager;
+ private final PanelShadowRenderer panelShadowRenderer;
+ private final ScheduledExecutorService runtimeExecutor;
+ private final ConcurrentHashMap<MoveEventListener, Executor> moveEventListenersMap =
+ new ConcurrentHashMap<>();
+ private final Map<PlaneType, Map<PlaneSemantic, AnchorPlacementImpl>> anchorableFilters =
+ new EnumMap<>(PlaneType.class);
+ // Visible for testing.
+ Consumer<ReformEvent> reformEventConsumer;
+ private Entity entity;
+ private Entity initialParent;
+ private Pose lastPose = new Pose();
+ private Vector3 lastScale = new Vector3(1f, 1f, 1f);
+ private Dimensions currentSize;
+ private boolean userAnchorable = false;
+ private AnchorEntity createdAnchorEntity;
+ private AnchorPlacementImpl createdAnchorPlacement;
+ @ScaleWithDistanceMode private int scaleWithDistanceMode = ScaleWithDistanceMode.DEFAULT;
+
+ public MovableComponentImpl(
+ boolean systemMovable,
+ boolean scaleInZ,
+ Set<AnchorPlacement> anchorPlacement,
+ boolean shouldDisposeParentAnchor,
+ PerceptionLibrary perceptionLibrary,
+ XrExtensions extensions,
+ ActivitySpaceImpl activitySpaceImpl,
+ AndroidXrEntity activitySpaceEntity,
+ PerceptionSpaceActivityPoseImpl perceptionSpaceActivityPose,
+ EntityManager entityManager,
+ PanelShadowRenderer panelShadowRenderer,
+ ScheduledExecutorService runtimeExecutor) {
+ this.systemMovable = systemMovable;
+ this.scaleInZ = scaleInZ;
+ this.shouldDisposeParentAnchor = shouldDisposeParentAnchor;
+ this.perceptionLibrary = perceptionLibrary;
+ this.extensions = extensions;
+ this.activitySpaceImpl = activitySpaceImpl;
+ this.activitySpaceEntity = activitySpaceEntity;
+ this.perceptionSpaceActivityPose = perceptionSpaceActivityPose;
+ this.entityManager = entityManager;
+ this.panelShadowRenderer = panelShadowRenderer;
+ this.runtimeExecutor = runtimeExecutor;
+ setUpAnchorPlacement(anchorPlacement);
+ }
+
+ @Override
+ public boolean onAttach(Entity entity) {
+ if (this.entity != null) {
+ Log.e(TAG, "Already attached to entity " + this.entity);
+ return false;
+ }
+ this.entity = entity;
+ ReformOptions reformOptions = ((AndroidXrEntity) entity).getReformOptions();
+ // The math for anchoring uses the pose relative to the activity space so we should not set
+ // the reform options to be relative to the parent if the entity is anchorable.
+ int reformFlags = userAnchorable ? 0 : ReformOptions.FLAG_POSE_RELATIVE_TO_PARENT;
+ reformFlags =
+ (systemMovable && !userAnchorable)
+ ? reformFlags | ReformOptions.FLAG_ALLOW_SYSTEM_MOVEMENT
+ : reformFlags;
+ reformFlags = scaleInZ ? reformFlags | ReformOptions.FLAG_SCALE_WITH_DISTANCE : reformFlags;
+ reformOptions.setFlags(reformFlags);
+ reformOptions.setEnabledReform(reformOptions.getEnabledReform() | ReformOptions.ALLOW_MOVE);
+ reformOptions.setScaleWithDistanceMode(
+ translateScaleWithDistanceMode(scaleWithDistanceMode));
+
+ // TODO: b/348037292 - Remove this special case for PanelEntityImpl.
+ if (entity instanceof PanelEntityImpl && currentSize == null) {
+ currentSize = ((PanelEntityImpl) entity).getSize();
+ }
+ if (currentSize != null) {
+ reformOptions.setCurrentSize(
+ new Vec3(currentSize.width, currentSize.height, currentSize.depth));
+ }
+ if (userAnchorable && systemMovable && reformEventConsumer == null) {
+ reformEventConsumer =
+ reformEvent -> {
+ Pair<Pose, Entity> unused = getUpdatedReformEventPoseAndParent(reformEvent);
+ };
+ }
+ lastPose = entity.getPose();
+ lastScale = entity.getScale();
+ ((AndroidXrEntity) entity).updateReformOptions();
+ if (reformEventConsumer != null) {
+ ((AndroidXrEntity) entity).addReformEventConsumer(reformEventConsumer, runtimeExecutor);
+ }
+ return true;
+ }
+
+ @Override
+ public void onDetach(Entity entity) {
+ ReformOptions reformOptions = ((AndroidXrEntity) entity).getReformOptions();
+ reformOptions.setEnabledReform(
+ reformOptions.getEnabledReform() & ~ReformOptions.ALLOW_MOVE);
+ // Clear any flags that were set by this component.
+ int reformFlags = reformOptions.getFlags();
+ reformFlags =
+ systemMovable
+ ? reformFlags & ~ReformOptions.FLAG_ALLOW_SYSTEM_MOVEMENT
+ : reformFlags;
+ reformFlags =
+ scaleInZ ? reformFlags & ~ReformOptions.FLAG_SCALE_WITH_DISTANCE : reformFlags;
+ reformOptions.setFlags(reformFlags);
+ ((AndroidXrEntity) entity).updateReformOptions();
+ if (reformEventConsumer != null) {
+ ((AndroidXrEntity) entity).removeReformEventConsumer(reformEventConsumer);
+ reformEventConsumer = null;
+ }
+ this.entity = null;
+ }
+
+ @Override
+ public void setSize(Dimensions dimensions) {
+ currentSize = dimensions;
+ if (entity == null) {
+ Log.i(TAG, "setSize called before component is attached to an Entity.");
+ return;
+ }
+ ReformOptions reformOptions = ((AndroidXrEntity) entity).getReformOptions();
+ reformOptions.setCurrentSize(
+ new Vec3(dimensions.width, dimensions.height, dimensions.depth));
+ ((AndroidXrEntity) entity).updateReformOptions();
+ }
+
+ @Override
+ @ScaleWithDistanceMode
+ public int getScaleWithDistanceMode() {
+ return scaleWithDistanceMode;
+ }
+
+ @Override
+ public void setScaleWithDistanceMode(@ScaleWithDistanceMode int scaleWithDistanceMode) {
+ this.scaleWithDistanceMode = scaleWithDistanceMode;
+ if (entity == null) {
+ Log.w(
+ TAG,
+ "setScaleWithDistanceMode called before component is attached to an Entity.");
+ return;
+ }
+ ReformOptions reformOptions = ((AndroidXrEntity) entity).getReformOptions();
+ reformOptions.setScaleWithDistanceMode(
+ translateScaleWithDistanceMode(scaleWithDistanceMode));
+ ((AndroidXrEntity) entity).updateReformOptions();
+ }
+
+ @Override
+ public void addMoveEventListener(Executor executor, MoveEventListener moveEventListener) {
+ if (reformEventConsumer != null) {
+ ((AndroidXrEntity) entity).removeReformEventConsumer(reformEventConsumer);
+ }
+ reformEventConsumer =
+ reformEvent -> {
+ if (reformEvent.getType() != ReformEvent.REFORM_TYPE_MOVE) {
+ return;
+ }
+ if (reformEvent.getState() == ReformEvent.REFORM_STATE_START) {
+ initialParent = entity.getParent();
+ }
+ Pose newPose;
+ Entity updatedParent = null;
+ if (userAnchorable) {
+ Pair<Pose, Entity> updatedPoseParentPair =
+ getUpdatedReformEventPoseAndParent(reformEvent);
+ newPose = updatedPoseParentPair.first;
+ updatedParent = updatedPoseParentPair.second;
+ } else {
+ newPose =
+ RuntimeUtils.getPose(
+ reformEvent.getProposedPosition(),
+ reformEvent.getProposedOrientation());
+ }
+ Vector3 newScale = RuntimeUtils.getVector3(reformEvent.getProposedScale());
+ Entity disposeEntity = null;
+
+ Entity parent = updatedParent;
+ moveEventListenersMap.forEach(
+ (listener, listenerExecutor) ->
+ executor.execute(
+ () ->
+ listener.onMoveEvent(
+ new MoveEvent(
+ reformEvent.getState(),
+ new Ray(
+ RuntimeUtils.getVector3(
+ reformEvent
+ .getInitialRayOrigin()),
+ RuntimeUtils.getVector3(
+ reformEvent
+ .getInitialRayDirection())),
+ new Ray(
+ RuntimeUtils.getVector3(
+ reformEvent
+ .getCurrentRayOrigin()),
+ RuntimeUtils.getVector3(
+ reformEvent
+ .getCurrentRayDirection())),
+ lastPose,
+ newPose,
+ lastScale,
+ newScale,
+ initialParent,
+ parent,
+ disposeEntity))));
+ lastPose = newPose;
+ lastScale = newScale;
+ };
+ moveEventListenersMap.put(moveEventListener, executor);
+ if (entity == null) {
+ Log.i(TAG, "setMoveEventListener called before component is attached to an Entity.");
+ return;
+ }
+ ((AndroidXrEntity) entity).addReformEventConsumer(reformEventConsumer, executor);
+ }
+
+ private void setUpAnchorPlacement(Set<AnchorPlacement> anchorPlacement) {
+
+ for (AnchorPlacement placement : anchorPlacement) {
+ if (!(placement instanceof AnchorPlacementImpl)) {
+ continue;
+ }
+ AnchorPlacementImpl placementImpl = (AnchorPlacementImpl) placement;
+ Map<PlaneSemantic, AnchorPlacementImpl> anchorablePlaneSemantic =
+ new EnumMap<>(PlaneSemantic.class);
+ for (PlaneSemantic planeSemantic : placementImpl.planeSemanticFilter) {
+ anchorablePlaneSemantic.put(planeSemantic, placementImpl);
+ }
+ for (PlaneType planeType : placementImpl.planeTypeFilter) {
+ this.anchorableFilters.put(planeType, anchorablePlaneSemantic);
+ }
+ }
+ if (!anchorableFilters.isEmpty()) {
+ this.userAnchorable = true;
+ }
+ }
+
+ @Override
+ public void removeMoveEventListener(MoveEventListener moveEventListener) {
+ moveEventListenersMap.remove(moveEventListener);
+ }
+
+ private Pair<Pose, Entity> getUpdatedReformEventPoseAndParent(ReformEvent reformEvent) {
+ if (reformEvent.getState() == ReformEvent.REFORM_STATE_END && shouldRenderPlaneShadow()) {
+ panelShadowRenderer.destroy();
+ }
+ Pose proposedPose =
+ RuntimeUtils.getPose(
+ reformEvent.getProposedPosition(), reformEvent.getProposedOrientation());
+ Pair<Pose, Entity> updatedEntity = updatePoseWithPlanes(proposedPose, reformEvent);
+ if (systemMovable) {
+ entity.setPose(updatedEntity.first);
+ }
+ return updatedEntity;
+ }
+
+ private Pair<Pose, Entity> updatePoseWithPlanes(Pose proposedPose, ReformEvent reformEvent) {
+ Session session = perceptionLibrary.getSession();
+ if (session == null) {
+ Log.w(TAG, "Unable to load perception session, cannot anchor object to a plane.");
+ return Pair.create(proposedPose, null);
+ }
+ List<Plane> planes = session.getAllPlanes();
+ if (planes.isEmpty()) {
+ return Pair.create(proposedPose, null);
+ }
+
+ // The proposed pose is relative to the activity space, it needs to be updated to be in the
+ // perception reference space to be compared against the planes..
+ Pose updatedPoseInOpenXr =
+ activitySpaceImpl.transformPoseTo(proposedPose, perceptionSpaceActivityPose);
+
+ // Create variables to store the plane in case we need to anchor to it later.
+ Plane anchorablePlane = null;
+ PlaneData anchorablePlaneData = null;
+ AnchorPlacementImpl anchorPlacement = null;
+ Long dataTimeNs = SystemClock.uptimeMillis() * 1000000;
+
+ // Update the pose based on all the known planes.
+ for (Plane plane : planes) {
+
+ PlaneData planeData = plane.getData(dataTimeNs);
+ if (planeData == null) {
+ continue;
+ }
+ Pose planePoseUpdate = updatePoseForPlane(planeData, updatedPoseInOpenXr);
+ if (planePoseUpdate == null) {
+ continue;
+ }
+ AnchorPlacementImpl newAnchorPlacement = getAnchorPlacementIfAnchorable(planeData);
+ if (newAnchorPlacement != null) {
+ anchorablePlane = plane;
+ anchorablePlaneData = planeData;
+ anchorPlacement = newAnchorPlacement;
+ }
+ updatedPoseInOpenXr = planePoseUpdate;
+ }
+
+ if (anchorablePlane != null) {
+ // If the reform state is end, we should try to anchor the entity to the plane.
+ if (reformEvent.getState() == ReformEvent.REFORM_STATE_END) {
+ Pair<Pose, AnchorEntity> resultPair =
+ anchorEntityToPlane(
+ updatedPoseInOpenXr,
+ anchorablePlane,
+ anchorablePlaneData,
+ anchorPlacement,
+ dataTimeNs);
+ if (resultPair.second != null) {
+ return Pair.create(resultPair.first, (Entity) resultPair.second);
+ }
+ } else {
+ // Otherwise, we should try to render the plane shadow onto the plane.
+ tryRenderPlaneShadow(
+ updatedPoseInOpenXr,
+ RuntimeUtils.fromPerceptionPose(anchorablePlaneData.centerPose));
+ }
+ } else if (shouldRenderPlaneShadow()) {
+ // If there is nothing to anchor to, hide the plane shadow.
+ panelShadowRenderer.hidePlane();
+ }
+
+ // If the entity was anchored and the reform is complete, update the entity to be in the
+ // activity space and remove the previously created anchor data. If
+ // shouldDisposeParentAnchor is
+ // true dispose the previously created anchor entity.
+ if (createdAnchorEntity != null
+ && entity.getParent() == createdAnchorEntity
+ && reformEvent.getState() == ReformEvent.REFORM_STATE_END) {
+
+ entity.setScale(
+ entity.getWorldSpaceScale().div(activitySpaceImpl.getWorldSpaceScale()));
+ entity.setParent(activitySpaceImpl);
+ checkAndDisposeAnchorEntity();
+ createdAnchorEntity = null;
+ createdAnchorPlacement = null;
+
+ // Move the updated pose back to the activity space.
+ Pose updatedPoseInActivitySpace =
+ perceptionSpaceActivityPose.transformPoseTo(
+ updatedPoseInOpenXr, activitySpaceImpl);
+ return Pair.create(updatedPoseInActivitySpace, activitySpaceImpl);
+ }
+
+ // If the entity has a parent, transform the pose to the parent's space.
+ Entity parent = entity.getParent();
+ if (parent == null || parent == activitySpaceImpl) {
+ return Pair.create(
+ perceptionSpaceActivityPose.transformPoseTo(
+ updatedPoseInOpenXr, activitySpaceImpl),
+ null);
+ }
+
+ return Pair.create(
+ perceptionSpaceActivityPose.transformPoseTo(updatedPoseInOpenXr, parent), null);
+ }
+
+ // Gets the anchor placement settings for the given plane data, if it is null the entity should
+ // not be anchored to this plane.
+ @Nullable
+ private AnchorPlacementImpl getAnchorPlacementIfAnchorable(PlaneData planeData) {
+ if (!userAnchorable || !systemMovable) {
+ return null;
+ }
+ Map<PlaneSemantic, AnchorPlacementImpl> anchorablePlaneSemantic =
+ anchorableFilters.get(RuntimeUtils.getPlaneType(planeData.type));
+ if (anchorablePlaneSemantic != null) {
+ if (anchorablePlaneSemantic.containsKey(
+ RuntimeUtils.getPlaneSemantic(planeData.label))) {
+ return anchorablePlaneSemantic.get(RuntimeUtils.getPlaneSemantic(planeData.label));
+ } else if (anchorablePlaneSemantic.containsKey(PlaneSemantic.ANY)) {
+ return anchorablePlaneSemantic.get(PlaneSemantic.ANY);
+ }
+ }
+ anchorablePlaneSemantic = anchorableFilters.get(PlaneType.ANY);
+ if (anchorablePlaneSemantic != null) {
+ if (anchorablePlaneSemantic.containsKey(
+ RuntimeUtils.getPlaneSemantic(planeData.label))) {
+ return anchorablePlaneSemantic.get(RuntimeUtils.getPlaneSemantic(planeData.label));
+ } else if (anchorablePlaneSemantic.containsKey(PlaneSemantic.ANY)) {
+ return anchorablePlaneSemantic.get(PlaneSemantic.ANY);
+ }
+ }
+ return null;
+ }
+
+ private Pair<Pose, AnchorEntity> anchorEntityToPlane(
+ Pose updatedPose,
+ Plane plane,
+ PlaneData anchorablePlaneData,
+ AnchorPlacementImpl anchorPlacement,
+ Long dataTimeNs) {
+ AnchorEntityImpl anchorEntity =
+ AnchorEntityImpl.createAnchorFromPlane(
+ extensions.createNode(),
+ plane,
+ new Pose(),
+ dataTimeNs,
+ activitySpaceImpl,
+ activitySpaceEntity,
+ extensions,
+ entityManager,
+ runtimeExecutor,
+ perceptionLibrary);
+ if (anchorEntity.getState() != AnchorEntityImpl.State.ANCHORED) {
+ return Pair.create(updatedPose, null);
+ }
+
+ // TODO: b/367754233: Fix the flashing when parented to a new anchor.
+ // Check the scale of the entity before the move so we can rescale when we move it to the
+ // AnchorEntity. Note the AnchorEntity has a scale of 1 so we don't need to also scale by
+ // the
+ // anchor entity's scale.
+ Vector3 entityScale = entity.getWorldSpaceScale();
+ entity.setScale(entityScale);
+ Quaternion planeRotation =
+ RuntimeUtils.fromPerceptionPose(anchorablePlaneData.centerPose).getRotation();
+ Pose rotatedPose =
+ new Pose(
+ updatedPose.getTranslation(),
+ PlaneUtils.rotateEntityToPlane(updatedPose.getRotation(), planeRotation));
+
+ Pose planeCenterPose = RuntimeUtils.fromPerceptionPose(anchorablePlaneData.centerPose);
+ Pose poseToAnchor = planeCenterPose.getInverse().compose(rotatedPose);
+ poseToAnchor =
+ new Pose(
+ new Vector3(
+ poseToAnchor.getTranslation().getX(),
+ 0f,
+ poseToAnchor.getTranslation().getZ()),
+ poseToAnchor.getRotation());
+ entity.setParent(anchorEntity);
+ // If the anchor placement settings specify that the anchor should be disposed, dispose of
+ // the
+ // previously created anchor entity.
+ checkAndDisposeAnchorEntity();
+ createdAnchorEntity = anchorEntity;
+ createdAnchorPlacement = anchorPlacement;
+ return Pair.create(poseToAnchor, anchorEntity);
+ }
+
+ @Nullable
+ private Pose updatePoseForPlane(PlaneData planeData, Pose proposedPoseInOpenXr) {
+ // Get the pose as related to the center of the plane.
+
+ Pose centerPose = RuntimeUtils.fromPerceptionPose(planeData.centerPose);
+ Pose centerPoseToProposedPose = centerPose.getInverse().compose(proposedPoseInOpenXr);
+
+ // The extents of the plane are in the X and Z directions so we can use those to determine
+ // if
+ // the point is outside the plane.
+ if (centerPoseToProposedPose.getTranslation().getX() < -planeData.extentWidth
+ || centerPoseToProposedPose.getTranslation().getX() > planeData.extentWidth
+ || centerPoseToProposedPose.getTranslation().getZ() < -planeData.extentHeight
+ || centerPoseToProposedPose.getTranslation().getZ() > planeData.extentHeight) {
+ return null;
+ }
+
+ // The distance between the point and the plane. If it is less than the minimum allowed
+ // distance, move it to the plane. We only need to take the y-value because the y-value is
+ // normal to the plane. We established above that the point is within the extents of the
+ // plane.
+ float distance = centerPoseToProposedPose.getTranslation().getY();
+ if (distance >= MIN_PLANE_ANCHOR_DISTANCE) {
+ return null;
+ }
+ centerPoseToProposedPose =
+ new Pose(
+ new Vector3(
+ centerPoseToProposedPose.getTranslation().getX(),
+ max(0f, distance),
+ centerPoseToProposedPose.getTranslation().getZ()),
+ centerPoseToProposedPose.getRotation());
+ return centerPose.compose(centerPoseToProposedPose);
+ }
+
+ private void tryRenderPlaneShadow(Pose proposedPose, Pose planePose) {
+ if (!shouldRenderPlaneShadow()) {
+ return;
+ }
+ panelShadowRenderer.updatePanelPose(proposedPose, planePose, (PanelEntityImpl) entity);
+ }
+
+ private boolean shouldRenderPlaneShadow() {
+ return entity instanceof PanelEntityImpl && systemMovable;
+ }
+
+ // Checks if there is a created anchor entity and if it should be disposed. If so, disposes of
+ // the
+ // anchor entity. Resets the createdAnchorEntity and createdAnchorPlacement to null.
+ private void checkAndDisposeAnchorEntity() {
+ if (createdAnchorEntity != null
+ && createdAnchorEntity.getChildren().isEmpty()
+ && createdAnchorPlacement != null
+ && shouldDisposeParentAnchor) {
+ createdAnchorEntity.dispose();
+ }
+ }
+
+ private static @ReformOptions.ScaleWithDistanceMode int translateScaleWithDistanceMode(
+ @ScaleWithDistanceMode int scale) {
+ switch (scale) {
+ case ScaleWithDistanceMode.DMM:
+ return ReformOptions.SCALE_WITH_DISTANCE_MODE_DMM;
+ default:
+ return ReformOptions.SCALE_WITH_DISTANCE_MODE_DEFAULT;
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/OpenXrActivityPoseHelper.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/OpenXrActivityPoseHelper.java
new file mode 100644
index 0000000..b977948
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/OpenXrActivityPoseHelper.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.util.Log;
+
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Vector3;
+
+/**
+ * A helper class for converting poses from an OpenXR pose to a pose in the activity space or world
+ * space.
+ */
+final class OpenXrActivityPoseHelper {
+ private static final String TAG = "OpenXrPoseHelper";
+ private final ActivitySpaceImpl activitySpace;
+ private final AndroidXrEntity activitySpaceRoot;
+
+ public OpenXrActivityPoseHelper(
+ ActivitySpaceImpl activitySpace, AndroidXrEntity activitySpaceRoot) {
+ this.activitySpace = activitySpace;
+ this.activitySpaceRoot = activitySpaceRoot;
+ }
+
+ /**
+ * Returns the pose relative to the activity space by transforming with the OpenXR reference
+ * space. If there is an error retrieving the openXR reference space, this will return the
+ * identity pose.
+ */
+ public Pose getPoseInActivitySpace(Pose openXrToPose) {
+ if (activitySpace == null) {
+ Log.e(TAG, "Cannot get pose in Activity Space with a null Activity Space.");
+ return new Pose();
+ }
+
+ // ActivitySpace and the ActivityPose should have unit scale and the ActivityPose should
+ // have no
+ // direct parent so we can just compose the two poses without scaling.
+ final Pose openXrToActivitySpace = activitySpace.getPoseInOpenXrReferenceSpace();
+ // TODO: b/353575470 throw an exception here instead of returning identity pose.
+ if (openXrToActivitySpace == null || openXrToPose == null) {
+ Log.e(
+ TAG,
+ "Cannot retrieve pose in underlying space for the ActivityPose. Returning"
+ + " identity pose.");
+ return new Pose();
+ }
+
+ final Pose activitySpaceToOpenXr = openXrToActivitySpace.getInverse();
+ return activitySpaceToOpenXr.compose(openXrToPose);
+ }
+
+ /** Returns the ActivityPose's pose in the activity space. */
+ public Pose getActivitySpacePose(Pose openXrToPose) {
+ if (activitySpaceRoot == null) {
+ Log.e(TAG, "Cannot get pose in World Space Pose with a null World Space Entity.");
+ return new Pose();
+ }
+
+ // ActivitySpace and the nodeless entity have unit scale and the nodeless entity has no
+ // direct
+ // parent so we can just compose the two poses without scaling.
+ final Pose activitySpaceToPose = this.getPoseInActivitySpace(openXrToPose);
+ final Pose worldSpaceToActivitySpace =
+ activitySpaceRoot.getPoseInActivitySpace().getInverse();
+ return worldSpaceToActivitySpace.compose(activitySpaceToPose);
+ }
+
+ /** Returns the scale of the WorldPose with respect to the activity space. */
+ public Vector3 getActivitySpaceScale(Vector3 openXrScale) {
+ if (activitySpace == null) {
+ Log.e(TAG, "Cannot get scale in Activity Space with a null Activity Space Entity.");
+ return new Vector3(1f, 1f, 1f);
+ }
+ return openXrScale.div(activitySpace.getWorldSpaceScale());
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/PanelEntityImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/PanelEntityImpl.java
new file mode 100644
index 0000000..cab3f98
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/PanelEntityImpl.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.util.Log;
+import android.view.SurfaceControlViewHost;
+import android.view.SurfaceControlViewHost.SurfacePackage;
+
+import androidx.xr.extensions.XrExtensions;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.extensions.node.NodeTransaction;
+import androidx.xr.scenecore.JxrPlatformAdapter.PanelEntity;
+import androidx.xr.scenecore.JxrPlatformAdapter.PixelDimensions;
+
+import java.util.Objects;
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * Implementation of PanelEntity.
+ *
+ * <p>(Requires API Level 30)
+ *
+ * <p>This entity shows 2D view on spatial panel.
+ */
+final class PanelEntityImpl extends BasePanelEntity implements PanelEntity {
+ private static final String TAG = "PanelEntity";
+ private final SurfaceControlViewHost surfaceControlViewHost;
+
+ // TODO(b/352630140): Create a static factory method for PanelEntityImpl and move the Extensions
+ // init there (out of JxrPlatformAdapterAxr)
+
+ public PanelEntityImpl(
+ Node node,
+ XrExtensions extensions,
+ EntityManager entityManager,
+ SurfaceControlViewHost surfaceControlViewHost,
+ PixelDimensions windowBoundsPx,
+ ScheduledExecutorService executor) {
+ super(node, extensions, entityManager, executor);
+ // We need to manually inform our base class of the pixelDimensions, even though the
+ // Extensions
+ // are initialized in the factory method. (ext.setWindowBounds, etc)
+ super.setPixelDimensions(windowBoundsPx);
+ this.surfaceControlViewHost = surfaceControlViewHost;
+ }
+
+ // TODO(b/352827267): Enforce minSDK API strategy - go/androidx-api-guidelines#compat-newapi
+ @Override
+ public void setPixelDimensions(PixelDimensions dimensions) {
+ super.setPixelDimensions(dimensions);
+
+ SurfacePackage surfacePackage =
+ Objects.requireNonNull(surfaceControlViewHost.getSurfacePackage());
+
+ surfaceControlViewHost.relayout(dimensions.width, dimensions.height);
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction
+ .setWindowBounds(surfacePackage, dimensions.width, dimensions.height)
+ .apply();
+ }
+ surfacePackage.release();
+ }
+
+ @SuppressWarnings("ObjectToString")
+ @Override
+ public void dispose() {
+ Log.i(TAG, "Disposing " + this);
+ surfaceControlViewHost.release();
+ super.dispose();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/PanelShadowRenderer.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/PanelShadowRenderer.java
new file mode 100644
index 0000000..9d5393e
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/PanelShadowRenderer.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.SurfaceControlViewHost;
+import android.view.SurfaceControlViewHost.SurfacePackage;
+import android.view.View;
+
+import androidx.xr.extensions.XrExtensions;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.extensions.node.NodeTransaction;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Vector3;
+
+import java.util.Objects;
+
+/** Class for rendering the border of a panel onto a perception plane. */
+class PanelShadowRenderer {
+ private static final float STROKE_WIDTH = 20f;
+ private static final float HALF_STROKE_WIDTH = STROKE_WIDTH / 2;
+ private static final float CORNER_RADIUS = 20f;
+ private static final float PANEL_BORDER_ADDED_MARGIN = 50f;
+ private final ActivitySpaceImpl activitySpaceImpl;
+ private final PerceptionSpaceActivityPoseImpl perceptionSpaceActivityPose;
+ private final XrExtensions extensions;
+ private SurfaceControlViewHost surfaceControlViewHost;
+ private final Handler handler;
+ private boolean isVisible;
+ private final Activity activity;
+ Node panelShadowNode;
+
+ /** PanelShadowView is a view with a blue border to enable the shadow effect. */
+ private static class PanelShadowView extends View {
+
+ public PanelShadowView(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onDrawForeground(Canvas canvas) {
+ super.onDrawForeground(canvas);
+ Path border = new Path();
+ border.addRoundRect(
+ HALF_STROKE_WIDTH,
+ HALF_STROKE_WIDTH,
+ canvas.getWidth() - HALF_STROKE_WIDTH,
+ canvas.getHeight() - HALF_STROKE_WIDTH,
+ CORNER_RADIUS,
+ CORNER_RADIUS,
+ Path.Direction.CW);
+ Paint paint = new Paint();
+ paint.setColor(0xFF54639F);
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(STROKE_WIDTH);
+ canvas.drawPath(border, paint);
+ }
+ }
+
+ public PanelShadowRenderer(
+ ActivitySpaceImpl activitySpaceImpl,
+ PerceptionSpaceActivityPoseImpl perceptionSpaceActivityPose,
+ Activity activity,
+ XrExtensions extensions) {
+ this.activitySpaceImpl = activitySpaceImpl;
+ this.perceptionSpaceActivityPose = perceptionSpaceActivityPose;
+ this.extensions = extensions;
+ this.activity = activity;
+ this.handler = new Handler(Looper.getMainLooper());
+ }
+
+ void updatePanelPose(
+ Pose openXrToProposedPanel, Pose openXrtoPlane, PanelEntityImpl panelEntity) {
+ // If there is no panel shadow node, create it.
+ if (panelShadowNode == null) {
+ createPanelShadow(openXrToProposedPanel, openXrtoPlane, panelEntity);
+ return;
+ }
+
+ Pose panelPoseInActivitySpace =
+ getUpdatedPanelPoseInActivitySpace(openXrToProposedPanel, openXrtoPlane);
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ if (!isVisible) {
+ transaction.setVisibility(panelShadowNode, true);
+ isVisible = true;
+ }
+ transaction
+ .setPosition(
+ panelShadowNode,
+ panelPoseInActivitySpace.getTranslation().getX(),
+ panelPoseInActivitySpace.getTranslation().getY(),
+ panelPoseInActivitySpace.getTranslation().getZ())
+ .setOrientation(
+ panelShadowNode,
+ panelPoseInActivitySpace.getRotation().getX(),
+ panelPoseInActivitySpace.getRotation().getY(),
+ panelPoseInActivitySpace.getRotation().getZ(),
+ panelPoseInActivitySpace.getRotation().getW())
+ .apply();
+ }
+ }
+
+ void hidePlane() {
+ if (!isVisible) {
+ return;
+ }
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction.setVisibility(panelShadowNode, false).apply();
+ }
+ isVisible = false;
+ }
+
+ void destroy() {
+ if (surfaceControlViewHost != null) {
+ handler.post(() -> surfaceControlViewHost.release());
+ }
+ if (panelShadowNode != null) {
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction.setParent(panelShadowNode, null).apply();
+ }
+ }
+ panelShadowNode = null;
+ }
+
+ private void createPanelShadow(
+ Pose openXrToProposedPanel, Pose openXrtoPlane, PanelEntityImpl panelEntity) {
+ View view = new PanelShadowView(activity);
+
+ // Scale the panel shadow to the size of the PanelEntity in the activity space.
+ Vector3 entityScale = panelEntity.getWorldSpaceScale();
+ float sizeX =
+ panelEntity.getPixelDimensions().width
+ * entityScale.getX()
+ / activitySpaceImpl.getWorldSpaceScale().getX()
+ + PANEL_BORDER_ADDED_MARGIN;
+ float sizeZ =
+ panelEntity.getPixelDimensions().height
+ * entityScale.getZ()
+ / activitySpaceImpl.getWorldSpaceScale().getX()
+ + PANEL_BORDER_ADDED_MARGIN;
+
+ Pose panelPoseInActivitySpace =
+ getUpdatedPanelPoseInActivitySpace(openXrToProposedPanel, openXrtoPlane);
+
+ panelShadowNode = extensions.createNode();
+
+ // The surfaceControlViewHost needs to be created on the main thread.
+ // TODO(b/352827267): Enforce minSDK API strategy - go/androidx-api-guidelines#compat-newapi
+ handler.post(
+ () -> {
+ surfaceControlViewHost =
+ new SurfaceControlViewHost(
+ activity,
+ Objects.requireNonNull(activity.getDisplay()),
+ new Binder());
+ surfaceControlViewHost.setView(view, (int) sizeX, (int) sizeZ);
+ SurfacePackage surfacePackage =
+ Objects.requireNonNull(surfaceControlViewHost.getSurfacePackage());
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction
+ .setName(panelShadowNode, "PanelRenderer")
+ .setSurfacePackage(panelShadowNode, surfacePackage)
+ .setWindowBounds(surfacePackage, (int) sizeX, (int) sizeZ)
+ .setVisibility(panelShadowNode, true)
+ .setPosition(
+ panelShadowNode,
+ panelPoseInActivitySpace.getTranslation().getX(),
+ panelPoseInActivitySpace.getTranslation().getY(),
+ panelPoseInActivitySpace.getTranslation().getZ())
+ .setOrientation(
+ panelShadowNode,
+ panelPoseInActivitySpace.getRotation().getX(),
+ panelPoseInActivitySpace.getRotation().getY(),
+ panelPoseInActivitySpace.getRotation().getZ(),
+ panelPoseInActivitySpace.getRotation().getW())
+ .setParent(panelShadowNode, activitySpaceImpl.getNode())
+ .apply();
+ }
+ surfacePackage.release();
+ });
+ isVisible = true;
+ }
+
+ private Pose getUpdatedPanelPoseInActivitySpace(
+ Pose openXrToProposedPanel, Pose openXrtoPlane) {
+ Pose planeToOpenXr = openXrtoPlane.getInverse();
+ Pose planeToPanel = planeToOpenXr.compose(openXrToProposedPanel);
+ Pose planeToProjectedPanel =
+ new Pose(
+ new Vector3(
+ planeToPanel.getTranslation().getX(),
+ 0f,
+ planeToPanel.getTranslation().getZ()),
+ planeToOpenXr
+ .getRotation()
+ .times(
+ PlaneUtils.rotateEntityToPlane(
+ openXrToProposedPanel.getRotation(),
+ openXrtoPlane.getRotation())));
+ Pose panelInOxr = openXrtoPlane.compose(planeToProjectedPanel);
+ return perceptionSpaceActivityPose.transformPoseTo(panelInOxr, activitySpaceImpl);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/PerceptionSpaceActivityPoseImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/PerceptionSpaceActivityPoseImpl.java
new file mode 100644
index 0000000..726f1f4
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/PerceptionSpaceActivityPoseImpl.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter.PerceptionSpaceActivityPose;
+import androidx.xr.scenecore.common.BaseActivityPose;
+
+/** A ActivityPose representing the origin of the OpenXR reference space. */
+final class PerceptionSpaceActivityPoseImpl extends BaseActivityPose
+ implements PerceptionSpaceActivityPose {
+
+ private final OpenXrActivityPoseHelper openXrActivityPoseHelper;
+
+ public PerceptionSpaceActivityPoseImpl(
+ ActivitySpaceImpl activitySpace, AndroidXrEntity activitySpaceRoot) {
+ this.openXrActivityPoseHelper =
+ new OpenXrActivityPoseHelper(activitySpace, activitySpaceRoot);
+ }
+
+ @Override
+ public Pose getPoseInActivitySpace() {
+ return openXrActivityPoseHelper.getPoseInActivitySpace(new Pose());
+ }
+
+ @Override
+ public Pose getActivitySpacePose() {
+ return openXrActivityPoseHelper.getActivitySpacePose(new Pose());
+ }
+
+ @Override
+ public Vector3 getActivitySpaceScale() {
+ // This ActivityPose is assumed to always have a scale of 1.0f in the OpenXR reference
+ // space.
+ return openXrActivityPoseHelper.getActivitySpaceScale(new Vector3(1f, 1f, 1f));
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/PlaneUtils.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/PlaneUtils.java
new file mode 100644
index 0000000..386028d
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/PlaneUtils.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import androidx.xr.runtime.math.Matrix4;
+import androidx.xr.runtime.math.Quaternion;
+import androidx.xr.runtime.math.Vector3;
+
+/** Utility functions for working with planes Poses. */
+final class PlaneUtils {
+ private PlaneUtils() {}
+
+ /**
+ * Gets the rotation relative to the plane to rotate the entity to be parallel to the plane.
+ *
+ * @param proposedRotation the initial rotation of the entity.
+ * @param planeRotation the rotation of the plane.
+ * @return the rotation of the panel rotated to be parallel to the plane relative to the plane.
+ */
+ static Quaternion rotateEntityToPlane(Quaternion proposedRotation, Quaternion planeRotation) {
+ // The y-vector of the plane is the normal of the plane. We need to rotate the panel so that
+ // the
+ // y-vector of the panel points along the plane and the z-vector is normal to the plane.
+ // Otherwise the panel will be sticking out of the plane.
+
+ // Create a rotation matrix from the quaternion of the plane to extract the normal.
+ Matrix4 planeMatrix = Matrix4.fromQuaternion(planeRotation);
+ // Create a rotation matrix from the quaternion for the proposed pose.
+ Matrix4 proposedRotationMatrix = Matrix4.fromQuaternion(proposedRotation);
+
+ // The z-vector of the panel should be the normal of the plane (which is the y-vector of the
+ // plane) so that the panel will be facing out of the plane.
+ float[] planeMatrixData = planeMatrix.getData();
+ Vector3 zRotation =
+ new Vector3(planeMatrixData[4], planeMatrixData[5], planeMatrixData[6])
+ .toNormalized();
+ // Get the x-vector of the panel so that we can use it to create the y-vector that is in the
+ // direction of the panel.
+ float[] poseMatrixData = proposedRotationMatrix.getData();
+ Vector3 poseVectorX =
+ new Vector3(poseMatrixData[0], poseMatrixData[1], poseMatrixData[2]).toNormalized();
+ // The y-vector is the cross product of the panel x-vector and the z-vector.
+ Vector3 yRotation = zRotation.cross(poseVectorX).toNormalized();
+ // The x-vector is the cross product of the y-vector and the z-vector so that they will all
+ // be
+ // orthogonal.
+ Vector3 xRotation = yRotation.cross(zRotation).toNormalized();
+ // Create a new rotation matrix from the x, y, and z vectors.
+ Matrix4 rotationMatrix = getRotationMatrixFromAxes(xRotation, yRotation, zRotation);
+ return rotationMatrix.getRotation();
+ }
+
+ private static Matrix4 getRotationMatrixFromAxes(Vector3 xAxis, Vector3 yAxis, Vector3 zAxis) {
+ return new Matrix4(
+ new float[] {
+ xAxis.getX(),
+ xAxis.getY(),
+ xAxis.getZ(),
+ 0f,
+ yAxis.getX(),
+ yAxis.getY(),
+ yAxis.getZ(),
+ 0f,
+ zAxis.getX(),
+ zAxis.getY(),
+ zAxis.getZ(),
+ 0f,
+ 0f,
+ 0f,
+ 0f,
+ 1f
+ });
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/PointerCaptureComponentImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/PointerCaptureComponentImpl.java
new file mode 100644
index 0000000..d16419b
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/PointerCaptureComponentImpl.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import androidx.annotation.NonNull;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.InputEventListener;
+import androidx.xr.scenecore.JxrPlatformAdapter.PointerCaptureComponent;
+import androidx.xr.scenecore.JxrPlatformAdapter.PointerCaptureComponent.StateListener;
+
+import java.util.concurrent.Executor;
+
+/** Implementation of PointerCaptureComponent. */
+final class PointerCaptureComponentImpl implements PointerCaptureComponent {
+
+ private final Executor executor;
+ private final StateListener stateListener;
+ private final InputEventListener inputListener;
+
+ private AndroidXrEntity attachedEntity;
+
+ public PointerCaptureComponentImpl(
+ @NonNull Executor executor,
+ @NonNull StateListener stateListener,
+ @NonNull InputEventListener inputListener) {
+ this.executor = executor;
+ this.stateListener = stateListener;
+ this.inputListener = inputListener;
+ }
+
+ @Override
+ public boolean onAttach(Entity entity) {
+ if (!(entity instanceof AndroidXrEntity) || attachedEntity != null) {
+ return false;
+ }
+
+ attachedEntity = (AndroidXrEntity) entity;
+ return attachedEntity.requestPointerCapture(executor, inputListener, stateListener);
+ }
+
+ @Override
+ public void onDetach(Entity entity) {
+ attachedEntity.stopPointerCapture();
+ attachedEntity = null;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/ResizableComponentImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/ResizableComponentImpl.java
new file mode 100644
index 0000000..e7932e0
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/ResizableComponentImpl.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.util.Log;
+
+import androidx.xr.extensions.Consumer;
+import androidx.xr.extensions.XrExtensions;
+import androidx.xr.extensions.node.NodeTransaction;
+import androidx.xr.extensions.node.ReformEvent;
+import androidx.xr.extensions.node.ReformOptions;
+import androidx.xr.extensions.node.Vec3;
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.ResizableComponent;
+import androidx.xr.scenecore.JxrPlatformAdapter.ResizeEvent;
+import androidx.xr.scenecore.JxrPlatformAdapter.ResizeEventListener;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+
+/** Implementation of ResizableComponent. */
+@SuppressWarnings({"BanConcurrentHashMap"})
+class ResizableComponentImpl implements ResizableComponent {
+
+ private static final String TAG = "ResizableComponentImpl";
+
+ private final XrExtensions extensions;
+ private final ExecutorService executor;
+ private final ConcurrentHashMap<ResizeEventListener, Executor> resizeEventListenerMap =
+ new ConcurrentHashMap<>();
+ // Visible for testing.
+ Consumer<ReformEvent> reformEventConsumer;
+ private Entity entity;
+ private Dimensions currentSize;
+ private Dimensions minSize;
+ private Dimensions maxSize;
+ private float fixedAspectRatio = 0.0f;
+
+ public ResizableComponentImpl(
+ ExecutorService executor,
+ XrExtensions extensions,
+ Dimensions minSize,
+ Dimensions maxSize) {
+ this.minSize = minSize;
+ this.maxSize = maxSize;
+ this.extensions = extensions;
+ this.executor = executor;
+ }
+
+ @Override
+ public boolean onAttach(Entity entity) {
+ if (this.entity != null) {
+ Log.e(TAG, "Already attached to entity " + this.entity);
+ return false;
+ }
+ this.entity = entity;
+ ReformOptions reformOptions = ((AndroidXrEntity) entity).getReformOptions();
+ reformOptions.setEnabledReform(
+ reformOptions.getEnabledReform() | ReformOptions.ALLOW_RESIZE);
+
+ // Update the current size if it's not set.
+ // TODO: b/348037292 - Remove this special case for PanelEntityImpl.
+ if (entity instanceof PanelEntityImpl && currentSize == null) {
+ currentSize = ((PanelEntityImpl) entity).getSize();
+ // TODO: b/350563642 - Add checks that size is within minsize and maxsize.
+ }
+ if (currentSize != null) {
+ reformOptions.setCurrentSize(
+ new Vec3(currentSize.width, currentSize.height, currentSize.depth));
+ }
+ reformOptions.setMinimumSize(new Vec3(minSize.width, minSize.height, minSize.depth));
+ reformOptions.setMaximumSize(new Vec3(maxSize.width, maxSize.height, maxSize.depth));
+ reformOptions.setFixedAspectRatio(fixedAspectRatio);
+ ((AndroidXrEntity) entity).updateReformOptions();
+ if (reformEventConsumer != null) {
+ ((AndroidXrEntity) entity).addReformEventConsumer(reformEventConsumer, executor);
+ }
+ return true;
+ }
+
+ @Override
+ public void onDetach(Entity entity) {
+ ReformOptions reformOptions = ((AndroidXrEntity) entity).getReformOptions();
+ reformOptions.setEnabledReform(
+ reformOptions.getEnabledReform() & ~ReformOptions.ALLOW_RESIZE);
+ ((AndroidXrEntity) entity).updateReformOptions();
+ if (reformEventConsumer != null) {
+ ((AndroidXrEntity) entity).removeReformEventConsumer(reformEventConsumer);
+ }
+ this.entity = null;
+ }
+
+ @Override
+ public void setSize(Dimensions size) {
+ // TODO: b/350821054 - Implement synchronization policy around Entity/Component updates.
+ currentSize = size;
+ if (entity == null) {
+ Log.e(TAG, "This component isn't attached to an entity.");
+ return;
+ }
+ ReformOptions reformOptions = ((AndroidXrEntity) entity).getReformOptions();
+ reformOptions.setCurrentSize(new Vec3(size.width, size.height, size.depth));
+ ((AndroidXrEntity) entity).updateReformOptions();
+ }
+
+ @Override
+ public void setMinimumSize(Dimensions minSize) {
+ this.minSize = minSize;
+ if (entity == null) {
+ Log.e(TAG, "This component isn't attached to an entity.");
+ return;
+ }
+ ReformOptions reformOptions = ((AndroidXrEntity) entity).getReformOptions();
+ reformOptions.setMinimumSize(new Vec3(minSize.width, minSize.height, minSize.depth));
+ ((AndroidXrEntity) entity).updateReformOptions();
+ }
+
+ @Override
+ public void setMaximumSize(Dimensions maxSize) {
+ this.maxSize = maxSize;
+ if (entity == null) {
+ Log.e(TAG, "This component isn't attached to an entity.");
+ return;
+ }
+ ReformOptions reformOptions = ((AndroidXrEntity) entity).getReformOptions();
+ reformOptions.setMaximumSize(new Vec3(maxSize.width, maxSize.height, maxSize.depth));
+ ((AndroidXrEntity) entity).updateReformOptions();
+ }
+
+ @Override
+ public void setFixedAspectRatio(float fixedAspectRatio) {
+ this.fixedAspectRatio = fixedAspectRatio;
+ if (entity == null) {
+ Log.e(TAG, "This component isn't attached to an entity.");
+ return;
+ }
+ ReformOptions reformOptions = ((AndroidXrEntity) entity).getReformOptions();
+ reformOptions.setFixedAspectRatio(fixedAspectRatio);
+ ((AndroidXrEntity) entity).updateReformOptions();
+ }
+
+ @Override
+ public void addResizeEventListener(
+ Executor resizeExecutor, ResizeEventListener resizeEventListener) {
+ resizeEventListenerMap.put(resizeEventListener, resizeExecutor);
+ if (reformEventConsumer != null) {
+ return;
+ }
+ reformEventConsumer =
+ reformEvent -> {
+ if (reformEvent.getType() != ReformEvent.REFORM_TYPE_RESIZE) {
+ return;
+ }
+ // Set the alpha to 0 when the resize starts and restore when resize ends, to
+ // hide the
+ // entity content while it's being resized.
+ switch (reformEvent.getState()) {
+ case ReformEvent.REFORM_STATE_START:
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ transaction
+ .setAlpha(((AndroidXrEntity) entity).getNode(), 0f)
+ .apply();
+ }
+ break;
+ case ReformEvent.REFORM_STATE_END:
+ entity.setAlpha(entity.getAlpha());
+ break;
+ default:
+ break;
+ }
+ Dimensions newSize =
+ new Dimensions(
+ reformEvent.getProposedSize().x,
+ reformEvent.getProposedSize().y,
+ reformEvent.getProposedSize().z);
+ // Update the resize affordance size.
+ setSize(newSize);
+ resizeEventListenerMap.forEach(
+ (listener, listenerExecutor) ->
+ listenerExecutor.execute(
+ () ->
+ listener.onResizeEvent(
+ new ResizeEvent(
+ RuntimeUtils
+ .getResizeEventState(
+ reformEvent
+ .getState()),
+ newSize))));
+ };
+ if (entity == null) {
+ Log.e(TAG, "This component isn't attached to an entity.");
+ return;
+ }
+ ((AndroidXrEntity) entity).addReformEventConsumer(reformEventConsumer, executor);
+ }
+
+ @Override
+ public void removeResizeEventListener(ResizeEventListener resizeEventListener) {
+ resizeEventListenerMap.remove(resizeEventListener);
+ if (resizeEventListenerMap.isEmpty()) {
+ ((AndroidXrEntity) entity).removeReformEventConsumer(reformEventConsumer);
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/RuntimeUtils.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/RuntimeUtils.java
new file mode 100644
index 0000000..09af0d1
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/RuntimeUtils.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.xr.extensions.environment.EnvironmentVisibilityState;
+import androidx.xr.extensions.environment.PassthroughVisibilityState;
+import androidx.xr.extensions.node.InputEvent.Action;
+import androidx.xr.extensions.node.InputEvent.PointerType;
+import androidx.xr.extensions.node.InputEvent.Source;
+import androidx.xr.extensions.node.Mat4f;
+import androidx.xr.extensions.node.Quatf;
+import androidx.xr.extensions.node.ReformEvent;
+import androidx.xr.extensions.node.Vec3;
+import androidx.xr.runtime.math.Matrix4;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Quaternion;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter.CameraViewActivityPose.Fov;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.InputEvent;
+import androidx.xr.scenecore.JxrPlatformAdapter.InputEvent.HitInfo;
+import androidx.xr.scenecore.JxrPlatformAdapter.PlaneSemantic;
+import androidx.xr.scenecore.JxrPlatformAdapter.PlaneType;
+import androidx.xr.scenecore.JxrPlatformAdapter.ResizeEvent;
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatialCapabilities;
+import androidx.xr.scenecore.impl.perception.Plane;
+
+final class RuntimeUtils {
+ private RuntimeUtils() {}
+
+ /** Convert JXRCore PlaneType to a PerceptionLibrary Plane.Type. */
+ static Plane.Type getPlaneType(PlaneType planeType) {
+ switch (planeType) {
+ case HORIZONTAL:
+ // TODO: b/329888869 - Allow Horizontal to work as both upward and downward facing.
+ // To do
+ // this it would have to return a collection.
+ return Plane.Type.HORIZONTAL_UPWARD_FACING;
+ case VERTICAL:
+ return Plane.Type.VERTICAL;
+ case ANY:
+ return Plane.Type.ARBITRARY;
+ }
+ return Plane.Type.ARBITRARY;
+ }
+
+ /** Convert a Perception Plane.Type to a JXRCore PlaneType. */
+ static PlaneType getPlaneType(Plane.Type planeType) {
+ switch (planeType) {
+ case HORIZONTAL_UPWARD_FACING:
+ case HORIZONTAL_DOWNWARD_FACING:
+ return PlaneType.HORIZONTAL;
+ case VERTICAL:
+ return PlaneType.VERTICAL;
+ default:
+ return PlaneType.ANY;
+ }
+ }
+
+ /** Convert a JXRCore PlaneSemantic to a PerceptionLibrary Plane.Label. */
+ static Plane.Label getPlaneLabel(PlaneSemantic planeSemantic) {
+ switch (planeSemantic) {
+ case WALL:
+ return Plane.Label.WALL;
+ case FLOOR:
+ return Plane.Label.FLOOR;
+ case CEILING:
+ return Plane.Label.CEILING;
+ case TABLE:
+ return Plane.Label.TABLE;
+ case ANY:
+ return Plane.Label.UNKNOWN;
+ }
+ return Plane.Label.UNKNOWN;
+ }
+
+ /** Convert a PerceptionLibrary Plane.Label to a JXRCore PlaneSemantic. */
+ static PlaneSemantic getPlaneSemantic(Plane.Label planeLabel) {
+ switch (planeLabel) {
+ case WALL:
+ return PlaneSemantic.WALL;
+ case FLOOR:
+ return PlaneSemantic.FLOOR;
+ case CEILING:
+ return PlaneSemantic.CEILING;
+ case TABLE:
+ return PlaneSemantic.TABLE;
+ default:
+ return PlaneSemantic.ANY;
+ }
+ }
+
+ @Nullable
+ private static HitInfo getHitInfo(
+ androidx.xr.extensions.node.InputEvent.HitInfo xrHitInfo, EntityManager entityManager) {
+ if (xrHitInfo == null
+ || xrHitInfo.getInputNode() == null
+ || xrHitInfo.getTransform() == null) {
+ return null;
+ }
+ // TODO: b/377541143 - Replace instance equality check in EntityManager.
+ Entity hitEntity = entityManager.getEntityForNode(xrHitInfo.getInputNode());
+ if (hitEntity == null) {
+ return null;
+ }
+ return new HitInfo(
+ entityManager.getEntityForNode(xrHitInfo.getInputNode()),
+ (xrHitInfo.getHitPosition() == null)
+ ? null
+ : getVector3(xrHitInfo.getHitPosition()),
+ getMatrix(xrHitInfo.getTransform()));
+ }
+
+ /**
+ * Converts an XR InputEvent to a JXRCore InputEvent.
+ *
+ * @param xrInputEvent an {@link androidx.xr.extensions.node.InputEvent} instance to be
+ * converted.
+ * @param entityManager an {@link EntityManager} instance to look up entities.
+ * @return a {@link androidx.xr.scenecore.JXRCoreRuntime.InputEvent} instance representing the
+ * input event.
+ */
+ static InputEvent getInputEvent(
+ @NonNull androidx.xr.extensions.node.InputEvent xrInputEvent,
+ @NonNull EntityManager entityManager) {
+ Vector3 origin = getVector3(xrInputEvent.getOrigin());
+ Vector3 direction = getVector3(xrInputEvent.getDirection());
+ HitInfo hitInfo = null;
+ HitInfo secondaryHitInfo = null;
+ if (xrInputEvent.getHitInfo() != null) {
+ hitInfo = getHitInfo(xrInputEvent.getHitInfo(), entityManager);
+ }
+ if (xrInputEvent.getSecondaryHitInfo() != null) {
+ secondaryHitInfo = getHitInfo(xrInputEvent.getSecondaryHitInfo(), entityManager);
+ }
+ return new InputEvent(
+ getInputEventSource(xrInputEvent.getSource()),
+ getInputEventPointerType(xrInputEvent.getPointerType()),
+ xrInputEvent.getTimestamp(),
+ origin,
+ direction,
+ getInputEventAction(xrInputEvent.getAction()),
+ hitInfo,
+ secondaryHitInfo);
+ }
+
+ @InputEvent.Source
+ static int getInputEventSource(@Source int xrInputEventSource) {
+ switch (xrInputEventSource) {
+ case androidx.xr.extensions.node.InputEvent.SOURCE_UNKNOWN:
+ return InputEvent.SOURCE_UNKNOWN;
+ case androidx.xr.extensions.node.InputEvent.SOURCE_HEAD:
+ return InputEvent.SOURCE_HEAD;
+ case androidx.xr.extensions.node.InputEvent.SOURCE_CONTROLLER:
+ return InputEvent.SOURCE_CONTROLLER;
+ case androidx.xr.extensions.node.InputEvent.SOURCE_HANDS:
+ return InputEvent.SOURCE_HANDS;
+ case androidx.xr.extensions.node.InputEvent.SOURCE_MOUSE:
+ return InputEvent.SOURCE_MOUSE;
+ case androidx.xr.extensions.node.InputEvent.SOURCE_GAZE_AND_GESTURE:
+ return InputEvent.SOURCE_GAZE_AND_GESTURE;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown Input Event Source: " + xrInputEventSource);
+ }
+ }
+
+ @InputEvent.PointerType
+ static int getInputEventPointerType(@PointerType int xrInputEventPointerType) {
+ switch (xrInputEventPointerType) {
+ case androidx.xr.extensions.node.InputEvent.POINTER_TYPE_DEFAULT:
+ return InputEvent.POINTER_TYPE_DEFAULT;
+ case androidx.xr.extensions.node.InputEvent.POINTER_TYPE_LEFT:
+ return InputEvent.POINTER_TYPE_LEFT;
+ case androidx.xr.extensions.node.InputEvent.POINTER_TYPE_RIGHT:
+ return InputEvent.POINTER_TYPE_RIGHT;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown Input Event Pointer Type: " + xrInputEventPointerType);
+ }
+ }
+
+ @InputEvent.Action
+ static int getInputEventAction(@Action int xrInputEventAction) {
+ switch (xrInputEventAction) {
+ case androidx.xr.extensions.node.InputEvent.ACTION_DOWN:
+ return InputEvent.ACTION_DOWN;
+ case androidx.xr.extensions.node.InputEvent.ACTION_UP:
+ return InputEvent.ACTION_UP;
+ case androidx.xr.extensions.node.InputEvent.ACTION_MOVE:
+ return InputEvent.ACTION_MOVE;
+ case androidx.xr.extensions.node.InputEvent.ACTION_CANCEL:
+ return InputEvent.ACTION_CANCEL;
+ case androidx.xr.extensions.node.InputEvent.ACTION_HOVER_MOVE:
+ return InputEvent.ACTION_HOVER_MOVE;
+ case androidx.xr.extensions.node.InputEvent.ACTION_HOVER_ENTER:
+ return InputEvent.ACTION_HOVER_ENTER;
+ case androidx.xr.extensions.node.InputEvent.ACTION_HOVER_EXIT:
+ return InputEvent.ACTION_HOVER_EXIT;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown Input Event Action: " + xrInputEventAction);
+ }
+ }
+
+ @ResizeEvent.ResizeState
+ static int getResizeEventState(@ReformEvent.ReformState int resizeState) {
+ switch (resizeState) {
+ case ReformEvent.REFORM_STATE_UNKNOWN:
+ return ResizeEvent.RESIZE_STATE_UNKNOWN;
+ case ReformEvent.REFORM_STATE_START:
+ return ResizeEvent.RESIZE_STATE_START;
+ case ReformEvent.REFORM_STATE_ONGOING:
+ return ResizeEvent.RESIZE_STATE_ONGOING;
+ case ReformEvent.REFORM_STATE_END:
+ return ResizeEvent.RESIZE_STATE_END;
+ default:
+ throw new IllegalArgumentException("Unknown Resize State: " + resizeState);
+ }
+ }
+
+ static Matrix4 getMatrix(Mat4f xrMatrix) {
+ float[] matrixData = new float[16];
+ for (int i = 0; i < 4; i++) {
+ System.arraycopy(xrMatrix.m[i], 0, matrixData, i * 4, 4);
+ }
+ return new Matrix4(matrixData);
+ }
+
+ static Pose getPose(Vec3 position, Quatf quatf) {
+ return new Pose(
+ new Vector3(position.x, position.y, position.z),
+ new Quaternion(quatf.x, quatf.y, quatf.z, quatf.w));
+ }
+
+ static Vector3 getVector3(Vec3 vec3) {
+ return new Vector3(vec3.x, vec3.y, vec3.z);
+ }
+
+ /**
+ * Converts from a perception pose type.
+ *
+ * @param perceptionPose a {@code androidx.xr.scenecore.impl.perception.Pose} instance
+ * representing the pose.
+ */
+ static Pose fromPerceptionPose(androidx.xr.scenecore.impl.perception.Pose perceptionPose) {
+ Vector3 translation =
+ new Vector3(perceptionPose.tx(), perceptionPose.ty(), perceptionPose.tz());
+ Quaternion rotation =
+ new Quaternion(
+ perceptionPose.qx(),
+ perceptionPose.qy(),
+ perceptionPose.qz(),
+ perceptionPose.qw());
+ return new Pose(translation, rotation);
+ }
+
+ /**
+ * Converts from a pose to a perception pose type.
+ *
+ * @param pose a {@code androidx.xr.runtime.math.Pose} instance representing the pose.
+ */
+ static androidx.xr.scenecore.impl.perception.Pose poseToPerceptionPose(Pose pose) {
+ return new androidx.xr.scenecore.impl.perception.Pose(
+ pose.getTranslation().getX(),
+ pose.getTranslation().getY(),
+ pose.getTranslation().getZ(),
+ pose.getRotation().getX(),
+ pose.getRotation().getY(),
+ pose.getRotation().getZ(),
+ pose.getRotation().getW());
+ }
+
+ /**
+ * Converts to a JXRCore FOV from a perception FOV type.
+ *
+ * @param perceptionFov a {@code androidx.xr.scenecore.impl.perception.Fov} instance
+ * representing the FOV.
+ */
+ static Fov fovFromPerceptionFov(androidx.xr.scenecore.impl.perception.Fov perceptionFov) {
+ return new Fov(
+ perceptionFov.getAngleLeft(),
+ perceptionFov.getAngleRight(),
+ perceptionFov.getAngleUp(),
+ perceptionFov.getAngleDown());
+ }
+
+ /**
+ * Converts to a perception FOV from a JXRCore FOV type.
+ *
+ * @param fov a {@code androidx.xr.scenecore.JxrPlatformAdapter.CameraViewActivityPose.Fov}
+ * instance representing the FOV.
+ */
+ static androidx.xr.scenecore.impl.perception.Fov perceptionFovFromFov(Fov fov) {
+ return new androidx.xr.scenecore.impl.perception.Fov(
+ fov.angleLeft, fov.angleRight, fov.angleUp, fov.angleDown);
+ }
+
+ /**
+ * Converts from the Extensions spatial capabilities to the runtime spatial capabilities.
+ *
+ * @param extCapabilities a {@link androidx.xr.extensions.space.SpatialCapabilities} instance to
+ * be converted.
+ */
+ static SpatialCapabilities convertSpatialCapabilities(
+ androidx.xr.extensions.space.SpatialCapabilities extCapabilities) {
+ @SpatialCapabilities.SpatialCapability int capabilities = 0;
+ if (extCapabilities.get(
+ androidx.xr.extensions.space.SpatialCapabilities.SPATIAL_UI_CAPABLE)) {
+ capabilities |= SpatialCapabilities.SPATIAL_CAPABILITY_UI;
+ }
+ if (extCapabilities.get(
+ androidx.xr.extensions.space.SpatialCapabilities.SPATIAL_3D_CONTENTS_CAPABLE)) {
+ capabilities |= SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT;
+ }
+ if (extCapabilities.get(
+ androidx.xr.extensions.space.SpatialCapabilities.PASSTHROUGH_CONTROL_CAPABLE)) {
+ capabilities |= SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL;
+ }
+ if (extCapabilities.get(
+ androidx.xr.extensions.space.SpatialCapabilities.APP_ENVIRONMENTS_CAPABLE)) {
+ capabilities |= SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT;
+ }
+ if (extCapabilities.get(
+ androidx.xr.extensions.space.SpatialCapabilities.SPATIAL_AUDIO_CAPABLE)) {
+ capabilities |= SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO;
+ }
+ if (extCapabilities.get(
+ androidx.xr.extensions.space.SpatialCapabilities
+ .SPATIAL_ACTIVITY_EMBEDDING_CAPABLE)) {
+ capabilities |= SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY;
+ }
+
+ return new SpatialCapabilities(capabilities);
+ }
+
+ /**
+ * Converts from the Extensions environment visibility state to the runtime environment
+ * visibility state.
+ *
+ * @param environmentState a {@link
+ * androidx.xr.extensions.environment.EnvironmentVisibilityState} instance to be converted.
+ */
+ static boolean getIsSpatialEnvironmentPreferenceActive(
+ @EnvironmentVisibilityState.State int environmentState) {
+ return environmentState == EnvironmentVisibilityState.APP_VISIBLE;
+ }
+
+ static float getPassthroughOpacity(PassthroughVisibilityState passthroughVisibilityState) {
+ int passthroughState = passthroughVisibilityState.getCurrentState();
+ if (passthroughState == PassthroughVisibilityState.DISABLED) {
+ return 0.0f;
+ } else {
+ float opacity = passthroughVisibilityState.getOpacity();
+ if (opacity > 0.0f) {
+ return opacity;
+ } else {
+ // When passthrough is enabled, the opacity should be greater than zero.
+ Log.e(
+ "RuntimeUtils",
+ "Passthrough is enabled, but active opacity value is "
+ + opacity
+ + ". Opacity should be greater than zero when Passthrough is"
+ + " enabled.");
+ return 1.0f;
+ }
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/SoundPoolExtensionsWrapperImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/SoundPoolExtensionsWrapperImpl.java
new file mode 100644
index 0000000..1bd0c5e
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/SoundPoolExtensionsWrapperImpl.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.media.SoundPool;
+
+import androidx.xr.extensions.media.SoundPoolExtensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.PointSourceAttributes;
+import androidx.xr.scenecore.JxrPlatformAdapter.SoundFieldAttributes;
+import androidx.xr.scenecore.JxrPlatformAdapter.SoundPoolExtensionsWrapper;
+
+/** Implementation of {@link SoundPoolExtensionsWrapper}. */
+final class SoundPoolExtensionsWrapperImpl implements SoundPoolExtensionsWrapper {
+
+ private final SoundPoolExtensions extensions;
+
+ SoundPoolExtensionsWrapperImpl(SoundPoolExtensions extensions) {
+ this.extensions = extensions;
+ }
+
+ @Override
+ public int play(
+ SoundPool soundPool,
+ int soundId,
+ PointSourceAttributes attributes,
+ float volume,
+ int priority,
+ int loop,
+ float rate) {
+ androidx.xr.extensions.media.PointSourceAttributes extAttributes =
+ MediaUtils.convertPointSourceAttributesToExtensions(attributes);
+ return extensions.playAsPointSource(
+ soundPool, soundId, extAttributes, volume, priority, loop, rate);
+ }
+
+ @Override
+ public int play(
+ SoundPool soundPool,
+ int soundId,
+ SoundFieldAttributes attributes,
+ float volume,
+ int priority,
+ int loop,
+ float rate) {
+ androidx.xr.extensions.media.SoundFieldAttributes extAttributes =
+ MediaUtils.convertSoundFieldAttributesToExtensions(attributes);
+
+ return extensions.playAsSoundField(
+ soundPool, soundId, extAttributes, volume, priority, loop, rate);
+ }
+
+ @Override
+ public int getSpatialSourceType(SoundPool soundPool, int streamId) {
+ return MediaUtils.convertExtensionsToSourceType(
+ extensions.getSpatialSourceType(soundPool, streamId));
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/SpatialEnvironmentImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/SpatialEnvironmentImpl.java
new file mode 100644
index 0000000..089f446
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/SpatialEnvironmentImpl.java
@@ -0,0 +1,574 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.app.Activity;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.xr.extensions.XrExtensionResult;
+import androidx.xr.extensions.XrExtensions;
+import androidx.xr.extensions.environment.EnvironmentVisibilityState;
+import androidx.xr.extensions.environment.PassthroughVisibilityState;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.extensions.node.NodeTransaction;
+import androidx.xr.extensions.passthrough.PassthroughState;
+import androidx.xr.extensions.space.SpatialState;
+import androidx.xr.scenecore.JxrPlatformAdapter.ExrImageResource;
+import androidx.xr.scenecore.JxrPlatformAdapter.GltfModelResource;
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatialCapabilities;
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatialEnvironment;
+
+import com.google.androidxr.splitengine.SplitEngineSubspaceManager;
+import com.google.androidxr.splitengine.SubspaceNode;
+import com.google.ar.imp.apibindings.ImpressApi;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/** Concrete implementation of SpatialEnvironment / XR Wallpaper for Android XR. */
+// TODO(b/373435470): Remove "deprecation"
+@SuppressWarnings({"deprecation", "BanSynchronizedMethods"})
+final class SpatialEnvironmentImpl implements SpatialEnvironment {
+
+ public static final String TAG = "SpatialEnvironmentImpl";
+
+ public static final String SKYBOX_NODE_NAME = "EnvironmentSkyboxNode";
+ public static final String GEOMETRY_NODE_NAME = "EnvironmentGeometryNode";
+ public static final String PASSTHROUGH_NODE_NAME = "EnvironmentPassthroughNode";
+ @VisibleForTesting final Node passthroughNode;
+ private final XrExtensions xrExtensions;
+ private final Node rootEnvironmentNode;
+ private final boolean useSplitEngine;
+ @Nullable private Activity activity;
+ // Used to represent the geometry
+ private Node geometryNode;
+ // the "xrExtensions.setEnvironment" call effectively makes a node into a skybox
+ private Node skyboxNode;
+ private SubspaceNode geometrySubspaceSplitEngine;
+ private int geometrySubspaceImpressNode;
+ private boolean isSpatialEnvironmentPreferenceActive = false;
+ @Nullable private SpatialEnvironmentPreference spatialEnvironmentPreference = null;
+
+ // The active passthrough opacity value is updated with every opacity change event. A null value
+ // indicates it has not yet been initialized and the value should be read from the
+ // spatialStateProvider.
+ private Float activePassthroughOpacity = null;
+ // Initialized to null to let system control opacity until preference is explicitly set.
+ private Float passthroughOpacityPreference = null;
+ private SplitEngineSubspaceManager splitEngineSubspaceManager;
+ private ImpressApi impressApi;
+ private final Supplier<SpatialState> spatialStateProvider;
+ private SpatialState previousSpatialState = null;
+
+ private final Set<Consumer<Boolean>> onSpatialEnvironmentChangedListeners =
+ Collections.synchronizedSet(new HashSet<>());
+
+ private final Set<Consumer<Float>> onPassthroughOpacityChangedListeners =
+ Collections.synchronizedSet(new HashSet<>());
+
+ public SpatialEnvironmentImpl(
+ @NonNull Activity activity,
+ @NonNull XrExtensions xrExtensions,
+ @NonNull Node rootSceneNode,
+ @NonNull Supplier<SpatialState> spatialStateProvider,
+ boolean useSplitEngine) {
+ this.activity = activity;
+ this.xrExtensions = xrExtensions;
+ this.passthroughNode = xrExtensions.createNode();
+ this.rootEnvironmentNode = xrExtensions.createNode();
+ this.geometryNode = xrExtensions.createNode();
+ this.skyboxNode = xrExtensions.createNode();
+ this.useSplitEngine = useSplitEngine;
+ this.spatialStateProvider = spatialStateProvider;
+
+ try (NodeTransaction transaction = xrExtensions.createNodeTransaction()) {
+ transaction
+ .setName(geometryNode, GEOMETRY_NODE_NAME)
+ .setName(skyboxNode, SKYBOX_NODE_NAME)
+ .setName(passthroughNode, PASSTHROUGH_NODE_NAME)
+ .setParent(geometryNode, rootEnvironmentNode)
+ .setParent(skyboxNode, rootEnvironmentNode)
+ .setParent(passthroughNode, rootSceneNode)
+ .apply();
+ }
+ }
+
+ // TODO: Remove these once we know the Equals() and Hashcode() methods are correct.
+ boolean hasEnvironmentVisibilityChanged(@NonNull SpatialState spatialState) {
+ if (previousSpatialState == null) {
+ return true;
+ }
+
+ final EnvironmentVisibilityState previousEnvironmentVisibility =
+ previousSpatialState.getEnvironmentVisibility();
+ final EnvironmentVisibilityState currentEnvironmentVisibility =
+ spatialState.getEnvironmentVisibility();
+
+ if (previousEnvironmentVisibility.getCurrentState()
+ != currentEnvironmentVisibility.getCurrentState()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ // TODO: Remove these once we know the Equals() and Hashcode() methods are correct.
+ boolean hasPassthroughVisibilityChanged(@NonNull SpatialState spatialState) {
+ if (previousSpatialState == null) {
+ return true;
+ }
+
+ final PassthroughVisibilityState previousPassthroughVisibility =
+ previousSpatialState.getPassthroughVisibility();
+ final PassthroughVisibilityState currentPassthroughVisibility =
+ spatialState.getPassthroughVisibility();
+
+ if (previousPassthroughVisibility.getCurrentState()
+ != currentPassthroughVisibility.getCurrentState()) {
+ return true;
+ }
+
+ if (previousPassthroughVisibility.getOpacity()
+ != currentPassthroughVisibility.getOpacity()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ // Package Private enum to return which spatial states have changed.
+ enum ChangedSpatialStates {
+ ENVIRONMENT_CHANGED,
+ PASSTHROUGH_CHANGED
+ }
+
+ // Package Private method to set the current passthrough opacity and
+ // isSpatialEnvironmentPreferenceActive from JxrPlatformAdapterAxr.
+ // This method is synchronized because it sets several internal state variables at once, which
+ // should be treated as an atomic set. We could consider replacing with AtomicReferences.
+ @CanIgnoreReturnValue
+ synchronized EnumSet<ChangedSpatialStates> setSpatialState(@NonNull SpatialState spatialState) {
+ EnumSet<ChangedSpatialStates> changedSpatialStates =
+ EnumSet.noneOf(ChangedSpatialStates.class);
+ boolean passthroughVisibilityChanged = hasPassthroughVisibilityChanged(spatialState);
+ if (passthroughVisibilityChanged) {
+ changedSpatialStates.add(ChangedSpatialStates.PASSTHROUGH_CHANGED);
+ this.activePassthroughOpacity =
+ RuntimeUtils.getPassthroughOpacity(spatialState.getPassthroughVisibility());
+ }
+
+ // TODO: b/371082454 - Check if the app is in FSM to ensure APP_VISIBLE refers to the
+ // current
+ // app and not another app that is visible.
+ boolean environmentVisibilityChanged = hasEnvironmentVisibilityChanged(spatialState);
+ if (environmentVisibilityChanged) {
+ changedSpatialStates.add(ChangedSpatialStates.ENVIRONMENT_CHANGED);
+ this.isSpatialEnvironmentPreferenceActive =
+ RuntimeUtils.getIsSpatialEnvironmentPreferenceActive(
+ spatialState.getEnvironmentVisibility().getCurrentState());
+ }
+
+ this.previousSpatialState = spatialState;
+ return changedSpatialStates;
+ }
+
+ /** Flushes passthrough Node state to XrExtensions. */
+ private void applyPassthroughChange(float opacityVal) {
+ if (opacityVal > 0.0f) {
+ try (NodeTransaction transaction = xrExtensions.createNodeTransaction()) {
+ transaction
+ .setPassthroughState(
+ passthroughNode, opacityVal, PassthroughState.PASSTHROUGH_MODE_MAX)
+ .apply();
+ }
+ } else {
+ try (NodeTransaction transaction = xrExtensions.createNodeTransaction()) {
+ transaction
+ .setPassthroughState(
+ passthroughNode,
+ /* passthroughOpacity= */ 0.0f,
+ PassthroughState.PASSTHROUGH_MODE_OFF)
+ .apply();
+ }
+ }
+ }
+
+ @Override
+ @NonNull
+ @CanIgnoreReturnValue
+ public SetPassthroughOpacityPreferenceResult setPassthroughOpacityPreference(
+ @Nullable Float opacity) {
+ // To work around floating-point precision issues, the opacity preference is documented to
+ // clamp
+ // to 0.0f if it is set below 1% opacity and it clamps to 1.0f if it is set above 99%
+ // opacity.
+
+ @Nullable
+ Float newPassthroughOpacityPreference =
+ opacity == null
+ ? null
+ : (opacity < 0.01f ? 0.0f : (opacity > 0.99f ? 1.0f : opacity));
+
+ if (Objects.equals(newPassthroughOpacityPreference, passthroughOpacityPreference)) {
+ return SetPassthroughOpacityPreferenceResult.CHANGE_APPLIED;
+ }
+
+ passthroughOpacityPreference = newPassthroughOpacityPreference;
+
+ // to this method when they are removed
+
+ // Passthrough should be enabled only if the user has explicitly set the
+ // PassthroughOpacityPreference to a non-null and non-zero value, otherwise disabled.
+ if (passthroughOpacityPreference != null && passthroughOpacityPreference != 0.0f) {
+ applyPassthroughChange(passthroughOpacityPreference.floatValue());
+ } else {
+ applyPassthroughChange(0.0f);
+ }
+
+ if (RuntimeUtils.convertSpatialCapabilities(
+ spatialStateProvider.get().getSpatialCapabilities())
+ .hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL)) {
+ return SetPassthroughOpacityPreferenceResult.CHANGE_APPLIED;
+ } else {
+ return SetPassthroughOpacityPreferenceResult.CHANGE_PENDING;
+ }
+ }
+
+ // Synchronized because we may need to update the entire Spatial State if the opacity has not
+ // been
+ // initialized previously.
+ @Override
+ public synchronized float getCurrentPassthroughOpacity() {
+ if (activePassthroughOpacity == null) {
+ setSpatialState(spatialStateProvider.get());
+ }
+ return activePassthroughOpacity.floatValue();
+ }
+
+ @Override
+ @Nullable
+ public Float getPassthroughOpacityPreference() {
+ return passthroughOpacityPreference;
+ }
+
+ // This is called on the Activity's UI thread - so we should be careful to not block it.
+ synchronized void firePassthroughOpacityChangedEvent(float opacity) {
+ for (Consumer<Float> listener : onPassthroughOpacityChangedListeners) {
+ listener.accept(opacity);
+ }
+ }
+
+ @Override
+ public void addOnPassthroughOpacityChangedListener(Consumer<Float> listener) {
+ onPassthroughOpacityChangedListeners.add(listener);
+ }
+
+ @Override
+ public void removeOnPassthroughOpacityChangedListener(Consumer<Float> listener) {
+ onPassthroughOpacityChangedListeners.remove(listener);
+ }
+
+ /**
+ * Stages updates to the CPM graph for the Environment to reflect a new skybox preference. If
+ * skybox is null, this method unsets the client skybox preference, resulting in the system
+ * skybox being used.
+ */
+ private void applySkybox(@Nullable ExrImageResourceImpl skybox) {
+ // We need to create a new node here because we can't re-use the old CPM node when changing
+ // geometry and skybox.
+ try (NodeTransaction transaction = xrExtensions.createNodeTransaction()) {
+ transaction.setParent(skyboxNode, null).apply();
+ }
+
+ this.skyboxNode = xrExtensions.createNode();
+ try (NodeTransaction transaction = xrExtensions.createNodeTransaction()) {
+ transaction
+ .setName(skyboxNode, SKYBOX_NODE_NAME)
+ .setParent(skyboxNode, rootEnvironmentNode);
+ if (skybox != null) {
+ transaction.setEnvironment(skyboxNode, skybox.getToken());
+ }
+ transaction.apply();
+ }
+ }
+
+ /**
+ * Stages updates to the CPM graph for the Environment to reflect a new geometry preference. If
+ * geometry is null, this method unsets the client geometry preference, resulting in the system
+ * geometry being used.
+ */
+ private void applyGeometryLegacy(@Nullable GltfModelResourceImpl geometry) {
+ // We need to create a new node here because we can't re-use the old CPM node when changing
+ // geometry and skybox.
+ try (NodeTransaction transaction = xrExtensions.createNodeTransaction()) {
+ transaction.setParent(geometryNode, null).apply();
+ }
+ this.geometryNode = xrExtensions.createNode();
+ try (NodeTransaction transaction = xrExtensions.createNodeTransaction()) {
+ transaction
+ .setName(geometryNode, GEOMETRY_NODE_NAME)
+ .setParent(geometryNode, rootEnvironmentNode);
+ if (geometry != null) {
+ transaction.setGltfModel(geometryNode, geometry.getExtensionModelToken());
+ }
+ transaction.apply();
+ }
+ }
+
+ /**
+ * Stages updates to the CPM graph for the Environment to reflect a new geometry preference. If
+ * geometry is null, this method unsets the client geometry preference, resulting in the system
+ * geometry being used.
+ *
+ * @throws IllegalStateException if called on a thread other than the main thread.
+ */
+ private void applyGeometrySplitEngine(@Nullable GltfModelResourceImplSplitEngine geometry) {
+ if (!Looper.getMainLooper().isCurrentThread()) {
+ throw new IllegalStateException("This method must be called on the main thread.");
+ }
+
+ int prevGeometrySubspaceImpressNode = -1;
+ SubspaceNode prevGeometrySubspaceSplitEngine = null;
+ if (geometrySubspaceSplitEngine != null) {
+ prevGeometrySubspaceSplitEngine = geometrySubspaceSplitEngine;
+ geometrySubspaceSplitEngine = null;
+ prevGeometrySubspaceImpressNode = geometrySubspaceImpressNode;
+ geometrySubspaceImpressNode = -1;
+ }
+
+ geometrySubspaceImpressNode = impressApi.createImpressNode();
+ String subspaceName = "geometry_subspace_" + geometrySubspaceImpressNode;
+
+ geometrySubspaceSplitEngine =
+ splitEngineSubspaceManager.createSubspace(
+ subspaceName, geometrySubspaceImpressNode);
+
+ try (NodeTransaction transaction = xrExtensions.createNodeTransaction()) {
+ transaction
+ .setName(geometrySubspaceSplitEngine.subspaceNode, GEOMETRY_NODE_NAME)
+ .setParent(geometrySubspaceSplitEngine.subspaceNode, rootEnvironmentNode)
+ .setPosition(geometrySubspaceSplitEngine.subspaceNode, 0.0f, 0.0f, 0.0f)
+ .setScale(geometrySubspaceSplitEngine.subspaceNode, 1.0f, 1.0f, 1.0f)
+ .setOrientation(
+ geometrySubspaceSplitEngine.subspaceNode, 0.0f, 0.0f, 0.0f, 1.0f)
+ .apply();
+ }
+
+ if (geometry != null) {
+ int modelImpressNode =
+ impressApi.instanceGltfModel(
+ geometry.getExtensionModelToken(), /* enableCollider= */ false);
+ impressApi.setImpressNodeParent(modelImpressNode, geometrySubspaceImpressNode);
+ }
+
+ if (prevGeometrySubspaceSplitEngine != null && prevGeometrySubspaceImpressNode != -1) {
+ // Detach the previous geometry subspace from the root environment node.
+ try (NodeTransaction transaction = xrExtensions.createNodeTransaction()) {
+ transaction.setParent(prevGeometrySubspaceSplitEngine.subspaceNode, null).apply();
+ }
+ splitEngineSubspaceManager.deleteSubspace(prevGeometrySubspaceSplitEngine.subspaceId);
+ impressApi.destroyImpressNode(prevGeometrySubspaceImpressNode);
+
+ prevGeometrySubspaceSplitEngine = null;
+ prevGeometrySubspaceImpressNode = -1;
+ }
+ }
+
+ void onSplitEngineReady(SplitEngineSubspaceManager subspaceManager, ImpressApi api) {
+ this.splitEngineSubspaceManager = subspaceManager;
+ this.impressApi = api;
+ }
+
+ @Override
+ @NonNull
+ @CanIgnoreReturnValue
+ public SetSpatialEnvironmentPreferenceResult setSpatialEnvironmentPreference(
+ @Nullable SpatialEnvironmentPreference newPreference) {
+ // TODO: b/378914007 This method is not safe for reentrant calls.
+
+ if (Objects.equals(newPreference, spatialEnvironmentPreference)) {
+ return SetSpatialEnvironmentPreferenceResult.CHANGE_APPLIED;
+ }
+
+ GltfModelResource newGeometry = newPreference == null ? null : newPreference.geometry;
+ GltfModelResource prevGeometry =
+ spatialEnvironmentPreference == null ? null : spatialEnvironmentPreference.geometry;
+ ExrImageResource newSkybox = newPreference == null ? null : newPreference.skybox;
+ ExrImageResource prevSkybox =
+ spatialEnvironmentPreference == null ? null : spatialEnvironmentPreference.skybox;
+
+ // TODO(b/329907079): Map GltfModelResourceImplSplitEngine to GltfModelResource in Impl
+ // Layer
+ if (newGeometry != null) {
+ if (useSplitEngine && !(newGeometry instanceof GltfModelResourceImplSplitEngine)) {
+ throw new IllegalArgumentException(
+ "SplitEngine is enabled but the prefererred geometry is not of type"
+ + " GltfModelResourceImplSplitEngine.");
+ } else if (!useSplitEngine && !(newGeometry instanceof GltfModelResourceImpl)) {
+ throw new IllegalArgumentException(
+ "SplitEngine is disabled but the prefererred geometry is not of type"
+ + " GltfModelResourceImpl.");
+ }
+ }
+
+ // TODO b/329907079: Map ExrImageResourceImpl to ExrImageResource in Impl Layer
+ if (newSkybox != null && !(newSkybox instanceof ExrImageResourceImpl)) {
+ throw new IllegalArgumentException(
+ "The prefererred skybox is not of type ExrImageResourceImpl.");
+ }
+
+ if (!Objects.equals(newGeometry, prevGeometry)) {
+ // TODO: b/354711945 - Remove this check once we migrate completely to SplitEngine
+ if (useSplitEngine) {
+ applyGeometrySplitEngine((GltfModelResourceImplSplitEngine) newGeometry);
+ } else {
+ applyGeometryLegacy((GltfModelResourceImpl) newGeometry);
+ }
+ }
+
+ if (!Objects.equals(newSkybox, prevSkybox)) {
+ // TODO: b/371221872 - If the preference object is non-null but contains a null skybox,
+ // we
+ // should set a black skybox. (This may require a change to the attach/detach logic
+ // below.)
+ applySkybox((ExrImageResourceImpl) newSkybox);
+ }
+
+ try (NodeTransaction transaction = xrExtensions.createNodeTransaction()) {
+ if (newSkybox == null && newGeometry == null) {
+ xrExtensions.detachSpatialEnvironment(
+ activity,
+ (result) -> logXrExtensionResult("detachSpatialEnvironment", result),
+ Runnable::run);
+ } else {
+ xrExtensions.attachSpatialEnvironment(
+ activity,
+ rootEnvironmentNode,
+ (result) -> logXrExtensionResult("attachSpatialEnvironment", result),
+ Runnable::run);
+ }
+ }
+
+ this.spatialEnvironmentPreference = newPreference;
+
+ if (RuntimeUtils.convertSpatialCapabilities(
+ spatialStateProvider.get().getSpatialCapabilities())
+ .hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT)) {
+ return SetSpatialEnvironmentPreferenceResult.CHANGE_APPLIED;
+ } else {
+ return SetSpatialEnvironmentPreferenceResult.CHANGE_PENDING;
+ }
+ }
+
+ private void logXrExtensionResult(String prefix, XrExtensionResult result) {
+ // TODO: b/376934871 - Better error handling?
+ switch (result.getResult()) {
+ case XrExtensionResult.XR_RESULT_SUCCESS:
+ case XrExtensionResult.XR_RESULT_SUCCESS_NOT_VISIBLE:
+ Log.d(TAG, prefix + ": success (" + result.getResult() + ")");
+ break;
+ case XrExtensionResult.XR_RESULT_IGNORED_ALREADY_APPLIED:
+ Log.d(TAG, prefix + ": ignored, already applied (" + result.getResult() + ")");
+ break;
+ case XrExtensionResult.XR_RESULT_ERROR_NOT_ALLOWED:
+ case XrExtensionResult.XR_RESULT_ERROR_SYSTEM:
+ Log.e(TAG, prefix + ": error (" + result.getResult() + ")");
+ break;
+ default:
+ Log.e(TAG, prefix + ": Unexpected return value (" + result.getResult() + ")");
+ break;
+ }
+ }
+
+ @Override
+ @Nullable
+ public SpatialEnvironmentPreference getSpatialEnvironmentPreference() {
+ return spatialEnvironmentPreference;
+ }
+
+ @Override
+ public boolean isSpatialEnvironmentPreferenceActive() {
+ return isSpatialEnvironmentPreferenceActive;
+ }
+
+ // This is called on the Activity's UI thread - so we should be careful to not block it.
+ synchronized void fireOnSpatialEnvironmentChangedEvent(
+ boolean isSpatialEnvironmentPreferenceActive) {
+ for (Consumer<Boolean> listener : onSpatialEnvironmentChangedListeners) {
+ listener.accept(isSpatialEnvironmentPreferenceActive);
+ }
+ }
+
+ @Override
+ public void addOnSpatialEnvironmentChangedListener(Consumer<Boolean> listener) {
+ onSpatialEnvironmentChangedListeners.add(listener);
+ }
+
+ @Override
+ public void removeOnSpatialEnvironmentChangedListener(Consumer<Boolean> listener) {
+ onSpatialEnvironmentChangedListeners.remove(listener);
+ }
+
+ /**
+ * Disposes of the environment and all of its resources.
+ *
+ * <p>This should be called when the environment is no longer needed.
+ */
+ public void dispose() {
+ if (useSplitEngine) {
+ if (this.geometrySubspaceSplitEngine != null) {
+ try (NodeTransaction transaction = xrExtensions.createNodeTransaction()) {
+ transaction.setParent(geometrySubspaceSplitEngine.subspaceNode, null).apply();
+ }
+ this.splitEngineSubspaceManager.deleteSubspace(
+ this.geometrySubspaceSplitEngine.subspaceId);
+ this.geometrySubspaceSplitEngine = null;
+ impressApi.destroyImpressNode(geometrySubspaceImpressNode);
+ }
+ }
+ this.activePassthroughOpacity = null;
+ this.passthroughOpacityPreference = null;
+ try (NodeTransaction transaction = xrExtensions.createNodeTransaction()) {
+ transaction
+ .setParent(skyboxNode, null)
+ .setParent(geometryNode, null)
+ .setParent(passthroughNode, null)
+ .apply();
+ }
+ this.geometrySubspaceSplitEngine = null;
+ this.geometrySubspaceImpressNode = 0;
+ this.splitEngineSubspaceManager = null;
+ this.impressApi = null;
+ this.spatialEnvironmentPreference = null;
+ this.isSpatialEnvironmentPreferenceActive = false;
+ this.onPassthroughOpacityChangedListeners.clear();
+ this.onSpatialEnvironmentChangedListeners.clear();
+ // TODO: b/376934871 - Check async results.
+ xrExtensions.detachSpatialEnvironment(activity, (result) -> {}, Runnable::run);
+ this.activity = null;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/StereoSurfaceEntityImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/StereoSurfaceEntityImpl.java
new file mode 100644
index 0000000..27584f0
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/StereoSurfaceEntityImpl.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.view.Surface;
+
+import androidx.xr.extensions.XrExtensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.StereoSurfaceEntity;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * Implementation of a RealityCore StereoSurfaceEntityImpl.
+ *
+ * <p>Unimplemented, this requires split engine.
+ */
+final class StereoSurfaceEntityImpl extends AndroidXrEntity implements StereoSurfaceEntity {
+ @StereoMode private int stereoMode;
+
+ public StereoSurfaceEntityImpl(
+ Entity parentEntity,
+ XrExtensions extensions,
+ EntityManager entityManager,
+ ScheduledExecutorService executor) {
+ super(extensions.createNode(), extensions, entityManager, executor);
+ throw new UnsupportedOperationException();
+ }
+
+ @SuppressWarnings("ObjectToString")
+ @Override
+ public void dispose() {
+ super.dispose();
+ }
+
+ @Override
+ public void setStereoMode(@StereoMode int mode) {
+ stereoMode = mode;
+ }
+
+ @Override
+ public int getStereoMode() {
+ return stereoMode;
+ }
+
+ @Override
+ public void setDimensions(Dimensions dimensions) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Dimensions getDimensions() {
+ return new Dimensions(0.0f, 0.0f, 0.0f);
+ }
+
+ @Override
+ public Surface getSurface() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/StereoSurfaceEntitySplitEngineImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/StereoSurfaceEntitySplitEngineImpl.java
new file mode 100644
index 0000000..60286b6
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/StereoSurfaceEntitySplitEngineImpl.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.view.Surface;
+
+import androidx.xr.extensions.XrExtensions;
+import androidx.xr.extensions.node.NodeTransaction;
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.StereoSurfaceEntity;
+
+import com.google.androidxr.splitengine.SplitEngineSubspaceManager;
+import com.google.androidxr.splitengine.SubspaceNode;
+import com.google.ar.imp.apibindings.ImpressApi;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * Implementation of a RealityCore StereoSurfaceEntitySplitEngine.
+ *
+ * <p>This is used to create an entity that contains a StereoSurfacePanel using the Split Engine
+ * route.
+ */
+final class StereoSurfaceEntitySplitEngineImpl extends AndroidXrEntity
+ implements StereoSurfaceEntity {
+ private final ImpressApi impressApi;
+ private final SplitEngineSubspaceManager splitEngineSubspaceManager;
+ private final SubspaceNode subspace;
+ // TODO: b/362520810 - Wrap impress nodes w/ Java class.
+ private final int panelImpressNode;
+ private final int subspaceImpressNode;
+ @StereoMode private int stereoMode = StereoSurfaceEntity.StereoMode.SIDE_BY_SIDE;
+ // This are the default dimensions Impress starts with for the Quad mesh used as the canvas
+ private Dimensions dimensions = new Dimensions(2.0f, 2.0f, 2.0f);
+
+ public StereoSurfaceEntitySplitEngineImpl(
+ Entity parentEntity,
+ ImpressApi impressApi,
+ SplitEngineSubspaceManager splitEngineSubspaceManager,
+ XrExtensions extensions,
+ EntityManager entityManager,
+ ScheduledExecutorService executor,
+ @StereoMode int stereoMode) {
+ super(extensions.createNode(), extensions, entityManager, executor);
+ this.impressApi = impressApi;
+ this.splitEngineSubspaceManager = splitEngineSubspaceManager;
+ this.stereoMode = stereoMode;
+ setParent(parentEntity);
+
+ // TODO(b/377906324): - Punt this logic to the UI thread, so that applications can create
+ // StereoSurface entities from any thread.
+
+ // System will only render Impress nodes that are parented by this subspace node.
+ this.subspaceImpressNode = impressApi.createImpressNode();
+ String subspaceName = "stereo_surface_panel_entity_subspace_" + subspaceImpressNode;
+
+ this.subspace =
+ splitEngineSubspaceManager.createSubspace(subspaceName, subspaceImpressNode);
+
+ try (NodeTransaction transaction = extensions.createNodeTransaction()) {
+ // Make the Entity node a parent of the subspace node.
+ transaction.setParent(subspace.subspaceNode, this.node).apply();
+ }
+ // The CPM node hierarchy is: Entity CPM node --- parent of ---> Subspace CPM node.
+ this.panelImpressNode = impressApi.createStereoSurface(stereoMode);
+ impressApi.setImpressNodeParent(panelImpressNode, subspaceImpressNode);
+ }
+
+ @SuppressWarnings("ObjectToString")
+ @Override
+ public void dispose() {
+ // TODO(b/377906324): - Punt this logic to the UI thread, so that applications can destroy
+ // StereoSurface entities from any thread.
+ splitEngineSubspaceManager.deleteSubspace(subspace.subspaceId);
+ impressApi.destroyImpressNode(subspaceImpressNode);
+ super.dispose();
+ }
+
+ @Override
+ public void setStereoMode(@StereoMode int mode) {
+ stereoMode = mode;
+ impressApi.setStereoModeForStereoSurface(panelImpressNode, mode);
+ }
+
+ @Override
+ public void setDimensions(Dimensions dimensions) {
+ // TODO(b/377906324): - Punt this logic to the UI thread, so that applications can call this
+ // method from any thread.
+ this.dimensions = dimensions;
+ impressApi.setCanvasDimensionsForStereoSurface(
+ panelImpressNode, dimensions.width, dimensions.height);
+ }
+
+ @Override
+ public Dimensions getDimensions() {
+ return dimensions;
+ }
+
+ @Override
+ @StereoMode
+ public int getStereoMode() {
+ return stereoMode;
+ }
+
+ @Override
+ public Surface getSurface() {
+ // TODO(b/377906324) - Either cache the surface in the constructor, or change this interface
+ // to
+ // return a Future.
+ return impressApi.getSurfaceFromStereoSurface(panelImpressNode);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/SystemSpaceEntityImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/SystemSpaceEntityImpl.java
new file mode 100644
index 0000000..7d6fe4d
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/SystemSpaceEntityImpl.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.xr.extensions.XrExtensions;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.runtime.math.Matrix4;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter;
+
+import java.io.Closeable;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * A parentless system-controlled JXRCore Entity that defines its own coordinate space.
+ *
+ * <p>It is expected to be the soft root of its own parent-child entity hierarchy.
+ */
+abstract class SystemSpaceEntityImpl extends AndroidXrEntity
+ implements JxrPlatformAdapter.SystemSpaceEntity {
+
+ protected Pose openXrReferenceSpacePose;
+ protected Vector3 worldSpaceScale = new Vector3(1f, 1f, 1f);
+ // Visible for testing.
+ Closeable nodeTransformCloseable;
+ private OnSpaceUpdatedListener spaceUpdatedListener;
+ private Executor spaceUpdatedExecutor;
+
+ public SystemSpaceEntityImpl(
+ Node node,
+ XrExtensions extensions,
+ EntityManager entityManager,
+ ScheduledExecutorService executor) {
+ super(node, extensions, entityManager, executor);
+
+ // The underlying CPM node is always expected to be updated in response to changes to
+ // the coordinate space represented by a SystemSpaceEntityImpl so we subscribe at
+ // construction.
+ subscribeToNodeTransform(node, executor);
+ }
+
+ /** Called when the underlying space has changed. */
+ public void onSpaceUpdated() {
+ if (spaceUpdatedListener != null) {
+ this.spaceUpdatedExecutor.execute(() -> spaceUpdatedListener.onSpaceUpdated());
+ }
+ }
+
+ /** Registers the SDK layer / application's listener for space updates. */
+ @Override
+ public void setOnSpaceUpdatedListener(
+ @Nullable OnSpaceUpdatedListener listener, @Nullable Executor executor) {
+ this.spaceUpdatedListener = listener;
+ this.spaceUpdatedExecutor = executor == null ? this.executor : executor;
+ }
+
+ /**
+ * Returns the pose relative to an OpenXR reference space.
+ *
+ * <p>The OpenXR reference space is the space returned by {@link
+ * XrExtensions#getOpenXrActivitySpaceType()}
+ */
+ public Pose getPoseInOpenXrReferenceSpace() {
+ return openXrReferenceSpacePose;
+ }
+
+ /**
+ * Sets the pose and scale of the entity in an OpenXR reference space and should call the
+ * onSpaceUpdated() callback to signal a change in the underlying space.
+ *
+ * @param openXrReferenceSpaceTransform 4x4 transformation matrix of the entity in an OpenXR
+ * reference space. The OpenXR reference space is of the type defined by the {@link
+ * XrExtensions#getOpenXrActivitySpaceType()} method.
+ */
+ protected void setOpenXrReferenceSpacePose(Matrix4 openXrReferenceSpaceTransform) {
+ // TODO: b/353511649 - Make SystemSpaceEntityImpl thread safe.
+ this.openXrReferenceSpacePose =
+ Matrix4Ext.getUnscaled(openXrReferenceSpaceTransform).getPose();
+
+ // TODO: b/367780918 - Consider using Matrix4.scale when it is fixed.
+ // Retrieve the scale from the matrix. The scale can be retrieved from the matrix by getting
+ // the magnitude of one of the rows of the matrix. Note that we are assuming uniform scale.
+ // SpaceFlinger might apply a scale to the task node, for example if the user caused the
+ // main
+ // panel to scale in Homespace mode.
+ float data00 = openXrReferenceSpaceTransform.getData()[0];
+ float data01 = openXrReferenceSpaceTransform.getData()[1];
+ float data02 = openXrReferenceSpaceTransform.getData()[2];
+ float scale = (float) Math.sqrt(data00 * data00 + data01 * data01 + data02 * data02);
+ this.worldSpaceScale = new Vector3(scale, scale, scale);
+ this.setScaleInternal(new Vector3(scale, scale, scale));
+ onSpaceUpdated();
+ }
+
+ /**
+ * Subscribes to the node's transform update events and caches the pose by calling
+ * setOpenXrReferenceSpacePose().
+ *
+ * @param node The node to subscribe to.
+ * @param executor The executor to run the callback on.
+ */
+ private void subscribeToNodeTransform(Node node, Executor executor) {
+ this.nodeTransformCloseable =
+ node.subscribeToTransform(
+ (transform) ->
+ setOpenXrReferenceSpacePose(
+ RuntimeUtils.getMatrix(transform.getTransform())),
+ executor);
+ }
+
+ @Override
+ public Vector3 getWorldSpaceScale() {
+ return this.worldSpaceScale;
+ }
+
+ /** Unsubscribes from the node's transform update events. */
+ private void unsubscribeFromNodeTransform() {
+ try {
+ this.nodeTransformCloseable.close();
+ } catch (Exception e) {
+ Log.w(
+ "SystemSpaceEntity",
+ "Could not close node transform subscription with error: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public void dispose() {
+ unsubscribeFromNodeTransform();
+ super.dispose();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/Anchor.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/Anchor.java
new file mode 100644
index 0000000..fc6d89f
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/Anchor.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl.perception;
+
+import android.os.IBinder;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.nio.ByteBuffer;
+import java.util.UUID;
+
+/**
+ * An Anchor keeps track of a position in the real world. This can be an arbitrary position or a
+ * position relative to a trackable.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class Anchor {
+ /**
+ * anchorId is an ID used as a reference to this anchor. It is equal to the XrSpace handle for
+ * anchor in the OpenXR session managed by the perception library.
+ */
+ private final long anchorId;
+
+ /**
+ * anchorToken is a Binder reference of the anchor, it can be used to import the anchor by an
+ * OpenXR session.
+ */
+ private final IBinder anchorToken;
+
+ /* UUID of the anchor.*/
+ private UUID uuid;
+
+ public Anchor(long anchorId, @NonNull IBinder anchorToken) {
+ this.anchorId = anchorId;
+ this.anchorToken = anchorToken;
+ this.uuid = null;
+ }
+
+ Anchor(AnchorData anchorData) {
+ this.anchorToken = anchorData.anchorToken;
+ this.anchorId = anchorData.anchorId;
+ this.uuid = null;
+ }
+
+ /** Returns the anchorId(native pointer) of the anchor. */
+ public long getAnchorId() {
+ return anchorId;
+ }
+
+ /**
+ * Returns an IBInder token to this anchor. This is used for sharing the anchor with other
+ * OpenXR sessions in other processes (such as SpaceFlinger).
+ */
+ @NonNull
+ public IBinder getAnchorToken() {
+ return anchorToken;
+ }
+
+ /**
+ * Detaches the anchor. Any process which has imported this anchor will see the anchor as
+ * untracked.
+ */
+ public boolean detach() {
+ return detachAnchor(anchorId);
+ }
+
+ /**
+ * Persists the anchor. When the anchor is persisted successfully, that means the environment
+ * and the anchor's relative position to the environment is saved to storage. The anchor could
+ * be recreated in the same environment in the following sessions with the original pose by
+ * calling Session.createAnchor(uuid).
+ *
+ * @return the UUID of the anchor being persisted.
+ */
+ @Nullable
+ public UUID persist() {
+ byte[] uuidBytes = persistAnchor(anchorId);
+ if (uuidBytes == null) {
+ return null;
+ }
+ ByteBuffer byteBuffer = ByteBuffer.wrap(uuidBytes);
+ long high = byteBuffer.getLong();
+ long low = byteBuffer.getLong();
+ this.uuid = new UUID(high, low);
+ return this.uuid;
+ }
+
+ private native boolean detachAnchor(long anchorId);
+
+ private native byte[] persistAnchor(long anchorId);
+
+ /**
+ * Gets the Persistent State of the anchor. If the anchor doesn't have any persist requests yet,
+ * it returns PersistState.PERSIST_NOT_REQUESTED. If the anchor was found by the uuid, it
+ * returns the current persist state of the anchor.
+ *
+ * @return the persist state of the anchor.
+ */
+ @NonNull
+ public PersistState getPersistState() {
+ if (uuid == null) {
+ return PersistState.PERSIST_NOT_REQUESTED;
+ }
+ PersistState state =
+ getPersistState(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits());
+ if (state == null) {
+ return PersistState.NOT_VALID;
+ }
+ return state;
+ }
+
+ private native PersistState getPersistState(long highBits, long lowBits);
+
+ /** Persistent State Enum for the anchor. It is retrieved with the getPersistState function. */
+ public enum PersistState {
+ PERSIST_NOT_REQUESTED,
+ PERSIST_PENDING,
+ PERSISTED,
+ NOT_VALID,
+ }
+
+ /** Data returned from native OpenXR layer when creating an anchor. */
+ static class AnchorData {
+ long anchorId;
+ IBinder anchorToken;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/Fov.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/Fov.java
new file mode 100644
index 0000000..0b6758e
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/Fov.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl.perception;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Represents the field of view. <a
+ * href="https://registry.khronos.org/OpenXR/specs/1.0/man/html/XrFovf.html">...</a>
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public final class Fov {
+ private final float angleLeft;
+ private final float angleRight;
+ private final float angleUp;
+ private final float angleDown;
+
+ public Fov(float angleLeft, float angleRight, float angleUp, float angleDown) {
+ this.angleLeft = angleLeft;
+ this.angleRight = angleRight;
+ this.angleUp = angleUp;
+ this.angleDown = angleDown;
+ }
+
+ /**
+ * Returns the angle of the left side of the field of view. For a symmetric field of view, this
+ * value is negative. *
+ */
+ public float getAngleLeft() {
+ return angleLeft;
+ }
+
+ /** Returns the angle of the right part of the field of view. */
+ public float getAngleRight() {
+ return angleRight;
+ }
+
+ /** Returns the angle of the top part of the field of view. */
+ public float getAngleUp() {
+ return angleUp;
+ }
+
+ /**
+ * Returns the angle of the bottom side of the field of view. For a symmetric field of view,
+ * this value is negative. *
+ */
+ public float getAngleDown() {
+ return angleDown;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object instanceof Fov) {
+ Fov that = (Fov) object;
+ return this.angleLeft == that.angleLeft
+ && this.angleRight == that.angleRight
+ && this.angleUp == that.angleUp
+ && this.angleDown == that.angleDown;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 31 * result + Float.floatToIntBits(angleLeft);
+ result = 31 * result + Float.floatToIntBits(angleRight);
+ result = 31 * result + Float.floatToIntBits(angleUp);
+ result = 31 * result + Float.floatToIntBits(angleDown);
+ return result;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/PerceptionLibrary.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/PerceptionLibrary.java
new file mode 100644
index 0000000..29dfaba
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/PerceptionLibrary.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl.perception;
+
+import android.app.Activity;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.xr.scenecore.impl.perception.exceptions.FailedToInitializeException;
+import androidx.xr.scenecore.impl.perception.exceptions.LibraryLoadingException;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * A library for handling perception on Jetpack XR.This will create and manage one or more OpenXR
+ * sessions and manage calls to it.
+ */
+@SuppressWarnings({"BanSynchronizedMethods", "BanConcurrentHashMap"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class PerceptionLibrary {
+ private static final String TAG = "PerceptionLibrary";
+
+ private static final String NATIVE_LIBRARY_NAME = "androidx.xr.runtime.openxr";
+ private static final ConcurrentHashMap<Activity, Session> activitySessionMap =
+ new ConcurrentHashMap<>();
+
+ @SuppressWarnings("NonFinalStaticField")
+ private static volatile boolean libraryLoaded = false;
+
+ private Session session = null;
+
+ public PerceptionLibrary() {}
+
+ @SuppressWarnings("VisiblySynchronized")
+ protected static synchronized void loadLibraryAsync(@NonNull String nativeLibraryName) {
+ if (libraryLoaded) {
+ return;
+ }
+ Log.i(TAG, "Loading native library: " + nativeLibraryName);
+ try {
+ System.loadLibrary(nativeLibraryName);
+ } catch (UnsatisfiedLinkError e) {
+ Log.e(TAG, "Unable to load " + nativeLibraryName);
+ return;
+ }
+ libraryLoaded = true;
+ }
+
+ /**
+ * Initializes a new session. It is an error to call this if there is already a session
+ * associated with the activity.
+ *
+ * @param activity This activity to associate with the OpenXR session.
+ * @param referenceSpaceType The base space type of the session.
+ * @param executor This executor is used to poll the OpenXR event loop.
+ * @return a new Session or null if there was an error creating the session.
+ * @throws java.util.concurrent.ExecutionException if there was an error to initialize the
+ * session. The cause of the ExecutionException will be an IllegalStateException if a valid
+ * session already exists, LibraryLoadingException if the internal native library failed to
+ * load, and a FailedToInitializeException if there was a failure to initialize the session
+ * internally.
+ */
+ // ResolvableFuture is marked as RestrictTo(LIBRARY_GROUP_PREFIX), which is intended for classes
+ // within AndroidX. We're in the process of migrating to AndroidX. Without suppressing this
+ // warning, however, we get a build error - go/bugpattern/RestrictTo.
+ @SuppressWarnings({"RestrictTo", "AsyncSuffixFuture"})
+ @Nullable
+ public ListenableFuture<Session> initSession(
+ @NonNull Activity activity,
+ @PerceptionLibraryConstants.OpenXrSpaceType int referenceSpaceType,
+ @NonNull ExecutorService executor) {
+
+ ResolvableFuture<Session> future = ResolvableFuture.create();
+ executor.execute(
+ () -> {
+ // TODO: b/373922954 - Early out of some of these operations if the future is
+ // cancelled.
+ if (!libraryLoaded) {
+ loadLibraryAsync(NATIVE_LIBRARY_NAME);
+ }
+ if (!libraryLoaded) {
+ Log.i(TAG, "Cannot init session since the native library failed to load.");
+ future.setException(
+ new LibraryLoadingException("Native library failed to load."));
+ return;
+ }
+ Log.i(TAG, stringFromJNI());
+ if (this.session != null) {
+ future.setException(new IllegalStateException("Session already exists."));
+ return;
+ }
+ if (activitySessionMap.containsKey(activity)) {
+ future.setException(
+ new IllegalStateException(
+ "Session already exists for the provided activity."));
+ return;
+ }
+ Session session = new Session(activity, referenceSpaceType, executor);
+ if (!session.initSession()) {
+ Log.e(TAG, "Failed to initialize a session.");
+ future.setException(
+ new FailedToInitializeException("Failed to initialize a session."));
+ return;
+ }
+
+ Log.i(TAG, "Loaded perception library.");
+ // Do another check to make sure another session wasn't created for this
+ // activity
+ // while we were initializing it.
+ if (activitySessionMap.putIfAbsent(activity, session) != null) {
+ future.setException(
+ new IllegalStateException(
+ "Session already exists for the provided activity."));
+ }
+ this.session = session;
+ future.set(this.session);
+ });
+ return future;
+ }
+
+ /** Returns the previously created session or null. */
+ @SuppressWarnings("VisiblySynchronized")
+ @Nullable
+ public synchronized Session getSession() {
+ return session;
+ }
+
+ /**
+ * JNI function that returns a string. This is a temporary function to validate the JNI
+ * implementation.
+ */
+ private native String stringFromJNI();
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/PerceptionLibraryConstants.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/PerceptionLibraryConstants.java
new file mode 100644
index 0000000..62f1486
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/PerceptionLibraryConstants.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl.perception;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Class to manage constants for the PerceptionLibrary. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public final class PerceptionLibraryConstants {
+
+ /** View OpenXR Reference Space Type. */
+ public static final int OPEN_XR_SPACE_TYPE_VIEW = 1;
+
+ /** Local OpenXR Reference Space Type. */
+ public static final int OPEN_XR_SPACE_TYPE_LOCAL = 2;
+
+ /** Stage OpenXR Reference Space Type. */
+ public static final int OPEN_XR_SPACE_TYPE_STAGE = 3;
+
+ /** Local Floor OpenXR Reference Space Type. */
+ public static final int OPEN_XR_SPACE_TYPE_LOCAL_FLOOR = 1000426000;
+
+ /** Unbounded OpenXR Reference Space Type. */
+ public static final int OPEN_XR_SPACE_TYPE_UNBOUNDED = 1000467000;
+
+ /** IntDef for OpenXR Reference Space Types. */
+ @IntDef({
+ OPEN_XR_SPACE_TYPE_VIEW,
+ OPEN_XR_SPACE_TYPE_LOCAL,
+ OPEN_XR_SPACE_TYPE_STAGE,
+ OPEN_XR_SPACE_TYPE_LOCAL_FLOOR,
+ OPEN_XR_SPACE_TYPE_UNBOUNDED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface OpenXrSpaceType {}
+
+ private PerceptionLibraryConstants() {}
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/Plane.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/Plane.java
new file mode 100644
index 0000000..6c58b8d
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/Plane.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl.perception;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A Plane is a type of Trackable that maps to a real world plane (e.g. a floor, a wall, or a table)
+ */
+// TODO: b/329875042 - Add a utility to convert this to an ARCore plane.
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class Plane implements Trackable {
+ private static final String TAG = "PerceptionPlane";
+ ArrayList<Anchor> attachedAnchors = new ArrayList<>();
+ Long planeId = 0L;
+ @PerceptionLibraryConstants.OpenXrSpaceType int referenceSpaceType = 0;
+
+ public Plane(
+ @SuppressWarnings("AutoBoxing") @NonNull Long planeId,
+ @PerceptionLibraryConstants.OpenXrSpaceType int referenceSpaceType) {
+ this.planeId = planeId;
+ this.referenceSpaceType = referenceSpaceType;
+ }
+
+ /**
+ * Creates an anchor on the trackable for a point in world space relative to the trackable.
+ *
+ * @param pose Specifies the pose relative to the center point of the trackable.
+ * @param timeNs The monotonic time retrieved using the SystemClock's uptimeMillis() or
+ * uptimeNanos() at which to get the pose, in nanoseconds. If time is null or negative, the
+ * current time will be used.
+ */
+ @Override
+ @Nullable
+ public Anchor createAnchor(
+ @NonNull Pose pose, @SuppressWarnings("AutoBoxing") @Nullable Long timeNs) {
+ Anchor.AnchorData anchorData =
+ createAnchorOnPlane(planeId, pose, timeNs == null ? -1 : timeNs);
+ if (anchorData == null) {
+ Log.i(TAG, "Failed to create an anchor.");
+ return null;
+ }
+ Log.i(TAG, "Creating an anchor result:" + anchorData.anchorToken);
+ Anchor anchor = new Anchor(anchorData);
+ attachedAnchors.add(anchor);
+ return anchor;
+ }
+
+ /**
+ * Retrieves data associated with the plane.
+ *
+ * @param timeNs The monotonic time retrieved using the SystemClock's uptimeMillis() or
+ * uptimeNanos() at which to get the pose, in nanoseconds. If time is null or negative, the
+ * current time will be used.
+ */
+ @Nullable
+ public PlaneData getData(@SuppressWarnings("AutoBoxing") @Nullable Long timeNs) {
+ return getPlaneData(planeId, referenceSpaceType, timeNs == null ? -1 : timeNs);
+ }
+
+ /** Returns all anchors attached to this trackable. */
+ @NonNull
+ @Override
+ public List<Anchor> getAnchors() {
+ return attachedAnchors;
+ }
+
+ private native PlaneData getPlaneData(
+ long planeId, int referenceSpaceType, long monotonicTimeNs);
+
+ private native Anchor.AnchorData createAnchorOnPlane(
+ long planeId, Pose pose, long monotonicTimeNs);
+
+ /**
+ * The direction of the plane. These enum values must be kept in sync with OpenXR
+ * XrPlaneTypeANDROID.
+ */
+ public enum Type {
+ /** A horizontal plane facing downward (e.g. a ceiling). */
+ HORIZONTAL_DOWNWARD_FACING(0),
+ /** A horizontal plane facing upward (e.g. floor or tabletop). */
+ HORIZONTAL_UPWARD_FACING(1),
+ /** A vertical plane (e.g. a wall). */
+ VERTICAL(2),
+ /** Any plane type. */
+ ARBITRARY(3);
+
+ public final int intValue;
+
+ Type(int intValue) {
+ this.intValue = intValue;
+ }
+
+ // Converts a native code to the corresponding enum value.
+ static Type forNumber(int intValue) {
+ for (Type type : values()) {
+ if (type.intValue == intValue) {
+ return type;
+ }
+ }
+ return ARBITRARY;
+ }
+ }
+
+ /**
+ * Data on the plane that can be used to determine the type of object that it is. These enum
+ * values must be kept in sync with OpenXR XrPlaneLabelANDROID.
+ */
+ public enum Label {
+ /** It was not possible to label the plane. */
+ UNKNOWN(0),
+ /** The plane is a wall. */
+ WALL(1),
+ /** The plane is a floor. */
+ FLOOR(2),
+ /** The plane is a ceiling */
+ CEILING(3),
+ /** The plane is a table */
+ TABLE(4);
+
+ public final int intValue;
+
+ Label(int intValue) {
+ this.intValue = intValue;
+ }
+
+ // Converts a native code to the corresponding enum value.
+ static Label forNumber(int intValue) {
+ for (Label label : values()) {
+ if (label.intValue == intValue) {
+ return label;
+ }
+ }
+ return UNKNOWN;
+ }
+ }
+
+ /** Data returned from the native OpenXR to describe the plane. */
+ public static class PlaneData {
+ /**
+ * The pose of the center of the plane. The positive y-axis of the pose will point
+ * perpendicular out of the plane's surface.
+ */
+ @NonNull public final Pose centerPose;
+
+ /** The width of the plane. */
+ public final float extentWidth;
+
+ /** The height of the plane. */
+ public final float extentHeight;
+
+ /** The direction of the plane. */
+ @NonNull public final Plane.Type type;
+
+ /** A label that can be used to determine the type of object that the plane is. */
+ @NonNull public final Plane.Label label;
+
+ public PlaneData(
+ @NonNull Pose centerPose,
+ float extentWidth,
+ float extentHeight,
+ int type,
+ int label) {
+ this.centerPose = centerPose;
+ this.extentWidth = extentWidth;
+ this.extentHeight = extentHeight;
+ this.type = Plane.Type.forNumber(type);
+ this.label = Plane.Label.forNumber(label);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object instanceof PlaneData) {
+ PlaneData that = (PlaneData) object;
+ return this.centerPose.equals(that.centerPose)
+ && this.extentWidth == that.extentWidth
+ && this.extentHeight == that.extentHeight
+ && this.type == that.type
+ && this.label == that.label;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 31 * result + centerPose.hashCode();
+ result = 31 * result + Float.floatToIntBits(extentWidth);
+ result = 31 * result + Float.floatToIntBits(extentHeight);
+ result = 31 * result + type.hashCode();
+ result = 31 * result + label.hashCode();
+ return result;
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/Pose.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/Pose.java
new file mode 100644
index 0000000..48a5d40b
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/Pose.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl.perception;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/** A translation and rotation of an object (e.g. Trackable, Anchor). */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public final class Pose {
+ private float tx;
+ private float ty;
+ private float tz;
+ private float qx;
+ private float qy;
+ private float qz;
+ private float qw;
+
+ public Pose(float tx, float ty, float tz, float qx, float qy, float qz, float qw) {
+ this.tx = tx;
+ this.ty = ty;
+ this.tz = tz;
+ this.qx = qx;
+ this.qy = qy;
+ this.qz = qz;
+ this.qw = qw;
+ }
+
+ // A pose at zero position and orientation.
+ @NonNull
+ public static Pose identity() {
+ return new Pose(0, 0, 0, 0, 0, 0, 1);
+ }
+
+ public void updateTranslation(float tx, float ty, float tz) {
+ this.tx = tx;
+ this.ty = ty;
+ this.tz = tz;
+ }
+
+ public void updateRotation(float qx, float qy, float qz, float qw) {
+ this.qx = qx;
+ this.qy = qy;
+ this.qz = qz;
+ this.qw = qw;
+ }
+
+ /** Returns the X components of this pose's translation. */
+ public float tx() {
+ return tx;
+ }
+
+ /** Returns the Y components of this pose's translation. */
+ public float ty() {
+ return ty;
+ }
+
+ /** Returns the Z components of this pose's translation. */
+ public float tz() {
+ return tz;
+ }
+
+ /** Returns the X component of this pose's rotation quaternion. */
+ public float qx() {
+ return qx;
+ }
+
+ /** Returns the Y component of this pose's rotation quaternion. */
+ public float qy() {
+ return qy;
+ }
+
+ /** Returns the Z component of this pose's rotation quaternion. */
+ public float qz() {
+ return qz;
+ }
+
+ /** Returns the W component of this pose's rotation quaternion. */
+ public float qw() {
+ return qw;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object instanceof Pose) {
+ Pose that = (Pose) object;
+ return this.tx == that.tx
+ && this.ty == that.ty
+ && this.tz == that.tz
+ && this.qx == that.qx
+ && this.qy == that.qy
+ && this.qz == that.qz
+ && this.qw == that.qw;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 31 * result + Float.floatToIntBits(tx);
+ result = 31 * result + Float.floatToIntBits(ty);
+ result = 31 * result + Float.floatToIntBits(tz);
+ result = 31 * result + Float.floatToIntBits(qx);
+ result = 31 * result + Float.floatToIntBits(qy);
+ result = 31 * result + Float.floatToIntBits(qz);
+ result = 31 * result + Float.floatToIntBits(qw);
+ return result;
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/Session.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/Session.java
new file mode 100644
index 0000000..ff61a8f
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/Session.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl.perception;
+
+import static java.util.stream.Collectors.toCollection;
+
+import android.app.Activity;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+
+/** A perception session is used to manage and call into the OpenXR session */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class Session {
+ /** Java constant representation of OpenXR's XR_NULL_HANDLE. */
+ public static final long XR_NULL_HANDLE = 0;
+
+ private static final String TAG = "PerceptionSession";
+ Activity activity;
+ @PerceptionLibraryConstants.OpenXrSpaceType int openXrReferenceSpaceType;
+ Executor executor;
+ HashMap<Long, Plane> foundPlanes = new HashMap<>();
+
+ Session(
+ Activity activity,
+ @PerceptionLibraryConstants.OpenXrSpaceType int openXrReferenceSpaceType,
+ Executor executor) {
+ this.activity = activity;
+ this.openXrReferenceSpaceType = openXrReferenceSpaceType;
+ this.executor = executor;
+ }
+
+ boolean initSession() {
+ boolean xrLoaded = createOpenXrSession(activity, openXrReferenceSpaceType);
+ if (!xrLoaded) {
+ Log.e(TAG, "Failed to load OpenXR session.");
+ return false;
+ }
+ return true;
+ }
+
+ /** Creates an anchor for the specified plane type. */
+ @Nullable
+ public Anchor createAnchor(
+ float minWidth, float minHeight, @NonNull Plane.Type type, @NonNull Plane.Label label) {
+ Anchor.AnchorData anchorData =
+ getAnchor(minWidth, minHeight, type.intValue, label.intValue);
+ if (anchorData == null) {
+ Log.i(TAG, "Failed to create an anchor.");
+ return null;
+ }
+ Log.i(TAG, "Creating an anchor result:" + anchorData.anchorToken);
+ return new Anchor(anchorData);
+ }
+
+ /**
+ * Recreates a persisted anchor for the specified UUID from the storage.
+ *
+ * @return the anchor if the operation succeeds.
+ */
+ @Nullable
+ public Anchor createAnchorFromUuid(@Nullable UUID uuid) {
+ if (uuid == null) {
+ Log.i(TAG, "UUID is null and cannot create a persisted anchor.");
+ return null;
+ }
+ Anchor.AnchorData anchorData =
+ createPersistedAnchor(
+ uuid.getMostSignificantBits(), uuid.getLeastSignificantBits());
+ if (anchorData == null) {
+ Log.i(TAG, "Failed to create a persisted anchor.");
+ return null;
+ }
+ Log.i(TAG, "Creating a persisted anchor result:" + anchorData.anchorToken);
+ return new Anchor(anchorData);
+ }
+
+ /**
+ * Returns all planes that can be found in the scene. The order is not guaranteed to be
+ * consistent. An anchor can be created from a plane object and it will be tied to that plane.
+ */
+ @NonNull
+ public List<Plane> getAllPlanes() {
+ return getPlanes().stream()
+ .map(
+ nativeId ->
+ foundPlanes.computeIfAbsent(
+ nativeId,
+ id -> new Plane(nativeId, openXrReferenceSpaceType)))
+ .collect(toCollection(ArrayList::new));
+ }
+
+ /** Returns the current head pose using the current timestamp in OpenXR. */
+ @Nullable
+ public Pose getHeadPose() {
+ Pose pose = getCurrentHeadPose();
+ if (pose == null) {
+ Log.w(TAG, "Failed to get the head pose.");
+ return null;
+ }
+ return pose;
+ }
+
+ /**
+ * Returns the left and right views mapping to the left and right eyes using the current
+ * timestamp from OpenXR.
+ */
+ @Nullable
+ public ViewProjections getStereoViews() {
+ Pair<ViewProjection, ViewProjection> stereoViews = getCurrentStereoViews();
+ if (stereoViews == null) {
+ Log.w(TAG, "Failed to get stereo views.");
+ return null;
+ }
+ return new ViewProjections(stereoViews.first, stereoViews.second);
+ }
+
+ /** Get the underlying OpenXR XrSession handle. */
+ public native long getNativeSession();
+
+ /** Get the underlying OpenXR XrInstance handle. */
+ public native long getNativeInstance();
+
+ private native List<Long> getPlanes();
+
+ private native Pose getCurrentHeadPose();
+
+ private native Pair<ViewProjection, ViewProjection> getCurrentStereoViews();
+
+ private native boolean createOpenXrSession(Activity activity, int referenceSpaceType);
+
+ private native Anchor.AnchorData getAnchor(
+ float minWidth, float minHeight, int type, int label);
+
+ private native Anchor.AnchorData createPersistedAnchor(long highBits, long lowBits);
+
+ /**
+ * Unpersists an anchor for the specified UUID.
+ *
+ * @return whether the unpersist operation succeeds or not.
+ */
+ public boolean unpersistAnchor(@Nullable UUID uuid) {
+ if (uuid == null) {
+ Log.i(TAG, "UUID is null and cannot unpersist the anchor.");
+ return false;
+ }
+ return unpersistAnchor(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits());
+ }
+
+ private native boolean unpersistAnchor(long highBits, long lowBits);
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/Trackable.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/Trackable.java
new file mode 100644
index 0000000..ccc0b16
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/Trackable.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl.perception;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.util.List;
+
+/**
+ * A Trackable is something that can be tracked in the real world (e.g. a plane). An anchor can be
+ * attached to a trackable in order to keep track of a point relative to that trackable.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface Trackable {
+
+ /**
+ * Creates an anchor on the trackable for a point in the specified reference space.
+ *
+ * @param pose Specifies the pose relative to the center point of the trackable.
+ * @param timeNs The monotonic time retrieved using the SystemClock's uptimeMillis() or
+ * uptimeNanos() at which to get the pose, in nanoseconds. If time is null or negative, the
+ * current time will be used.
+ */
+ @Nullable
+ Anchor createAnchor(@NonNull Pose pose, @SuppressWarnings("AutoBoxing") @Nullable Long timeNs);
+
+ /** Returns all anchors attached to this trackable. */
+ @NonNull
+ List<Anchor> getAnchors();
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/ViewProjection.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/ViewProjection.java
new file mode 100644
index 0000000..6b61de7
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/ViewProjection.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl.perception;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Contains the view projection state. <a
+ * href="https://registry.khronos.org/OpenXR/specs/1.0/man/html/XrView.html">...</a>
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class ViewProjection {
+ private final Pose pose;
+ private final Fov fov;
+
+ public ViewProjection(@NonNull Pose pose, @NonNull Fov fov) {
+ this.pose = pose;
+ this.fov = fov;
+ }
+
+ /** Returns the location and orientation of the camera/eye pose. */
+ @NonNull
+ public Pose getPose() {
+ return pose;
+ }
+
+ /** Returns the four sides of the projection / view frustum. */
+ @NonNull
+ public Fov getFov() {
+ return fov;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object instanceof ViewProjection) {
+ ViewProjection that = (ViewProjection) object;
+ return this.pose.equals(that.pose) && this.fov.equals(that.fov);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return pose.hashCode() + fov.hashCode();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/ViewProjections.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/ViewProjections.java
new file mode 100644
index 0000000..803e0e0
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/ViewProjections.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl.perception;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/** Contains the view projections for both eyes */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public final class ViewProjections {
+ private final ViewProjection leftEye;
+ private final ViewProjection rightEye;
+
+ public ViewProjections(@NonNull ViewProjection leftEye, @NonNull ViewProjection rightEye) {
+ this.leftEye = leftEye;
+ this.rightEye = rightEye;
+ }
+
+ // Returns the left eye view projection.
+ @NonNull
+ public ViewProjection getLeftEye() {
+ return leftEye;
+ }
+
+ // Returns the right eye view projection.
+ @NonNull
+ public ViewProjection getRightEye() {
+ return rightEye;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object instanceof ViewProjections) {
+ ViewProjections that = (ViewProjections) object;
+ return this.leftEye.equals(that.leftEye) && this.rightEye.equals(that.rightEye);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return leftEye.hashCode() + rightEye.hashCode();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/exceptions/FailedToInitializeException.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/exceptions/FailedToInitializeException.java
new file mode 100644
index 0000000..07a2710
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/exceptions/FailedToInitializeException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl.perception.exceptions;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/** Thrown if there is a failure in initializing a PerceptionLibrary Session. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class FailedToInitializeException extends RuntimeException {
+
+ public FailedToInitializeException(@NonNull String message) {
+ super(message);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/exceptions/LibraryLoadingException.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/exceptions/LibraryLoadingException.java
new file mode 100644
index 0000000..4de142a
--- /dev/null
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/perception/exceptions/LibraryLoadingException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl.perception.exceptions;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/** Thrown if there is a failure in loading a native library. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class LibraryLoadingException extends RuntimeException {
+
+ public LibraryLoadingException(@NonNull String message) {
+ super(message);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/ActivityPoseTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/ActivityPoseTest.kt
new file mode 100644
index 0000000..bf44e6e
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/ActivityPoseTest.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import androidx.xr.runtime.math.Pose
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class ActivityPoseTest {
+ private val entityManager = EntityManager()
+ private val mockRuntime = mock<JxrPlatformAdapter>()
+ private val mockActivitySpace = mock<JxrPlatformAdapter.ActivitySpace>()
+ private val mockHeadActivityPose = mock<JxrPlatformAdapter.HeadActivityPose>()
+ private val mockCameraViewActivityPose = mock<JxrPlatformAdapter.CameraViewActivityPose>()
+ private val mockPerceptionSpaceActivityPose =
+ mock<JxrPlatformAdapter.PerceptionSpaceActivityPose>()
+
+ private lateinit var spatialUser: SpatialUser
+ private var head: Head? = null
+ private lateinit var activitySpace: ActivitySpace
+ private var camera: CameraView? = null
+ private lateinit var perceptionSpace: PerceptionSpace
+
+ @Before
+ fun setUp() {
+ whenever(mockRuntime.headActivityPose).thenReturn(mockHeadActivityPose)
+ whenever(mockRuntime.getCameraViewActivityPose(anyInt()))
+ .thenReturn(mockCameraViewActivityPose)
+ whenever(mockRuntime.activitySpace).thenReturn(mockActivitySpace)
+ whenever(mockRuntime.perceptionSpaceActivityPose)
+ .thenReturn(mockPerceptionSpaceActivityPose)
+ head = Head.create(mockRuntime)
+ camera = CameraView.createLeft(mockRuntime)
+ activitySpace = ActivitySpace.create(mockRuntime, entityManager)
+ perceptionSpace = PerceptionSpace.create(mockRuntime)
+ }
+
+ @Test
+ fun allActivityPoseTransformPoseTo_callsRuntimeActivityPoseImplTransformPoseTo() {
+ whenever(mockHeadActivityPose.transformPoseTo(any(), any())).thenReturn(Pose())
+ whenever(mockCameraViewActivityPose.transformPoseTo(any(), any())).thenReturn(Pose())
+ whenever(mockPerceptionSpaceActivityPose.transformPoseTo(any(), any())).thenReturn(Pose())
+ val pose = Pose.Identity
+
+ assertThat(head?.transformPoseTo(pose, head!!)).isEqualTo(pose)
+ assertThat(camera!!.transformPoseTo(pose, camera!!)).isEqualTo(pose)
+ assertThat(perceptionSpace.transformPoseTo(pose, perceptionSpace)).isEqualTo(pose)
+
+ verify(mockHeadActivityPose).transformPoseTo(any(), any())
+ verify(mockCameraViewActivityPose).transformPoseTo(any(), any())
+ }
+
+ @Test
+ fun allActivityPoseTransformPoseToEntity_callsRuntimeActivityPoseImplTransformPoseTo() {
+ whenever(mockHeadActivityPose.transformPoseTo(any(), any())).thenReturn(Pose())
+ whenever(mockCameraViewActivityPose.transformPoseTo(any(), any())).thenReturn(Pose())
+ whenever(mockPerceptionSpaceActivityPose.transformPoseTo(any(), any())).thenReturn(Pose())
+ val pose = Pose.Identity
+
+ assertThat(head!!.transformPoseTo(pose, activitySpace)).isEqualTo(pose)
+ assertThat(camera!!.transformPoseTo(pose, activitySpace)).isEqualTo(pose)
+ assertThat(perceptionSpace.transformPoseTo(pose, activitySpace)).isEqualTo(pose)
+
+ verify(mockHeadActivityPose).transformPoseTo(any(), any())
+ verify(mockCameraViewActivityPose).transformPoseTo(any(), any())
+ verify(mockPerceptionSpaceActivityPose).transformPoseTo(any(), any())
+ }
+
+ @Test
+ fun allActivityPoseTransformPoseFromEntity_callsRuntimeActivityPoseImplTransformPoseTo() {
+ whenever(mockActivitySpace.transformPoseTo(any(), any())).thenReturn(Pose())
+ val pose = Pose.Identity
+
+ assertThat(activitySpace.transformPoseTo(pose, head!!)).isEqualTo(pose)
+ assertThat(activitySpace.transformPoseTo(pose, camera!!)).isEqualTo(pose)
+ assertThat(activitySpace.transformPoseTo(pose, perceptionSpace)).isEqualTo(pose)
+
+ verify(mockActivitySpace, times(3)).transformPoseTo(any(), any())
+ }
+
+ @Test
+ fun allActivityPoseGetActivitySpacePose_callsRuntimeActivityPoseImplGetActivitySpacePose() {
+ whenever(mockHeadActivityPose.activitySpacePose).thenReturn(Pose())
+ whenever(mockCameraViewActivityPose.activitySpacePose).thenReturn(Pose())
+ whenever(mockPerceptionSpaceActivityPose.activitySpacePose).thenReturn(Pose())
+ val pose = Pose.Identity
+
+ assertThat(head!!.getActivitySpacePose()).isEqualTo(pose)
+ assertThat(camera!!.getActivitySpacePose()).isEqualTo(pose)
+ assertThat(perceptionSpace.getActivitySpacePose()).isEqualTo(pose)
+
+ verify(mockHeadActivityPose).activitySpacePose
+ verify(mockCameraViewActivityPose).activitySpacePose
+ verify(mockPerceptionSpaceActivityPose).activitySpacePose
+ }
+
+ @Test
+ fun cameraView_getFov_returnsFov() {
+ val rtFov = JxrPlatformAdapter.CameraViewActivityPose.Fov(1f, 2f, 3f, 4f)
+ whenever(mockCameraViewActivityPose.fov).thenReturn(rtFov)
+
+ assertThat(camera!!.fov).isEqualTo(Fov(1f, 2f, 3f, 4f))
+
+ verify(mockCameraViewActivityPose).fov
+ }
+
+ @Test
+ fun cameraView_getFovTwice_returnsUpdatedFov() {
+ val rtFov = JxrPlatformAdapter.CameraViewActivityPose.Fov(1f, 2f, 3f, 4f)
+ whenever(mockCameraViewActivityPose.fov)
+ .thenReturn(rtFov)
+ .thenReturn(JxrPlatformAdapter.CameraViewActivityPose.Fov(5f, 6f, 7f, 8f))
+
+ assertThat(camera!!.fov).isEqualTo(Fov(1f, 2f, 3f, 4f))
+ assertThat(camera!!.fov).isEqualTo(Fov(5f, 6f, 7f, 8f))
+
+ verify(mockCameraViewActivityPose, times(2)).fov
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/EntityManagerTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/EntityManagerTest.kt
new file mode 100644
index 0000000..6a7ac4c
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/EntityManagerTest.kt
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import android.app.Activity
+import android.widget.TextView
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.Futures
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.toJavaDuration
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class EntityManagerTest {
+ private val activity = Robolectric.buildActivity(Activity::class.java).create().start().get()
+ private val mockRuntime = mock<JxrPlatformAdapter>()
+ private val mockGltfModelEntityImpl = mock<JxrPlatformAdapter.GltfEntity>()
+ private val mockPanelEntityImpl = mock<JxrPlatformAdapter.PanelEntity>()
+ private val mockAnchorEntityImpl = mock<JxrPlatformAdapter.AnchorEntity>()
+ private val mockActivityPanelEntity = mock<JxrPlatformAdapter.ActivityPanelEntity>()
+ private val mockContentlessEntity = mock<JxrPlatformAdapter.Entity>()
+ private val entityManager = EntityManager()
+ private lateinit var session: Session
+ private lateinit var activitySpace: ActivitySpace
+ private lateinit var gltfModel: GltfModel
+ private lateinit var gltfModelEntity: GltfModelEntity
+ private lateinit var panelEntity: PanelEntity
+ private lateinit var anchorEntity: AnchorEntity
+ private lateinit var activityPanelEntity: ActivityPanelEntity
+ private lateinit var contentlessEntity: Entity
+
+ @Before
+ fun setUp() {
+ whenever(mockRuntime.spatialEnvironment).thenReturn(mock())
+ whenever(mockRuntime.activitySpace).thenReturn(mock())
+ whenever(mockRuntime.activitySpaceRootImpl).thenReturn(mock())
+ whenever(mockRuntime.headActivityPose).thenReturn(mock())
+ whenever(mockRuntime.perceptionSpaceActivityPose).thenReturn(mock())
+ whenever(mockRuntime.loadGltfByAssetNameSplitEngine(Mockito.anyString()))
+ .thenReturn(Futures.immediateFuture(mock()))
+ whenever(mockRuntime.createGltfEntity(any(), any(), any()))
+ .thenReturn(mockGltfModelEntityImpl)
+ whenever(mockRuntime.createPanelEntity(any(), any(), any(), any(), any(), any(), any()))
+ .thenReturn(mockPanelEntityImpl)
+ whenever(mockRuntime.createAnchorEntity(any(), any(), any(), any()))
+ .thenReturn(mockAnchorEntityImpl)
+ whenever(mockAnchorEntityImpl.state)
+ .thenReturn(JxrPlatformAdapter.AnchorEntity.State.UNANCHORED)
+ whenever(mockAnchorEntityImpl.persistState)
+ .thenReturn(JxrPlatformAdapter.AnchorEntity.PersistState.PERSIST_NOT_REQUESTED)
+ whenever(mockRuntime.createActivityPanelEntity(any(), any(), any(), any(), any()))
+ .thenReturn(mockActivityPanelEntity)
+ whenever(mockRuntime.createEntity(any(), any(), any())).thenReturn(mockContentlessEntity)
+ whenever(mockRuntime.mainPanelEntity).thenReturn(mockPanelEntityImpl)
+ session = Session.create(activity, mockRuntime)
+ activitySpace = ActivitySpace.create(mockRuntime, entityManager)
+ }
+
+ @Test
+ fun creatingEntity_addsEntityToEntityManager() {
+ createContentlessEntity()
+ createPanelEntity()
+ createAnchorEntity()
+ createActivityPanelEntity()
+ createGltfEntity()
+
+ // The entityManager contains activity space.
+ assertThat(entityManager.getAllEntities().size).isAtLeast(5)
+ assertThat(entityManager.getAllEntities())
+ .containsAtLeast(
+ contentlessEntity,
+ panelEntity,
+ anchorEntity,
+ activityPanelEntity,
+ gltfModelEntity,
+ )
+ }
+
+ @Test
+ fun getEntityForRtEntity_returnsEntity() {
+ createContentlessEntity()
+ createPanelEntity()
+ createAnchorEntity()
+ createActivityPanelEntity()
+ createGltfEntity()
+
+ assertThat(entityManager.getEntityForRtEntity(mockContentlessEntity))
+ .isEqualTo(contentlessEntity)
+ assertThat(entityManager.getEntityForRtEntity(mockPanelEntityImpl)).isEqualTo(panelEntity)
+ assertThat(entityManager.getEntityForRtEntity(mockAnchorEntityImpl)).isEqualTo(anchorEntity)
+ assertThat(entityManager.getEntityForRtEntity(mockGltfModelEntityImpl))
+ .isEqualTo(gltfModelEntity)
+ assertThat(entityManager.getEntityForRtEntity(mockActivityPanelEntity))
+ .isEqualTo(activityPanelEntity)
+ }
+
+ @Test
+ fun getEntityForRtEntity_returnsNullWhenNoRtEntityFound() {
+ assertThat(entityManager.getEntityForRtEntity(mockContentlessEntity)).isNull()
+ assertThat(entityManager.getEntityForRtEntity(mockPanelEntityImpl)).isNull()
+ assertThat(entityManager.getEntityForRtEntity(mockAnchorEntityImpl)).isNull()
+ assertThat(entityManager.getEntityForRtEntity(mockGltfModelEntityImpl)).isNull()
+ assertThat(entityManager.getEntityForRtEntity(mockActivityPanelEntity)).isNull()
+ }
+
+ @Test
+ fun getEntityByType_returnsEntityOfType() {
+ createContentlessEntity()
+ createPanelEntity()
+ createAnchorEntity()
+ createActivityPanelEntity()
+ createGltfEntity()
+
+ assertThat(entityManager.getEntities<Entity>())
+ .containsAtLeast(
+ contentlessEntity,
+ panelEntity,
+ anchorEntity,
+ activityPanelEntity,
+ gltfModelEntity,
+ )
+ assertThat(entityManager.getEntities<ContentlessEntity>())
+ .containsExactly(contentlessEntity)
+ assertThat(entityManager.getEntities<PanelEntity>()).contains(panelEntity)
+ assertThat(entityManager.getEntities<AnchorEntity>()).containsExactly(anchorEntity)
+ assertThat(entityManager.getEntities<ActivityPanelEntity>())
+ .containsExactly(activityPanelEntity)
+ assertThat(entityManager.getEntities<GltfModelEntity>()).containsExactly(gltfModelEntity)
+ }
+
+ @Test
+ fun disposeEntity_removesEntityFromEntityManager() {
+ createContentlessEntity()
+ createPanelEntity()
+ createAnchorEntity()
+ createActivityPanelEntity()
+ createGltfEntity()
+ assertThat(entityManager.getAllEntities().size).isAtLeast(5)
+ assertThat(entityManager.getAllEntities())
+ .containsAtLeast(
+ contentlessEntity,
+ panelEntity,
+ anchorEntity,
+ activityPanelEntity,
+ gltfModelEntity,
+ )
+
+ contentlessEntity.dispose()
+
+ assertThat(entityManager.getAllEntities().size).isAtLeast(4)
+ assertThat(entityManager.getAllEntities()).doesNotContain(contentlessEntity)
+ }
+
+ @Test
+ fun clearEntityManager_removesAllEntityFromEntityManager() {
+ createContentlessEntity()
+ createPanelEntity()
+ createAnchorEntity()
+ createActivityPanelEntity()
+ createGltfEntity()
+ assertThat(entityManager.getAllEntities().size).isAtLeast(5)
+ assertThat(entityManager.getAllEntities())
+ .containsAtLeast(
+ contentlessEntity,
+ panelEntity,
+ anchorEntity,
+ activityPanelEntity,
+ gltfModelEntity,
+ )
+
+ entityManager.clear()
+
+ assertThat(entityManager.getAllEntities()).isEmpty()
+ }
+
+ @Test
+ fun removeRtEntity_removesEntityFromEntityManager() {
+ createContentlessEntity()
+ createPanelEntity()
+ createAnchorEntity()
+ createActivityPanelEntity()
+ createGltfEntity()
+ assertThat(entityManager.getAllEntities().size).isAtLeast(5)
+ assertThat(entityManager.getAllEntities())
+ .containsAtLeast(
+ contentlessEntity,
+ panelEntity,
+ anchorEntity,
+ activityPanelEntity,
+ gltfModelEntity,
+ )
+
+ entityManager.removeEntity(mockContentlessEntity)
+
+ assertThat(entityManager.getAllEntities().size).isAtLeast(4)
+ assertThat(entityManager.getAllEntities()).doesNotContain(contentlessEntity)
+ }
+
+ private fun createPanelEntity() {
+ panelEntity =
+ PanelEntity.create(
+ mockRuntime,
+ entityManager,
+ TextView(activity),
+ Dimensions(720f, 480f),
+ Dimensions(0.1f, 0.1f, 0.1f),
+ "test",
+ activity,
+ )
+ }
+
+ private fun createGltfEntity() {
+ gltfModel = session.createGltfResourceAsync("test.glb").get()
+ gltfModelEntity = GltfModelEntity.create(mockRuntime, entityManager, gltfModel)
+ }
+
+ private fun createAnchorEntity() {
+ anchorEntity =
+ AnchorEntity.create(
+ mockRuntime,
+ entityManager,
+ Dimensions(),
+ PlaneType.ANY,
+ PlaneSemantic.ANY,
+ 10.seconds.toJavaDuration(),
+ )
+ }
+
+ private fun createActivityPanelEntity() {
+ activityPanelEntity =
+ ActivityPanelEntity.create(
+ mockRuntime,
+ entityManager,
+ PixelDimensions(640, 480),
+ "test",
+ activity,
+ )
+ }
+
+ private fun createContentlessEntity() {
+ contentlessEntity = ContentlessEntity.create(mockRuntime, entityManager, "test")
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/EntityTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/EntityTest.kt
new file mode 100644
index 0000000..24c74dd
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/EntityTest.kt
@@ -0,0 +1,1137 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import android.app.Activity
+import android.content.Intent
+import android.widget.TextView
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Vector3
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.Futures
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import java.util.UUID
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.toJavaDuration
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+
+// TODO: b/329902726 - Add a fake runtime and verify CPM integration.
+// TODO: b/369199417 - Update EntityTest once createGltfResourceAsync is default.
+@RunWith(RobolectricTestRunner::class)
+class EntityTest {
+ private val activity = Robolectric.buildActivity(Activity::class.java).create().start().get()
+ private val mockRuntime = mock<JxrPlatformAdapter>()
+ private val mockGltfModelEntityImpl = mock<JxrPlatformAdapter.GltfEntity>()
+ private val mockPanelEntityImpl = mock<JxrPlatformAdapter.PanelEntity>()
+ private val mockAnchorEntityImpl = mock<JxrPlatformAdapter.AnchorEntity>()
+ private val mockActivityPanelEntity = mock<JxrPlatformAdapter.ActivityPanelEntity>()
+ private val mockContentlessEntity = mock<JxrPlatformAdapter.Entity>()
+ private val entityManager = EntityManager()
+ private lateinit var session: Session
+ private lateinit var activitySpace: ActivitySpace
+ private lateinit var gltfModel: GltfModel
+ private lateinit var gltfModelEntity: GltfModelEntity
+ private lateinit var panelEntity: PanelEntity
+ private lateinit var anchorEntity: AnchorEntity
+ private lateinit var activityPanelEntity: ActivityPanelEntity
+ private lateinit var contentlessEntity: Entity
+
+ interface FakeComponent : Component
+
+ interface SubtypeFakeComponent : FakeComponent
+
+ // Introduce a test Runtime ActivitySpace to test the bounds changed callback.
+ class TestRtActivitySpace : JxrPlatformAdapter.ActivitySpace {
+ private var boundsChangedListener:
+ JxrPlatformAdapter.ActivitySpace.OnBoundsChangedListener? =
+ null
+ var getBoundsCalled: Boolean = false
+ var setScaleCalled: Boolean = false
+ var getScaleCalled: Boolean = false
+ var setAlphaCalled: Boolean = false
+ var getAlphaCalled: Boolean = false
+ var setHiddenCalled: Boolean = false
+ var getHiddenCalled: Boolean = false
+ var getActivitySpaceAlphaCalled: Boolean = false
+ var getWorldSpaceScaleCalled: Boolean = false
+ var getActivitySpaceScaleCalled: Boolean = false
+
+ override fun setPose(pose: Pose) {}
+
+ override fun getPose(): Pose {
+ return Pose()
+ }
+
+ override fun getActivitySpacePose(): Pose {
+ return Pose()
+ }
+
+ override fun transformPoseTo(
+ pose: Pose,
+ destination: JxrPlatformAdapter.ActivityPose
+ ): Pose {
+ return Pose()
+ }
+
+ override fun setSize(dimensions: JxrPlatformAdapter.Dimensions) {}
+
+ override fun getAlpha(): Float = 1.0f.apply { getAlphaCalled = true }
+
+ override fun setAlpha(alpha: Float) {
+ setAlphaCalled = true
+ }
+
+ override fun isHidden(includeParents: Boolean): Boolean =
+ false.apply { getHiddenCalled = true }
+
+ override fun setHidden(hidden: Boolean) {
+ setHiddenCalled = true
+ }
+
+ override fun getActivitySpaceAlpha(): Float =
+ 1.0f.apply { getActivitySpaceAlphaCalled = true }
+
+ override fun setScale(scale: Vector3) {
+ setScaleCalled = true
+ }
+
+ override fun getScale(): Vector3 {
+ getScaleCalled = true
+ return Vector3()
+ }
+
+ override fun getWorldSpaceScale(): Vector3 {
+ getWorldSpaceScaleCalled = true
+ return Vector3()
+ }
+
+ override fun getActivitySpaceScale(): Vector3 {
+ getActivitySpaceScaleCalled = true
+ return Vector3()
+ }
+
+ override fun addInputEventListener(
+ executor: Executor,
+ consumer: JxrPlatformAdapter.InputEventListener,
+ ) {}
+
+ override fun removeInputEventListener(consumer: JxrPlatformAdapter.InputEventListener) {}
+
+ override fun getParent(): JxrPlatformAdapter.Entity? {
+ return null
+ }
+
+ override fun setParent(parent: JxrPlatformAdapter.Entity?) {}
+
+ override fun addChild(child: JxrPlatformAdapter.Entity) {}
+
+ override fun addChildren(children: List<JxrPlatformAdapter.Entity>) {}
+
+ override fun getChildren(): List<JxrPlatformAdapter.Entity> {
+ return emptyList()
+ }
+
+ override fun setContentDescription(text: String) {}
+
+ override fun dispose() {}
+
+ override fun addComponent(component: JxrPlatformAdapter.Component): Boolean = false
+
+ override fun removeComponent(component: JxrPlatformAdapter.Component) {}
+
+ override fun removeAllComponents() {}
+
+ override fun getBounds(): JxrPlatformAdapter.Dimensions =
+ JxrPlatformAdapter.Dimensions(1f, 1f, 1f).apply { getBoundsCalled = true }
+
+ override fun addOnBoundsChangedListener(
+ listener: JxrPlatformAdapter.ActivitySpace.OnBoundsChangedListener
+ ) {
+ boundsChangedListener = listener
+ }
+
+ override fun removeOnBoundsChangedListener(
+ listener: JxrPlatformAdapter.ActivitySpace.OnBoundsChangedListener
+ ) {
+ boundsChangedListener = null
+ }
+
+ fun sendBoundsChanged(dimensions: JxrPlatformAdapter.Dimensions) {
+ boundsChangedListener?.onBoundsChanged(dimensions)
+ }
+
+ override fun setOnSpaceUpdatedListener(
+ listener: JxrPlatformAdapter.SystemSpaceEntity.OnSpaceUpdatedListener?,
+ executor: Executor?,
+ ) {}
+ }
+
+ private val testActivitySpace = TestRtActivitySpace()
+
+ @Before
+ fun setUp() {
+ whenever(mockRuntime.spatialEnvironment).thenReturn(mock())
+ whenever(mockRuntime.activitySpace).thenReturn(testActivitySpace)
+ whenever(mockRuntime.activitySpaceRootImpl).thenReturn(mock())
+ whenever(mockRuntime.headActivityPose).thenReturn(mock())
+ whenever(mockRuntime.perceptionSpaceActivityPose).thenReturn(mock())
+ whenever(mockRuntime.loadGltfByAssetNameSplitEngine(Mockito.anyString()))
+ .thenReturn(Futures.immediateFuture(mock()))
+ whenever(mockRuntime.createGltfEntity(any(), any(), any()))
+ .thenReturn(mockGltfModelEntityImpl)
+ whenever(mockRuntime.createPanelEntity(any(), any(), any(), any(), any(), any(), any()))
+ .thenReturn(mockPanelEntityImpl)
+ whenever(mockRuntime.createAnchorEntity(any(), any(), any(), any()))
+ .thenReturn(mockAnchorEntityImpl)
+ whenever(mockAnchorEntityImpl.state)
+ .thenReturn(JxrPlatformAdapter.AnchorEntity.State.UNANCHORED)
+ whenever(mockAnchorEntityImpl.persistState)
+ .thenReturn(JxrPlatformAdapter.AnchorEntity.PersistState.PERSIST_NOT_REQUESTED)
+ whenever(mockRuntime.createActivityPanelEntity(any(), any(), any(), any(), any()))
+ .thenReturn(mockActivityPanelEntity)
+ whenever(mockRuntime.createEntity(any(), any(), any())).thenReturn(mockContentlessEntity)
+ whenever(mockRuntime.getMainPanelEntity()).thenReturn(mockPanelEntityImpl)
+ session = Session.create(activity, mockRuntime)
+ activitySpace = ActivitySpace.create(mockRuntime, entityManager)
+ gltfModel = session.createGltfResourceAsync("test.glb").get()
+ gltfModelEntity = GltfModelEntity.create(mockRuntime, entityManager, gltfModel)
+ panelEntity =
+ PanelEntity.create(
+ mockRuntime,
+ entityManager = entityManager,
+ TextView(activity),
+ Dimensions(720f, 480f),
+ Dimensions(0.1f, 0.1f, 0.1f),
+ "test",
+ activity,
+ )
+ anchorEntity =
+ AnchorEntity.create(
+ mockRuntime,
+ entityManager,
+ Dimensions(),
+ PlaneType.ANY,
+ PlaneSemantic.ANY,
+ 10.seconds.toJavaDuration(),
+ )
+ activityPanelEntity =
+ ActivityPanelEntity.create(
+ mockRuntime,
+ entityManager = entityManager,
+ PixelDimensions(640, 480),
+ "test",
+ activity,
+ )
+ contentlessEntity = ContentlessEntity.create(mockRuntime, entityManager, "test")
+ }
+
+ @Test
+ fun anchorEntityCreateWithNullTimeout_passesNullToImpl() {
+ whenever(mockRuntime.createAnchorEntity(any(), any(), any(), any()))
+ .thenReturn(mockAnchorEntityImpl)
+ anchorEntity =
+ AnchorEntity.create(
+ mockRuntime,
+ entityManager,
+ Dimensions(),
+ PlaneType.ANY,
+ PlaneSemantic.ANY,
+ )
+
+ assertThat(anchorEntity).isNotNull()
+ }
+
+ @Test
+ fun allEntitySetParent_callsRuntimeEntityImplSetParent() {
+ panelEntity.setParent(activitySpace)
+ gltfModelEntity.setParent(activitySpace)
+ anchorEntity.setParent(activitySpace)
+ activityPanelEntity.setParent(activitySpace)
+
+ verify(mockPanelEntityImpl).parent = session.runtime.activitySpace
+ verify(mockGltfModelEntityImpl).parent = session.runtime.activitySpace
+ verify(mockAnchorEntityImpl).parent = session.runtime.activitySpace
+ verify(mockActivityPanelEntity).parent = session.runtime.activitySpace
+ }
+
+ @Test
+ fun allEntitySetParentNull_SetsNullParent() {
+ panelEntity.setParent(null)
+ gltfModelEntity.setParent(null)
+ anchorEntity.setParent(null)
+ activityPanelEntity.setParent(null)
+
+ verify(mockPanelEntityImpl).parent = null
+ verify(mockGltfModelEntityImpl).parent = null
+ verify(mockAnchorEntityImpl).parent = null
+ verify(mockActivityPanelEntity).parent = null
+ }
+
+ @Test
+ fun allEntityGetParent_callsRuntimeEntityImplGetParent() {
+ val rtActivitySpace = session.runtime.activitySpace
+ whenever(mockActivityPanelEntity.parent).thenReturn(rtActivitySpace)
+ whenever(mockPanelEntityImpl.parent).thenReturn(mockActivityPanelEntity)
+ whenever(mockGltfModelEntityImpl.parent).thenReturn(mockPanelEntityImpl)
+ whenever(mockContentlessEntity.parent).thenReturn(mockGltfModelEntityImpl)
+ whenever(mockAnchorEntityImpl.parent).thenReturn(mockContentlessEntity)
+
+ assertThat(activityPanelEntity.getParent()).isEqualTo(activitySpace)
+ assertThat(panelEntity.getParent()).isEqualTo(activityPanelEntity)
+ assertThat(gltfModelEntity.getParent()).isEqualTo(panelEntity)
+ assertThat(contentlessEntity.getParent()).isEqualTo(gltfModelEntity)
+ assertThat(anchorEntity.getParent()).isEqualTo(contentlessEntity)
+
+ verify(mockActivityPanelEntity).parent
+ verify(mockPanelEntityImpl).parent
+ verify(mockGltfModelEntityImpl).parent
+ verify(mockContentlessEntity).parent
+ verify(mockAnchorEntityImpl).parent
+ }
+
+ @Test
+ fun allEntityGetParent_nullParent_callsRuntimeEntityImplGetParent() {
+ whenever(mockActivityPanelEntity.parent).thenReturn(null)
+ whenever(mockPanelEntityImpl.parent).thenReturn(null)
+ whenever(mockGltfModelEntityImpl.parent).thenReturn(null)
+ whenever(mockContentlessEntity.parent).thenReturn(null)
+ whenever(mockAnchorEntityImpl.parent).thenReturn(null)
+
+ assertThat(activityPanelEntity.getParent()).isEqualTo(null)
+ assertThat(panelEntity.getParent()).isEqualTo(null)
+ assertThat(gltfModelEntity.getParent()).isEqualTo(null)
+ assertThat(contentlessEntity.getParent()).isEqualTo(null)
+ assertThat(anchorEntity.getParent()).isEqualTo(null)
+
+ verify(mockActivityPanelEntity).parent
+ verify(mockPanelEntityImpl).parent
+ verify(mockGltfModelEntityImpl).parent
+ verify(mockContentlessEntity).parent
+ verify(mockAnchorEntityImpl).parent
+ }
+
+ @Test
+ fun allEntityAddChild_callsRuntimeEntityImplAddChild() {
+ anchorEntity.addChild(panelEntity)
+ panelEntity.addChild(gltfModelEntity)
+ gltfModelEntity.addChild(activityPanelEntity)
+
+ verify(mockAnchorEntityImpl).addChild(mockPanelEntityImpl)
+ verify(mockPanelEntityImpl).addChild(mockGltfModelEntityImpl)
+ verify(mockGltfModelEntityImpl).addChild(mockActivityPanelEntity)
+ }
+
+ @Test
+ fun allEntitySetPose_callsRuntimeEntityImplSetPose() {
+ val pose = Pose.Identity
+
+ panelEntity.setPose(pose)
+ gltfModelEntity.setPose(pose)
+ anchorEntity.setPose(pose)
+ activityPanelEntity.setPose(pose)
+
+ verify(mockPanelEntityImpl).setPose(any())
+ verify(mockGltfModelEntityImpl).setPose(any())
+ verify(mockAnchorEntityImpl).setPose(any())
+ verify(mockActivityPanelEntity).setPose(any())
+ }
+
+ @Test
+ fun allEntityGetPose_callsRuntimeEntityImplGetPose() {
+ whenever(mockPanelEntityImpl.pose).thenReturn(Pose())
+ whenever(mockGltfModelEntityImpl.pose).thenReturn(Pose())
+ whenever(mockAnchorEntityImpl.pose).thenReturn(Pose())
+ whenever(mockActivityPanelEntity.pose).thenReturn(Pose())
+ val pose = Pose.Identity
+
+ assertThat(panelEntity.getPose()).isEqualTo(pose)
+ assertThat(gltfModelEntity.getPose()).isEqualTo(pose)
+ assertThat(anchorEntity.getPose()).isEqualTo(pose)
+ assertThat(activityPanelEntity.getPose()).isEqualTo(pose)
+
+ verify(mockPanelEntityImpl).getPose()
+ verify(mockGltfModelEntityImpl).getPose()
+ verify(mockAnchorEntityImpl).getPose()
+ verify(mockActivityPanelEntity).getPose()
+ }
+
+ @Test
+ fun allEntityGetActivitySpacePose_callsRuntimeEntityImplGetActivitySpacePose() {
+ whenever(mockPanelEntityImpl.activitySpacePose).thenReturn(Pose())
+ whenever(mockGltfModelEntityImpl.activitySpacePose).thenReturn(Pose())
+ whenever(mockAnchorEntityImpl.activitySpacePose).thenReturn(Pose())
+ whenever(mockActivityPanelEntity.activitySpacePose).thenReturn(Pose())
+ val pose = Pose.Identity
+
+ assertThat(panelEntity.getActivitySpacePose()).isEqualTo(pose)
+ assertThat(gltfModelEntity.getActivitySpacePose()).isEqualTo(pose)
+ assertThat(anchorEntity.getActivitySpacePose()).isEqualTo(pose)
+ assertThat(activityPanelEntity.getActivitySpacePose()).isEqualTo(pose)
+
+ verify(mockPanelEntityImpl).activitySpacePose
+ verify(mockGltfModelEntityImpl).activitySpacePose
+ verify(mockAnchorEntityImpl).activitySpacePose
+ verify(mockActivityPanelEntity).activitySpacePose
+ }
+
+ @Test
+ fun allEntitySetAlpha_callsRuntimeEntityImplSetAlpha() {
+ val alpha = 0.1f
+
+ panelEntity.setAlpha(alpha)
+ gltfModelEntity.setAlpha(alpha)
+ anchorEntity.setAlpha(alpha)
+ activityPanelEntity.setAlpha(alpha)
+ contentlessEntity.setAlpha(alpha)
+ activitySpace.setAlpha(alpha)
+
+ verify(mockPanelEntityImpl).setAlpha(any())
+ verify(mockGltfModelEntityImpl).setAlpha(any())
+ verify(mockAnchorEntityImpl).setAlpha(any())
+ verify(mockActivityPanelEntity).setAlpha(any())
+ verify(mockContentlessEntity).setAlpha(any())
+ assertThat(testActivitySpace.setAlphaCalled).isTrue()
+ }
+
+ @Test
+ fun allEntityGetAlpha_callsRuntimeEntityImplGetAlpha() {
+ whenever(mockPanelEntityImpl.getAlpha()).thenReturn(0.1f)
+ whenever(mockGltfModelEntityImpl.getAlpha()).thenReturn(0.1f)
+ whenever(mockAnchorEntityImpl.getAlpha()).thenReturn(0.1f)
+ whenever(mockActivityPanelEntity.getAlpha()).thenReturn(0.1f)
+
+ assertThat(panelEntity.getAlpha()).isEqualTo(0.1f)
+ assertThat(gltfModelEntity.getAlpha()).isEqualTo(0.1f)
+ assertThat(anchorEntity.getAlpha()).isEqualTo(0.1f)
+ assertThat(activityPanelEntity.getAlpha()).isEqualTo(0.1f)
+
+ assertThat(activitySpace.getAlpha()).isEqualTo(1.0f)
+ assertThat(testActivitySpace.getAlphaCalled).isTrue()
+
+ verify(mockPanelEntityImpl).getAlpha()
+ verify(mockGltfModelEntityImpl).getAlpha()
+ verify(mockAnchorEntityImpl).getAlpha()
+ verify(mockActivityPanelEntity).getAlpha()
+ }
+
+ @Test
+ fun allEntitySetHiddened_callsRuntimeEntityImplSetHidden() {
+ panelEntity.setHidden(true)
+ gltfModelEntity.setHidden(true)
+ anchorEntity.setHidden(true)
+ activityPanelEntity.setHidden(false)
+ contentlessEntity.setHidden(false)
+ activitySpace.setHidden(false)
+
+ verify(mockPanelEntityImpl).setHidden(true)
+ verify(mockGltfModelEntityImpl).setHidden(true)
+ verify(mockAnchorEntityImpl).setHidden(true)
+ verify(mockActivityPanelEntity).setHidden(false)
+ verify(mockContentlessEntity).setHidden(false)
+ assertThat(testActivitySpace.setHiddenCalled).isTrue()
+ }
+
+ @Test
+ fun allEntityGetActivitySpaceAlpha_callsRuntimeEntityImplGetActivitySpaceAlpha() {
+ whenever(mockPanelEntityImpl.activitySpaceAlpha).thenReturn(0.1f)
+ whenever(mockGltfModelEntityImpl.activitySpaceAlpha).thenReturn(0.1f)
+ whenever(mockAnchorEntityImpl.activitySpaceAlpha).thenReturn(0.1f)
+ whenever(mockActivityPanelEntity.activitySpaceAlpha).thenReturn(0.1f)
+
+ assertThat(panelEntity.getActivitySpaceAlpha()).isEqualTo(0.1f)
+ assertThat(gltfModelEntity.getActivitySpaceAlpha()).isEqualTo(0.1f)
+ assertThat(anchorEntity.getActivitySpaceAlpha()).isEqualTo(0.1f)
+ assertThat(activityPanelEntity.getActivitySpaceAlpha()).isEqualTo(0.1f)
+
+ assertThat(activitySpace.getActivitySpaceAlpha()).isEqualTo(1.0f)
+ assertThat(testActivitySpace.getActivitySpaceAlphaCalled).isTrue()
+
+ verify(mockPanelEntityImpl).activitySpaceAlpha
+ verify(mockGltfModelEntityImpl).activitySpaceAlpha
+ verify(mockAnchorEntityImpl).activitySpaceAlpha
+ verify(mockActivityPanelEntity).activitySpaceAlpha
+ }
+
+ @Test
+ fun allEntitySetScale_callsRuntimeEntityImplSetScale() {
+ val scale = 0.1f
+
+ panelEntity.setScale(scale)
+ gltfModelEntity.setScale(scale)
+ // Note that in production we expect this to raise an exception, but that should be handled
+ // by the runtime Entity.
+ anchorEntity.setScale(scale)
+ activityPanelEntity.setScale(scale)
+ contentlessEntity.setScale(scale)
+ // Note that in production we expect this to do nothing.
+ activitySpace.setScale(scale)
+
+ verify(mockPanelEntityImpl).setScale(any())
+ verify(mockGltfModelEntityImpl).setScale(any())
+ verify(mockAnchorEntityImpl).setScale(any())
+ verify(mockActivityPanelEntity).setScale(any())
+ verify(mockContentlessEntity).setScale(any())
+ assertThat(testActivitySpace.setScaleCalled).isTrue()
+ }
+
+ @Test
+ fun allEntityGetScale_callsRuntimeEntityImplGetScale() {
+ val runtimeScale = Vector3(0.1f, 0.1f, 0.1f)
+ val sdkScale = 0.1f
+
+ whenever(mockPanelEntityImpl.getScale()).thenReturn(runtimeScale)
+ whenever(mockGltfModelEntityImpl.getScale()).thenReturn(runtimeScale)
+ whenever(mockAnchorEntityImpl.getScale()).thenReturn(runtimeScale)
+ whenever(mockActivityPanelEntity.getScale()).thenReturn(runtimeScale)
+
+ assertThat(panelEntity.getScale()).isEqualTo(sdkScale)
+ assertThat(gltfModelEntity.getScale()).isEqualTo(sdkScale)
+ assertThat(anchorEntity.getScale()).isEqualTo(sdkScale)
+ assertThat(activityPanelEntity.getScale()).isEqualTo(sdkScale)
+
+ // This is unrealistic, but we want to make sure the SDK delegates to the runtimeImpl.
+ assertThat(activitySpace.getScale()).isEqualTo(0f)
+ assertThat(testActivitySpace.getScaleCalled).isTrue()
+
+ verify(mockPanelEntityImpl).getScale()
+ verify(mockGltfModelEntityImpl).getScale()
+ verify(mockAnchorEntityImpl).getScale()
+ verify(mockActivityPanelEntity).getScale()
+ }
+
+ @Test
+ fun allEntityGetWorldSpaceScale_callsRuntimeEntityImplGetWorldSpaceScale() {
+ val runtimeScale = Vector3(0.1f, 0.1f, 0.1f)
+ val sdkScale = 0.1f
+
+ whenever(mockPanelEntityImpl.getWorldSpaceScale()).thenReturn(runtimeScale)
+ whenever(mockGltfModelEntityImpl.getWorldSpaceScale()).thenReturn(runtimeScale)
+ whenever(mockAnchorEntityImpl.getWorldSpaceScale()).thenReturn(runtimeScale)
+ whenever(mockActivityPanelEntity.getWorldSpaceScale()).thenReturn(runtimeScale)
+
+ assertThat(panelEntity.getWorldSpaceScale()).isEqualTo(sdkScale)
+ assertThat(gltfModelEntity.getWorldSpaceScale()).isEqualTo(sdkScale)
+ assertThat(anchorEntity.getWorldSpaceScale()).isEqualTo(sdkScale)
+ assertThat(activityPanelEntity.getWorldSpaceScale()).isEqualTo(sdkScale)
+
+ // This is unrealistic, but we want to make sure the SDK delegates to the runtimeImpl.
+ assertThat(activitySpace.getWorldSpaceScale()).isEqualTo(0f)
+ assertThat(testActivitySpace.getWorldSpaceScaleCalled).isTrue()
+
+ verify(mockPanelEntityImpl).getWorldSpaceScale()
+ verify(mockGltfModelEntityImpl).getWorldSpaceScale()
+ verify(mockAnchorEntityImpl).getWorldSpaceScale()
+ verify(mockActivityPanelEntity).getWorldSpaceScale()
+ }
+
+ @Test
+ fun allEntityTransformPoseTo_callsRuntimeEntityImplTransformPoseTo() {
+ whenever(mockPanelEntityImpl.transformPoseTo(any(), any())).thenReturn(Pose())
+ whenever(mockGltfModelEntityImpl.transformPoseTo(any(), any())).thenReturn(Pose())
+ whenever(mockAnchorEntityImpl.transformPoseTo(any(), any())).thenReturn(Pose())
+ whenever(mockActivityPanelEntity.transformPoseTo(any(), any())).thenReturn(Pose())
+ whenever(mockContentlessEntity.transformPoseTo(any(), any())).thenReturn(Pose())
+ val pose = Pose.Identity
+
+ assertThat(panelEntity.transformPoseTo(pose, panelEntity)).isEqualTo(pose)
+ assertThat(gltfModelEntity.transformPoseTo(pose, panelEntity)).isEqualTo(pose)
+ assertThat(anchorEntity.transformPoseTo(pose, panelEntity)).isEqualTo(pose)
+ assertThat(activityPanelEntity.transformPoseTo(pose, panelEntity)).isEqualTo(pose)
+ assertThat(contentlessEntity.transformPoseTo(pose, panelEntity)).isEqualTo(pose)
+
+ verify(mockPanelEntityImpl).transformPoseTo(any(), any())
+ verify(mockGltfModelEntityImpl).transformPoseTo(any(), any())
+ verify(mockAnchorEntityImpl).transformPoseTo(any(), any())
+ verify(mockActivityPanelEntity).transformPoseTo(any(), any())
+ verify(mockContentlessEntity).transformPoseTo(any(), any())
+ }
+
+ @Test
+ fun allEntitySetSize_callsRuntimeEntityImplSetSize() {
+ val dimensions = Dimensions(0.3f, 0.2f, 0.1f)
+
+ panelEntity.setSize(dimensions)
+ gltfModelEntity.setSize(dimensions)
+ anchorEntity.setSize(dimensions)
+ activityPanelEntity.setSize(dimensions)
+
+ verify(mockPanelEntityImpl).setSize(any())
+ verify(mockGltfModelEntityImpl).setSize(any())
+ verify(mockAnchorEntityImpl).setSize(any())
+ verify(mockActivityPanelEntity).setSize(any())
+ }
+
+ @Test
+ fun allNonPanelEntityGetSize_returnsSize() {
+ val dimensions = Dimensions(0.3f, 0.2f, 0.1f)
+
+ gltfModelEntity.setSize(dimensions)
+ anchorEntity.setSize(dimensions)
+
+ assertThat(gltfModelEntity.getSize()).isEqualTo(dimensions)
+ assertThat(anchorEntity.getSize()).isEqualTo(dimensions)
+ }
+
+ @Test
+ fun allPanelEntityGetSize_callsRuntimeEntityImplGetSize() {
+ val dimensions = JxrPlatformAdapter.Dimensions(320.0f, 240.0f, 0.0f)
+ val expectedDimensions = dimensions.toDimensions()
+
+ whenever(mockPanelEntityImpl.getSize()).thenReturn(dimensions)
+ whenever(mockActivityPanelEntity.getSize()).thenReturn(dimensions)
+
+ assertThat(panelEntity.getSize()).isEqualTo(expectedDimensions)
+ assertThat(activityPanelEntity.getSize()).isEqualTo(expectedDimensions)
+
+ verify(mockPanelEntityImpl).getSize()
+ verify(mockActivityPanelEntity).getSize()
+ }
+
+ @Test
+ fun allPanelEntityGetPixelDensity_callsRuntimeEntityImplGetPixelDensity() {
+ val pixelDensity = Vector3(14.0f, 14.0f, 14.0f)
+ val expectedPixelDensity = pixelDensity
+ whenever(mockPanelEntityImpl.getPixelDensity()).thenReturn(pixelDensity)
+ whenever(mockActivityPanelEntity.getPixelDensity()).thenReturn(pixelDensity)
+
+ assertThat(panelEntity.getPixelDensity()).isEqualTo(expectedPixelDensity)
+ assertThat(activityPanelEntity.getPixelDensity()).isEqualTo(expectedPixelDensity)
+
+ verify(mockPanelEntityImpl).getPixelDensity()
+ verify(mockActivityPanelEntity).getPixelDensity()
+ }
+
+ @Test
+ fun allPanelEntitySetPixelDimensions_callsRuntimeEntityImplSetPixelDimensions() {
+ val dimensions = PixelDimensions(320, 240)
+ panelEntity.setPixelDimensions(dimensions)
+ activityPanelEntity.setPixelDimensions(dimensions)
+
+ verify(mockPanelEntityImpl).setPixelDimensions(any())
+ verify(mockActivityPanelEntity).setPixelDimensions(any())
+ }
+
+ @Test
+ fun allPanelEntityGetPixelDimensions_callsRuntimeEntityImplGetPixelDimensions() {
+ val pixelDimensions = JxrPlatformAdapter.PixelDimensions(320, 240)
+ val expectedPixelDimensions = pixelDimensions.toPixelDimensions()
+
+ whenever(mockPanelEntityImpl.getPixelDimensions()).thenReturn(pixelDimensions)
+ whenever(mockActivityPanelEntity.getPixelDimensions()).thenReturn(pixelDimensions)
+
+ assertThat(panelEntity.getPixelDimensions()).isEqualTo(expectedPixelDimensions)
+ assertThat(activityPanelEntity.getPixelDimensions()).isEqualTo(expectedPixelDimensions)
+
+ verify(mockPanelEntityImpl).getPixelDimensions()
+ verify(mockActivityPanelEntity).getPixelDimensions()
+ }
+
+ @Test
+ fun allEntityDispose_callsRuntimeEntityImplDispose() {
+ gltfModelEntity.dispose()
+ panelEntity.dispose()
+ anchorEntity.dispose()
+ activityPanelEntity.dispose()
+
+ verify(mockGltfModelEntityImpl).dispose()
+ verify(mockPanelEntityImpl).dispose()
+ verify(mockAnchorEntityImpl).dispose()
+ verify(mockActivityPanelEntity).dispose()
+ }
+
+ @Test
+ fun activityPanelEntityLaunchActivity_callsImplLaunchActivity() {
+ val launchIntent = Intent(activity.applicationContext, Activity::class.java)
+ activityPanelEntity.launchActivity(launchIntent, null)
+
+ verify(mockActivityPanelEntity).launchActivity(launchIntent, null)
+ }
+
+ @Test
+ fun activityPanelEntityMoveActivity_callsImplMoveActivity() {
+ activityPanelEntity.moveActivity(activity)
+
+ verify(mockActivityPanelEntity).moveActivity(any())
+ }
+
+ @Test
+ fun activitySpaceGetBounds_callsImplGetBounds() {
+ val activitySpace = ActivitySpace.create(mockRuntime, entityManager)
+ val bounds = activitySpace.getBounds()
+ assertThat(bounds).isNotNull()
+ assertThat(testActivitySpace.getBoundsCalled).isTrue()
+ }
+
+ @Test
+ fun activitySpaceSetBoundsListener_receivesBoundsChangedCallback() {
+ val activitySpace = ActivitySpace.create(mockRuntime, entityManager)
+ var called = false
+ val boundsChangedListener =
+ Consumer<Dimensions> { newBounds ->
+ assertThat(newBounds.width).isEqualTo(0.3f)
+ assertThat(newBounds.height).isEqualTo(0.2f)
+ assertThat(newBounds.depth).isEqualTo(0.1f)
+ called = true
+ }
+
+ activitySpace.addBoundsChangedListener(directExecutor(), boundsChangedListener)
+ testActivitySpace.sendBoundsChanged(JxrPlatformAdapter.Dimensions(0.3f, 0.2f, 0.1f))
+ assertThat(called).isTrue()
+
+ called = false
+ activitySpace.removeBoundsChangedListener(boundsChangedListener)
+ testActivitySpace.sendBoundsChanged(JxrPlatformAdapter.Dimensions(0.5f, 0.5f, 0.5f))
+ assertThat(called).isFalse()
+ }
+
+ @Test
+ fun setOnSpaceUpdatedListener_withNullParams_callsRuntimeSetOnSpaceUpdatedListener() {
+ val mockRtActivitySpace = mock<JxrPlatformAdapter.ActivitySpace>()
+ whenever(mockRuntime.activitySpace).thenReturn(mockRtActivitySpace)
+ val activitySpace = ActivitySpace.create(mockRuntime, entityManager)
+
+ activitySpace.setOnSpaceUpdatedListener(null)
+ verify(mockRtActivitySpace).setOnSpaceUpdatedListener(null, null)
+ }
+
+ @Test
+ fun setOnSpaceUpdatedListener_receivesRuntimeSetOnSpaceUpdatedListenerCallbacks() {
+ val mockRtActivitySpace = mock<JxrPlatformAdapter.ActivitySpace>()
+ whenever(mockRuntime.activitySpace).thenReturn(mockRtActivitySpace)
+ val activitySpace = ActivitySpace.create(mockRuntime, entityManager)
+
+ var listenerCalled = false
+ val captor = argumentCaptor<JxrPlatformAdapter.SystemSpaceEntity.OnSpaceUpdatedListener>()
+ activitySpace.setOnSpaceUpdatedListener({ listenerCalled = true }, directExecutor())
+ verify(mockRtActivitySpace).setOnSpaceUpdatedListener(captor.capture(), any())
+ captor.firstValue.onSpaceUpdated()
+ assertThat(listenerCalled).isTrue()
+ }
+
+ @Test
+ fun setOnSpaceUpdatedListener_anchorEntity_withNullParams_callsRuntimeSetOnSpaceUpdatedListener() {
+ anchorEntity.setOnSpaceUpdatedListener(null)
+ verify(mockAnchorEntityImpl).setOnSpaceUpdatedListener(null, null)
+ }
+
+ @Test
+ fun setOnSpaceUpdatedListener_anchorEntity_receivesRuntimeSetOnSpaceUpdatedListenerCallbacks() {
+ var listenerCalled = false
+ val captor = argumentCaptor<JxrPlatformAdapter.SystemSpaceEntity.OnSpaceUpdatedListener>()
+ anchorEntity.setOnSpaceUpdatedListener({ listenerCalled = true }, directExecutor())
+
+ verify(mockAnchorEntityImpl).setOnSpaceUpdatedListener(captor.capture(), any())
+ assertThat(listenerCalled).isFalse()
+ captor.firstValue.onSpaceUpdated()
+ assertThat(listenerCalled).isTrue()
+ }
+
+ @Test
+ fun mainPanelEntity_isMainPanelEntity() {
+ val mainPanelEntity = session.mainPanelEntity
+ assertThat(mainPanelEntity.isMainPanelEntity).isTrue()
+ }
+
+ @Test
+ fun mainPanelEntity_isSingleton() {
+ val mainPanelEntity = session.mainPanelEntity
+ val mainPanelEntity2 = session.mainPanelEntity
+
+ assertThat(mainPanelEntity2).isSameInstanceAs(mainPanelEntity)
+ verify(mockRuntime, times(1)).mainPanelEntity
+ }
+
+ @Test
+ fun contentlessEntity_isCreated() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+ }
+
+ @Test
+ fun contentlessEntity_canSetPose() {
+ val entity = session.createEntity("test")
+ val setPose = Pose.Identity
+ entity.setPose(setPose)
+
+ val captor = argumentCaptor<Pose>()
+ verify(mockContentlessEntity).setPose(captor.capture())
+
+ val pose = captor.firstValue
+ assertThat(pose.translation.x).isEqualTo(setPose.translation.x)
+ assertThat(pose.translation.y).isEqualTo(setPose.translation.y)
+ assertThat(pose.translation.z).isEqualTo(setPose.translation.z)
+ }
+
+ @Test
+ fun contentlessEntity_canGetPose() {
+ whenever(mockContentlessEntity.pose).thenReturn(Pose())
+
+ val entity = session.createEntity("test")
+ val pose = Pose.Identity
+
+ assertThat(entity.getPose()).isEqualTo(pose)
+ verify(mockContentlessEntity).getPose()
+ }
+
+ @Test
+ fun contentlessEntity_canGetActivitySpacePose() {
+ whenever(mockContentlessEntity.activitySpacePose).thenReturn(Pose())
+
+ val entity = session.createEntity("test")
+ val pose = Pose.Identity
+
+ assertThat(entity.getActivitySpacePose()).isEqualTo(pose)
+ verify(mockContentlessEntity).activitySpacePose
+ }
+
+ @Test
+ fun allEntity_addComponentInvokesOnAttach() {
+ val component = mock<Component>()
+ whenever(component.onAttach(any())).thenReturn(true)
+
+ assertThat(panelEntity.addComponent(component)).isTrue()
+ verify(component).onAttach(panelEntity)
+
+ assertThat(gltfModelEntity.addComponent(component)).isTrue()
+ verify(component).onAttach(gltfModelEntity)
+
+ assertThat(anchorEntity.addComponent(component)).isTrue()
+ verify(component).onAttach(anchorEntity)
+
+ assertThat(activityPanelEntity.addComponent(component)).isTrue()
+ verify(component).onAttach(activityPanelEntity)
+ }
+
+ @Test
+ fun allEntity_addComponentFailsIfOnAttachFails() {
+ val component = mock<Component>()
+ whenever(component.onAttach(any())).thenReturn(false)
+
+ assertThat(panelEntity.addComponent(component)).isFalse()
+ verify(component).onAttach(panelEntity)
+
+ assertThat(gltfModelEntity.addComponent(component)).isFalse()
+ verify(component).onAttach(gltfModelEntity)
+
+ assertThat(anchorEntity.addComponent(component)).isFalse()
+ verify(component).onAttach(anchorEntity)
+
+ assertThat(activityPanelEntity.addComponent(component)).isFalse()
+ verify(component).onAttach(activityPanelEntity)
+ }
+
+ @Test
+ fun allEntity_removeComponentInvokesOnDetach() {
+ val component = mock<Component>()
+ whenever(component.onAttach(any())).thenReturn(true)
+
+ assertThat(panelEntity.addComponent(component)).isTrue()
+ verify(component).onAttach(panelEntity)
+
+ assertThat(gltfModelEntity.addComponent(component)).isTrue()
+ verify(component).onAttach(gltfModelEntity)
+
+ assertThat(anchorEntity.addComponent(component)).isTrue()
+ verify(component).onAttach(anchorEntity)
+
+ assertThat(activityPanelEntity.addComponent(component)).isTrue()
+ verify(component).onAttach(activityPanelEntity)
+
+ panelEntity.removeComponent(component)
+ verify(component).onDetach(panelEntity)
+
+ gltfModelEntity.removeComponent(component)
+ verify(component).onDetach(gltfModelEntity)
+
+ anchorEntity.removeComponent(component)
+ verify(component).onDetach(anchorEntity)
+
+ activityPanelEntity.removeComponent(component)
+ verify(component).onDetach(activityPanelEntity)
+ }
+
+ @Test
+ fun allEntity_addingSameComponentTypeAgainSucceeds() {
+ val component1 = mock<Component>()
+ val component2 = mock<Component>()
+ whenever(component1.onAttach(any())).thenReturn(true)
+ whenever(component2.onAttach(any())).thenReturn(true)
+
+ assertThat(panelEntity.addComponent(component1)).isTrue()
+ assertThat(panelEntity.addComponent(component2)).isTrue()
+ verify(component1).onAttach(panelEntity)
+ verify(component2).onAttach(panelEntity)
+
+ assertThat(gltfModelEntity.addComponent(component1)).isTrue()
+ assertThat(gltfModelEntity.addComponent(component2)).isTrue()
+ verify(component1).onAttach(gltfModelEntity)
+ verify(component2).onAttach(panelEntity)
+
+ assertThat(anchorEntity.addComponent(component1)).isTrue()
+ assertThat(anchorEntity.addComponent(component2)).isTrue()
+ verify(component1).onAttach(anchorEntity)
+ verify(component2).onAttach(panelEntity)
+
+ assertThat(activityPanelEntity.addComponent(component1)).isTrue()
+ assertThat(activityPanelEntity.addComponent(component2)).isTrue()
+ verify(component1).onAttach(activityPanelEntity)
+ verify(component2).onAttach(panelEntity)
+ }
+
+ @Test
+ fun allEntity_addDifferentComponentTypesInvokesOnAttachOnAll() {
+ val component1 = mock<Component>()
+ val component2 = mock<FakeComponent>()
+ whenever(component1.onAttach(any())).thenReturn(true)
+ whenever(component2.onAttach(any())).thenReturn(true)
+
+ assertThat(panelEntity.addComponent(component1)).isTrue()
+ assertThat(panelEntity.addComponent(component2)).isTrue()
+ verify(component1).onAttach(panelEntity)
+ verify(component2).onAttach(panelEntity)
+
+ assertThat(gltfModelEntity.addComponent(component1)).isTrue()
+ assertThat(gltfModelEntity.addComponent(component2)).isTrue()
+ verify(component1).onAttach(gltfModelEntity)
+ verify(component2).onAttach(gltfModelEntity)
+
+ assertThat(anchorEntity.addComponent(component1)).isTrue()
+ assertThat(anchorEntity.addComponent(component2)).isTrue()
+ verify(component1).onAttach(anchorEntity)
+ verify(component2).onAttach(anchorEntity)
+
+ assertThat(activityPanelEntity.addComponent(component1)).isTrue()
+ assertThat(activityPanelEntity.addComponent(component2)).isTrue()
+ verify(component1).onAttach(activityPanelEntity)
+ verify(component2).onAttach(activityPanelEntity)
+ }
+
+ @Test
+ fun allEntity_removeAllComponentsInvokesOnDetachOnAll() {
+ val component1 = mock<Component>()
+ val component2 = mock<FakeComponent>()
+ whenever(component1.onAttach(any())).thenReturn(true)
+ whenever(component2.onAttach(any())).thenReturn(true)
+
+ assertThat(panelEntity.addComponent(component1)).isTrue()
+ assertThat(panelEntity.addComponent(component2)).isTrue()
+ verify(component1).onAttach(panelEntity)
+ verify(component2).onAttach(panelEntity)
+
+ assertThat(gltfModelEntity.addComponent(component1)).isTrue()
+ assertThat(gltfModelEntity.addComponent(component2)).isTrue()
+ verify(component1).onAttach(gltfModelEntity)
+ verify(component2).onAttach(gltfModelEntity)
+
+ assertThat(anchorEntity.addComponent(component1)).isTrue()
+ assertThat(anchorEntity.addComponent(component2)).isTrue()
+ verify(component1).onAttach(anchorEntity)
+ verify(component2).onAttach(anchorEntity)
+
+ assertThat(activityPanelEntity.addComponent(component1)).isTrue()
+ assertThat(activityPanelEntity.addComponent(component2)).isTrue()
+ verify(component1).onAttach(activityPanelEntity)
+ verify(component2).onAttach(activityPanelEntity)
+
+ panelEntity.removeAllComponents()
+ verify(component1).onDetach(panelEntity)
+ verify(component2).onDetach(panelEntity)
+
+ gltfModelEntity.removeAllComponents()
+ verify(component1).onDetach(gltfModelEntity)
+ verify(component2).onDetach(gltfModelEntity)
+
+ anchorEntity.removeAllComponents()
+ verify(component1).onDetach(anchorEntity)
+ verify(component2).onDetach(anchorEntity)
+
+ activityPanelEntity.removeAllComponents()
+ verify(component1).onDetach(activityPanelEntity)
+ verify(component2).onDetach(activityPanelEntity)
+ }
+
+ @Test
+ fun allEntity_addSameComponentMultipleTimesInvokesOnAttachMutipleTimes() {
+ val component = mock<Component>()
+ whenever(component.onAttach(any())).thenReturn(true)
+
+ assertThat(panelEntity.addComponent(component)).isTrue()
+ assertThat(panelEntity.addComponent(component)).isTrue()
+ verify(component, times(2)).onAttach(panelEntity)
+
+ assertThat(gltfModelEntity.addComponent(component)).isTrue()
+ assertThat(gltfModelEntity.addComponent(component)).isTrue()
+ verify(component, times(2)).onAttach(gltfModelEntity)
+
+ assertThat(anchorEntity.addComponent(component)).isTrue()
+ assertThat(anchorEntity.addComponent(component)).isTrue()
+ verify(component, times(2)).onAttach(anchorEntity)
+
+ assertThat(activityPanelEntity.addComponent(component)).isTrue()
+ assertThat(activityPanelEntity.addComponent(component)).isTrue()
+ verify(component, times(2)).onAttach(activityPanelEntity)
+ }
+
+ @Test
+ fun allEntity_removeSameComponentMultipleTimesInvokesOnDetachOnce() {
+ val component = mock<Component>()
+ whenever(component.onAttach(any())).thenReturn(true)
+
+ assertThat(panelEntity.addComponent(component)).isTrue()
+ verify(component).onAttach(panelEntity)
+
+ panelEntity.removeComponent(component)
+ panelEntity.removeComponent(component)
+ verify(component).onDetach(panelEntity)
+
+ assertThat(gltfModelEntity.addComponent(component)).isTrue()
+ verify(component).onAttach(gltfModelEntity)
+
+ gltfModelEntity.removeComponent(component)
+ gltfModelEntity.removeComponent(component)
+ verify(component).onDetach(gltfModelEntity)
+
+ assertThat(anchorEntity.addComponent(component)).isTrue()
+ verify(component).onAttach(anchorEntity)
+
+ anchorEntity.removeComponent(component)
+ anchorEntity.removeComponent(component)
+ verify(component).onDetach(anchorEntity)
+
+ assertThat(activityPanelEntity.addComponent(component)).isTrue()
+ verify(component).onAttach(activityPanelEntity)
+
+ activityPanelEntity.removeComponent(component)
+ activityPanelEntity.removeComponent(component)
+ verify(component).onDetach(activityPanelEntity)
+ }
+
+ @Test
+ fun anchorEntity_canGetDefaultPersistState() {
+ assertThat(anchorEntity.getPersistState())
+ .isEqualTo(AnchorEntity.PersistState.PERSIST_NOT_REQUESTED)
+ }
+
+ @Test
+ fun anchorEntity_createPersistAnchorSuccess() {
+ whenever(mockRuntime.createPersistedAnchorEntity(any(), any()))
+ .thenReturn(mockAnchorEntityImpl)
+ val persistAnchorEntity = AnchorEntity.create(mockRuntime, entityManager, UUID.randomUUID())
+ assertThat(persistAnchorEntity).isNotNull()
+ }
+
+ @Test
+ fun allEntity_disposeRemovesAllComponents() {
+ val component = mock<Component>()
+ whenever(component.onAttach(any())).thenReturn(true)
+
+ assertThat(panelEntity.addComponent(component)).isTrue()
+ verify(component).onAttach(panelEntity)
+
+ panelEntity.dispose()
+ verify(component).onDetach(panelEntity)
+
+ assertThat(gltfModelEntity.addComponent(component)).isTrue()
+ verify(component).onAttach(gltfModelEntity)
+
+ gltfModelEntity.dispose()
+ verify(component).onDetach(gltfModelEntity)
+
+ assertThat(anchorEntity.addComponent(component)).isTrue()
+ verify(component).onAttach(anchorEntity)
+
+ anchorEntity.dispose()
+ verify(component).onDetach(anchorEntity)
+
+ assertThat(activityPanelEntity.addComponent(component)).isTrue()
+ verify(component).onAttach(activityPanelEntity)
+
+ activityPanelEntity.dispose()
+ verify(component).onDetach(activityPanelEntity)
+ }
+
+ @Test
+ fun allEntity_getComponentsReturnsAttachedComponents() {
+ val component1 = mock<Component>()
+ val component2 = mock<FakeComponent>()
+ whenever(component1.onAttach(any())).thenReturn(true)
+ whenever(component2.onAttach(any())).thenReturn(true)
+
+ assertThat(panelEntity.addComponent(component1)).isTrue()
+ assertThat(panelEntity.addComponent(component2)).isTrue()
+ verify(component1).onAttach(panelEntity)
+ verify(component2).onAttach(panelEntity)
+ assertThat(panelEntity.getComponents()).containsExactly(component1, component2)
+
+ assertThat(gltfModelEntity.addComponent(component1)).isTrue()
+ assertThat(gltfModelEntity.addComponent(component2)).isTrue()
+ verify(component1).onAttach(gltfModelEntity)
+ verify(component2).onAttach(gltfModelEntity)
+ assertThat(gltfModelEntity.getComponents()).containsExactly(component1, component2)
+
+ assertThat(anchorEntity.addComponent(component1)).isTrue()
+ assertThat(anchorEntity.addComponent(component2)).isTrue()
+ verify(component1).onAttach(anchorEntity)
+ verify(component2).onAttach(anchorEntity)
+ assertThat(anchorEntity.getComponents()).containsExactly(component1, component2)
+
+ assertThat(activityPanelEntity.addComponent(component1)).isTrue()
+ assertThat(activityPanelEntity.addComponent(component2)).isTrue()
+ verify(component1).onAttach(activityPanelEntity)
+ verify(component2).onAttach(activityPanelEntity)
+ assertThat(activityPanelEntity.getComponents()).containsExactly(component1, component2)
+ }
+
+ @Test
+ fun getComponentsOfType_returnsAttachedComponents() {
+ val component1 = mock<Component>()
+ val component2 = mock<FakeComponent>()
+ val component3 = mock<SubtypeFakeComponent>()
+ whenever(component1.onAttach(any())).thenReturn(true)
+ whenever(component2.onAttach(any())).thenReturn(true)
+ whenever(component3.onAttach(any())).thenReturn(true)
+
+ assertThat(panelEntity.addComponent(component1)).isTrue()
+ assertThat(panelEntity.addComponent(component2)).isTrue()
+ assertThat(panelEntity.addComponent(component3)).isTrue()
+ verify(component1).onAttach(panelEntity)
+ verify(component2).onAttach(panelEntity)
+ verify(component3).onAttach(panelEntity)
+ assertThat(panelEntity.getComponentsOfType(FakeComponent::class.java))
+ .containsExactly(component2, component3)
+ }
+
+ @Test
+ fun setCornerRadius() {
+ val radius = 2.0f
+ panelEntity.setCornerRadius(radius)
+ verify(mockPanelEntityImpl).cornerRadius = radius
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/InteractableComponentTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/InteractableComponentTest.kt
new file mode 100644
index 0000000..03bcec5
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/InteractableComponentTest.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import android.app.Activity
+import androidx.xr.runtime.math.Matrix4
+import androidx.xr.runtime.math.Vector3
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class InteractableComponentTest {
+ private val activity = Robolectric.buildActivity(Activity::class.java).create().start().get()
+ private val mockRuntime = mock<JxrPlatformAdapter>()
+ private lateinit var session: Session
+ private val mockContentlessEntity = mock<JxrPlatformAdapter.Entity>()
+ private val entity by lazy { session.createEntity("test") }
+
+ @Before
+ fun setUp() {
+ whenever(mockRuntime.spatialEnvironment).thenReturn(mock())
+ whenever(mockRuntime.activitySpace).thenReturn(mock())
+ whenever(mockRuntime.activitySpaceRootImpl).thenReturn(mock())
+ whenever(mockRuntime.headActivityPose).thenReturn(mock())
+ whenever(mockRuntime.perceptionSpaceActivityPose).thenReturn(mock())
+ whenever(mockRuntime.getMainPanelEntity()).thenReturn(mock())
+ whenever(mockRuntime.createEntity(any(), any(), any())).thenReturn(mockContentlessEntity)
+ session = Session.create(activity, mockRuntime)
+ }
+
+ @Test
+ fun addInteractableComponent_addsRuntimeInteractableComponent() {
+ assertThat(entity).isNotNull()
+
+ whenever(mockRuntime.createInteractableComponent(any(), any())).thenReturn(mock())
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val mockListener = mock<InputEventListener>()
+ val executor = directExecutor()
+ val interactableComponent = session.createInteractableComponent(executor, mockListener)
+
+ assertThat(entity.addComponent(interactableComponent)).isTrue()
+ verify(mockRuntime).createInteractableComponent(any(), anyOrNull())
+ verify(mockContentlessEntity).addComponent(any())
+ }
+
+ @Test
+ fun removeInteractableComponent_removesRuntimeInteractableComponent() {
+ assertThat(entity).isNotNull()
+
+ whenever(mockRuntime.createInteractableComponent(any(), any())).thenReturn(mock())
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val mockListener = mock<InputEventListener>()
+ val executor = directExecutor()
+ val interactableComponent = session.createInteractableComponent(executor, mockListener)
+
+ assertThat(entity.addComponent(interactableComponent)).isTrue()
+
+ entity.removeComponent(interactableComponent)
+ verify(mockContentlessEntity).removeComponent(any())
+ }
+
+ @Test
+ fun interactableComponent_canAttachOnlyOnce() {
+ val entity2 = session.createEntity("test")
+ assertThat(entity).isNotNull()
+
+ whenever(mockRuntime.createInteractableComponent(any(), any())).thenReturn(mock())
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val mockListener = mock<InputEventListener>()
+ val executor = directExecutor()
+ val interactableComponent = session.createInteractableComponent(executor, mockListener)
+
+ assertThat(entity.addComponent(interactableComponent)).isTrue()
+ assertThat(entity2.addComponent(interactableComponent)).isFalse()
+ }
+
+ @Test
+ fun interactableComponent_canAttachAgainAfterDetach() {
+ assertThat(entity).isNotNull()
+
+ whenever(mockRuntime.createInteractableComponent(any(), any())).thenReturn(mock())
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val mockListener = mock<InputEventListener>()
+ val executor = directExecutor()
+ val interactableComponent = session.createInteractableComponent(executor, mockListener)
+
+ assertThat(entity.addComponent(interactableComponent)).isTrue()
+ entity.removeComponent(interactableComponent)
+ assertThat(entity.addComponent(interactableComponent)).isTrue()
+ }
+
+ @Test
+ fun interactableComponent_propagatesHitInfoInInputEvents() {
+ val mockRtInteractableComponent = mock<JxrPlatformAdapter.InteractableComponent>()
+ whenever(mockRuntime.createInteractableComponent(any(), any()))
+ .thenReturn(mockRtInteractableComponent)
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val mockListener = mock<InputEventListener>()
+ val interactableComponent =
+ session.createInteractableComponent(directExecutor(), mockListener)
+ assertThat(entity.addComponent(interactableComponent)).isTrue()
+ val listenerCaptor = argumentCaptor<JxrPlatformAdapter.InputEventListener>()
+ verify(mockRuntime).createInteractableComponent(any(), listenerCaptor.capture())
+ val rtInputEventListener = listenerCaptor.lastValue
+ val rtInputEvent =
+ JxrPlatformAdapter.InputEvent(
+ JxrPlatformAdapter.InputEvent.SOURCE_HANDS,
+ JxrPlatformAdapter.InputEvent.POINTER_TYPE_RIGHT,
+ 123456789L,
+ Vector3.Zero,
+ Vector3.One,
+ JxrPlatformAdapter.InputEvent.ACTION_DOWN,
+ JxrPlatformAdapter.InputEvent.HitInfo(
+ mockContentlessEntity,
+ Vector3.One,
+ Matrix4.Identity
+ ),
+ null,
+ )
+ rtInputEventListener.onInputEvent(rtInputEvent)
+ val inputEventCaptor = argumentCaptor<InputEvent>()
+ verify(mockListener).onInputEvent(inputEventCaptor.capture())
+ val inputEvent = inputEventCaptor.lastValue
+ assertThat(inputEvent.source).isEqualTo(InputEvent.SOURCE_HANDS)
+ assertThat(inputEvent.pointerType).isEqualTo(InputEvent.POINTER_TYPE_RIGHT)
+ assertThat(inputEvent.timestamp).isEqualTo(rtInputEvent.timestamp)
+ assertThat(inputEvent.action).isEqualTo(InputEvent.ACTION_DOWN)
+ assertThat(inputEvent.hitInfo).isNotNull()
+ assertThat(inputEvent.hitInfo!!.inputEntity).isEqualTo(entity)
+ assertThat(inputEvent.hitInfo!!.hitPosition).isEqualTo(Vector3.One)
+ assertThat(inputEvent.hitInfo!!.transform).isEqualTo(Matrix4.Identity)
+ assertThat(inputEvent.secondaryHitInfo).isNull()
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/MovableComponentTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/MovableComponentTest.kt
new file mode 100644
index 0000000..f5c6a13
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/MovableComponentTest.kt
@@ -0,0 +1,331 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import android.app.Activity
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Vector3
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.kotlin.any
+import org.mockito.kotlin.isA
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class MovableComponentTest {
+ private val activity = Robolectric.buildActivity(Activity::class.java).create().start().get()
+ private val mockRuntime = mock<JxrPlatformAdapter>()
+ private lateinit var session: Session
+ private val mockActivitySpace = mock<JxrPlatformAdapter.ActivitySpace>()
+ private val mockContentlessEntity = mock<JxrPlatformAdapter.Entity>()
+ private val mockAnchorEntity = mock<JxrPlatformAdapter.AnchorEntity>()
+ private val entityManager = EntityManager()
+
+ @Before
+ fun setUp() {
+ whenever(mockRuntime.spatialEnvironment).thenReturn(mock())
+ whenever(mockRuntime.activitySpace).thenReturn(mockActivitySpace)
+ whenever(mockRuntime.activitySpaceRootImpl).thenReturn(mock())
+ whenever(mockRuntime.headActivityPose).thenReturn(mock())
+ whenever(mockRuntime.perceptionSpaceActivityPose).thenReturn(mock())
+ whenever(mockRuntime.getMainPanelEntity()).thenReturn(mock())
+ whenever(mockRuntime.createEntity(any(), any(), any())).thenReturn(mockContentlessEntity)
+ whenever(mockRuntime.createAnchorEntity(any(), any(), any(), any()))
+ .thenReturn(mockAnchorEntity)
+ whenever(mockAnchorEntity.state)
+ .thenReturn(JxrPlatformAdapter.AnchorEntity.State.UNANCHORED)
+ whenever(mockAnchorEntity.persistState)
+ .thenReturn(JxrPlatformAdapter.AnchorEntity.PersistState.PERSIST_NOT_REQUESTED)
+ session = Session.create(activity, mockRuntime)
+ }
+
+ @Test
+ fun addMovableComponent_addsRuntimeMovableComponent() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+ val mockAnchorPlacement = mock<JxrPlatformAdapter.AnchorPlacement>()
+ whenever(mockRuntime.createMovableComponent(any(), any(), any(), any())).thenReturn(mock())
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ whenever(
+ mockRuntime.createAnchorPlacementForPlanes(
+ setOf(JxrPlatformAdapter.PlaneType.HORIZONTAL),
+ setOf(JxrPlatformAdapter.PlaneSemantic.WALL),
+ )
+ )
+ .thenReturn(mockAnchorPlacement)
+
+ val anchorPlacement =
+ AnchorPlacement.createForPlanes(setOf(PlaneType.HORIZONTAL), setOf(PlaneSemantic.WALL))
+
+ val movableComponent =
+ session.createMovableComponent(
+ systemMovable = false,
+ scaleInZ = false,
+ anchorPlacement = setOf(anchorPlacement),
+ shouldDisposeParentAnchor = false,
+ )
+
+ assertThat(entity.addComponent(movableComponent)).isTrue()
+ verify(mockRuntime).createMovableComponent(false, false, setOf(mockAnchorPlacement), false)
+ verify(mockContentlessEntity).addComponent(any())
+ }
+
+ @Test
+ fun addMovableComponentDefaultArguments_addsRuntimeMovableComponentWithDefaults() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+ whenever(mockRuntime.createMovableComponent(any(), any(), any(), any())).thenReturn(mock())
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val movableComponent = session.createMovableComponent()
+
+ assertThat(entity.addComponent(movableComponent)).isTrue()
+ verify(mockRuntime)
+ .createMovableComponent(
+ /*systemMovable=*/ true,
+ /*scaleInZ=*/ true,
+ /*anchorPlacement=*/ emptySet(),
+ /*shouldDisposeParentAnchor=*/ true,
+ )
+ verify(mockContentlessEntity).addComponent(any())
+ }
+
+ @Test
+ fun removeMovableComponent_removesRuntimeMovableComponent() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+ whenever(mockRuntime.createMovableComponent(any(), any(), any(), any())).thenReturn(mock())
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val movableComponent = session.createMovableComponent()
+ assertThat(entity.addComponent(movableComponent)).isTrue()
+
+ entity.removeComponent(movableComponent)
+ verify(mockContentlessEntity).removeComponent(any())
+ }
+
+ @Test
+ fun movableComponent_canAttachOnlyOnce() {
+ val entity = session.createEntity("test")
+ val entity2 = session.createEntity("test")
+ assertThat(entity).isNotNull()
+ whenever(mockRuntime.createMovableComponent(any(), any(), any(), any())).thenReturn(mock())
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val movableComponent = session.createMovableComponent()
+
+ assertThat(entity.addComponent(movableComponent)).isTrue()
+ assertThat(entity2.addComponent(movableComponent)).isFalse()
+ }
+
+ @Test
+ fun movableComponent_setSizeInvokesRuntimeMovableComponentSetSize() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+
+ val mockRtMovableComponent = mock<JxrPlatformAdapter.MovableComponent>()
+ whenever(mockRuntime.createMovableComponent(any(), any(), any(), any()))
+ .thenReturn(mockRtMovableComponent)
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val movableComponent = session.createMovableComponent()
+ assertThat(entity.addComponent(movableComponent)).isTrue()
+
+ val testSize = Dimensions(2f, 2f, 0f)
+ movableComponent.size = testSize
+
+ assertThat(movableComponent.size).isEqualTo(testSize)
+ verify(mockRtMovableComponent).setSize(any())
+ }
+
+ @Test
+ fun movableComponent_addMoveListenerInvokesRuntimeMovableComponentAddMoveEventListener() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+ val mockRtMovableComponent = mock<JxrPlatformAdapter.MovableComponent>()
+ whenever(mockRuntime.createMovableComponent(any(), any(), any(), any()))
+ .thenReturn(mockRtMovableComponent)
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val movableComponent = session.createMovableComponent()
+ assertThat(entity.addComponent(movableComponent)).isTrue()
+ val mockMoveListener = mock<MoveListener>()
+ movableComponent.addMoveListener(directExecutor(), mockMoveListener)
+
+ val captor = ArgumentCaptor.forClass(JxrPlatformAdapter.MoveEventListener::class.java)
+ verify(mockRtMovableComponent).addMoveEventListener(any(), captor.capture())
+ val rtMoveEventListener = captor.value
+ var rtMoveEvent =
+ JxrPlatformAdapter.MoveEvent(
+ MoveEvent.MOVE_STATE_START,
+ JxrPlatformAdapter.Ray(Vector3(0f, 0f, 0f), Vector3(1f, 1f, 1f)),
+ JxrPlatformAdapter.Ray(Vector3(1f, 1f, 1f), Vector3(2f, 2f, 2f)),
+ Pose(),
+ Pose(),
+ Vector3(1f, 1f, 1f),
+ Vector3(1f, 1f, 1f),
+ mockActivitySpace,
+ /* updatedParent= */ null,
+ /* disposedEntity= */ null,
+ )
+ rtMoveEventListener.onMoveEvent(rtMoveEvent)
+
+ verify(mockMoveListener).onMoveStart(any(), any(), any(), any(), any())
+
+ rtMoveEvent =
+ JxrPlatformAdapter.MoveEvent(
+ MoveEvent.MOVE_STATE_ONGOING,
+ JxrPlatformAdapter.Ray(Vector3(0f, 0f, 0f), Vector3(1f, 1f, 1f)),
+ JxrPlatformAdapter.Ray(Vector3(1f, 1f, 1f), Vector3(2f, 2f, 2f)),
+ Pose(),
+ Pose(),
+ Vector3(1f, 1f, 1f),
+ Vector3(1f, 1f, 1f),
+ mockActivitySpace,
+ /* updatedParent= */ null,
+ /* disposedEntity= */ null,
+ )
+ rtMoveEventListener.onMoveEvent(rtMoveEvent)
+
+ verify(mockMoveListener).onMoveUpdate(any(), any(), any(), any())
+
+ rtMoveEvent =
+ JxrPlatformAdapter.MoveEvent(
+ MoveEvent.MOVE_STATE_END,
+ JxrPlatformAdapter.Ray(Vector3(0f, 0f, 0f), Vector3(1f, 1f, 1f)),
+ JxrPlatformAdapter.Ray(Vector3(1f, 1f, 1f), Vector3(2f, 2f, 2f)),
+ Pose(),
+ Pose(),
+ Vector3(1f, 1f, 1f),
+ Vector3(1f, 1f, 1f),
+ mockActivitySpace,
+ mockAnchorEntity,
+ /* disposedEntity= */ null,
+ )
+ rtMoveEventListener.onMoveEvent(rtMoveEvent)
+
+ verify(mockMoveListener).onMoveEnd(any(), any(), any(), any(), isA<AnchorEntity>())
+ }
+
+ @Test
+ fun movableComponent_addMultipleMoveEventListenersInvokesAllListeners() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+ val mockRtMovableComponent = mock<JxrPlatformAdapter.MovableComponent>()
+ whenever(mockRuntime.createMovableComponent(any(), any(), any(), any()))
+ .thenReturn(mockRtMovableComponent)
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val movableComponent = session.createMovableComponent()
+ assertThat(entity.addComponent(movableComponent)).isTrue()
+ val mockMoveListener = mock<MoveListener>()
+ movableComponent.addMoveListener(directExecutor(), mockMoveListener)
+ val mockMoveListener2 = mock<MoveListener>()
+ movableComponent.addMoveListener(directExecutor(), mockMoveListener2)
+
+ val captor = ArgumentCaptor.forClass(JxrPlatformAdapter.MoveEventListener::class.java)
+ verify(mockRtMovableComponent, times(2)).addMoveEventListener(any(), captor.capture())
+ val rtMoveEventListener1 = captor.allValues[0]
+ val rtMoveEventListener2 = captor.allValues[1]
+ val rtMoveEvent =
+ JxrPlatformAdapter.MoveEvent(
+ MoveEvent.MOVE_STATE_START,
+ JxrPlatformAdapter.Ray(Vector3(0f, 0f, 0f), Vector3(1f, 1f, 1f)),
+ JxrPlatformAdapter.Ray(Vector3(1f, 1f, 1f), Vector3(2f, 2f, 2f)),
+ Pose(),
+ Pose(),
+ Vector3(1f, 1f, 1f),
+ Vector3(1f, 1f, 1f),
+ mockActivitySpace,
+ /* updatedParent= */ null,
+ /* disposedEntity= */ null,
+ )
+
+ rtMoveEventListener1.onMoveEvent(rtMoveEvent)
+ rtMoveEventListener2.onMoveEvent(rtMoveEvent)
+
+ verify(mockMoveListener).onMoveStart(any(), any(), any(), any(), any())
+ verify(mockMoveListener2).onMoveStart(any(), any(), any(), any(), any())
+ }
+
+ @Test
+ fun movableComponent_removeMoveEventListenerInvokesRuntimeRemoveMoveEventListener() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+ val mockRtMovableComponent = mock<JxrPlatformAdapter.MovableComponent>()
+ whenever(mockRuntime.createMovableComponent(any(), any(), any(), any()))
+ .thenReturn(mockRtMovableComponent)
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val movableComponent = session.createMovableComponent()
+ assertThat(entity.addComponent(movableComponent)).isTrue()
+ val mockMoveListener = mock<MoveListener>()
+ movableComponent.addMoveListener(directExecutor(), mockMoveListener)
+ val mockMoveListener2 = mock<MoveListener>()
+ movableComponent.addMoveListener(directExecutor(), mockMoveListener2)
+
+ val captor = ArgumentCaptor.forClass(JxrPlatformAdapter.MoveEventListener::class.java)
+ verify(mockRtMovableComponent, times(2)).addMoveEventListener(any(), captor.capture())
+ val rtMoveEventListener1 = captor.allValues[0]
+ val rtMoveEventListener2 = captor.allValues[1]
+ val rtMoveEvent =
+ JxrPlatformAdapter.MoveEvent(
+ MoveEvent.MOVE_STATE_START,
+ JxrPlatformAdapter.Ray(Vector3(0f, 0f, 0f), Vector3(1f, 1f, 1f)),
+ JxrPlatformAdapter.Ray(Vector3(1f, 1f, 1f), Vector3(2f, 2f, 2f)),
+ Pose(),
+ Pose(),
+ Vector3(1f, 1f, 1f),
+ Vector3(1f, 1f, 1f),
+ mockActivitySpace,
+ /* updatedParent= */ null,
+ /* disposedEntity= */ null,
+ )
+
+ rtMoveEventListener1.onMoveEvent(rtMoveEvent)
+ rtMoveEventListener2.onMoveEvent(rtMoveEvent)
+
+ verify(mockMoveListener).onMoveStart(any(), any(), any(), any(), any())
+ verify(mockMoveListener2).onMoveStart(any(), any(), any(), any(), any())
+
+ movableComponent.removeMoveListener(mockMoveListener)
+ verify(mockRtMovableComponent).removeMoveEventListener(rtMoveEventListener1)
+
+ rtMoveEventListener2.onMoveEvent(rtMoveEvent)
+ // The first listener, which we removed, should not be called again.
+ verify(mockMoveListener, times(1)).onMoveStart(any(), any(), any(), any(), any())
+ verify(mockMoveListener2, times(2)).onMoveStart(any(), any(), any(), any(), any())
+
+ movableComponent.removeMoveListener(mockMoveListener2)
+ verify(mockRtMovableComponent).removeMoveEventListener(rtMoveEventListener2)
+ }
+
+ @Test
+ fun movablecomponent_canAttachAgainAfterDetach() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+ whenever(mockRuntime.createMovableComponent(any(), any(), any(), any())).thenReturn(mock())
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val movableComponent = session.createMovableComponent()
+
+ assertThat(entity.addComponent(movableComponent)).isTrue()
+ entity.removeComponent(movableComponent)
+ assertThat(entity.addComponent(movableComponent)).isTrue()
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/PointerCaptureComponentTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/PointerCaptureComponentTest.kt
new file mode 100644
index 0000000..6ff171e
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/PointerCaptureComponentTest.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import android.app.Activity
+import androidx.xr.runtime.math.Matrix4
+import androidx.xr.runtime.math.Vector3
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class PointerCaptureComponentTest {
+ private val activity = Robolectric.buildActivity(Activity::class.java).create().start().get()
+ private val mockRuntime = mock<JxrPlatformAdapter>()
+ private lateinit var session: Session
+ private val mockRtEntity = mock<JxrPlatformAdapter.Entity>()
+ private val mockRtComponent = mock<JxrPlatformAdapter.PointerCaptureComponent>()
+
+ private val stateListener =
+ object : PointerCaptureComponent.StateListener {
+ var lastState: Int = -1
+
+ override fun onStateChanged(newState: Int) {
+ lastState = newState
+ }
+ }
+
+ private val inputListener =
+ object : InputEventListener {
+ lateinit var lastEvent: InputEvent
+
+ override fun onInputEvent(inputEvent: InputEvent) {
+ lastEvent = inputEvent
+ }
+ }
+
+ @Before
+ fun setUp() {
+ whenever(mockRuntime.spatialEnvironment).thenReturn(mock())
+ whenever(mockRuntime.activitySpace).thenReturn(mock())
+ whenever(mockRuntime.activitySpaceRootImpl).thenReturn(mock())
+ whenever(mockRuntime.headActivityPose).thenReturn(mock())
+ whenever(mockRuntime.perceptionSpaceActivityPose).thenReturn(mock())
+ whenever(mockRuntime.mainPanelEntity).thenReturn(mock())
+ whenever(mockRuntime.createEntity(any(), any(), any())).thenReturn(mockRtEntity)
+ whenever(mockRtEntity.addComponent(any())).thenReturn(true)
+ whenever(mockRuntime.createPointerCaptureComponent(any(), any(), any()))
+ .thenReturn(mockRtComponent)
+
+ session = Session.create(activity, mockRuntime)
+ }
+
+ @Test
+ fun addComponent_addsRuntimeComponent() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+
+ val pointerCaptureComponent =
+ PointerCaptureComponent.create(session, directExecutor(), stateListener, inputListener)
+ assertThat(entity.addComponent(pointerCaptureComponent)).isTrue()
+
+ verify(mockRtEntity).addComponent(any())
+ verify(mockRuntime).createPointerCaptureComponent(any(), any(), any())
+ }
+
+ @Test
+ fun addComponent_failsIfAlreadyAttached() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+
+ val pointerCaptureComponent =
+ PointerCaptureComponent.create(session, directExecutor(), stateListener, inputListener)
+ assertThat(entity.addComponent(pointerCaptureComponent)).isTrue()
+ assertThat(entity.addComponent(pointerCaptureComponent)).isFalse()
+ }
+
+ @Test
+ fun stateListener_propagatesCorrectlyFromRuntime() {
+ val entity = session.createEntity("test")
+ val pointerCaptureComponent =
+ PointerCaptureComponent.create(session, directExecutor(), stateListener, inputListener)
+ val stateListenerCaptor =
+ argumentCaptor<JxrPlatformAdapter.PointerCaptureComponent.StateListener>()
+
+ assertThat(entity.addComponent(pointerCaptureComponent)).isTrue()
+ verify(mockRuntime)
+ .createPointerCaptureComponent(any(), stateListenerCaptor.capture(), any())
+
+ // Verify all states are properly converted and propagated.
+ val stateListenerCaptured: JxrPlatformAdapter.PointerCaptureComponent.StateListener =
+ stateListenerCaptor.lastValue
+ stateListenerCaptured.onStateChanged(
+ JxrPlatformAdapter.PointerCaptureComponent.POINTER_CAPTURE_STATE_ACTIVE
+ )
+ assertThat(stateListener.lastState)
+ .isEqualTo(PointerCaptureComponent.POINTER_CAPTURE_STATE_ACTIVE)
+
+ stateListenerCaptured.onStateChanged(
+ JxrPlatformAdapter.PointerCaptureComponent.POINTER_CAPTURE_STATE_PAUSED
+ )
+ assertThat(stateListener.lastState)
+ .isEqualTo(PointerCaptureComponent.POINTER_CAPTURE_STATE_PAUSED)
+
+ stateListenerCaptured.onStateChanged(
+ JxrPlatformAdapter.PointerCaptureComponent.POINTER_CAPTURE_STATE_STOPPED
+ )
+ assertThat(stateListener.lastState)
+ .isEqualTo(PointerCaptureComponent.POINTER_CAPTURE_STATE_STOPPED)
+ }
+
+ @Test
+ fun inputEventListener_propagatesFromRuntime() {
+ val entity = session.createEntity("test")
+ val pointerCaptureComponent =
+ PointerCaptureComponent.create(session, directExecutor(), stateListener, inputListener)
+ val inputListenerCaptor = argumentCaptor<JxrPlatformAdapter.InputEventListener>()
+
+ assertThat(entity.addComponent(pointerCaptureComponent)).isTrue()
+ verify(mockRuntime)
+ .createPointerCaptureComponent(any(), any(), inputListenerCaptor.capture())
+
+ val inputEvent =
+ JxrPlatformAdapter.InputEvent(
+ JxrPlatformAdapter.InputEvent.SOURCE_HANDS,
+ JxrPlatformAdapter.InputEvent.POINTER_TYPE_LEFT,
+ 100,
+ Vector3(),
+ Vector3(0f, 0f, 1f),
+ JxrPlatformAdapter.InputEvent.ACTION_DOWN,
+ JxrPlatformAdapter.InputEvent.HitInfo(mockRtEntity, Vector3.One, Matrix4.Identity),
+ null,
+ )
+
+ // Only compare non-floating point values for stability
+ inputListenerCaptor.lastValue.onInputEvent(inputEvent)
+ assertThat(inputListener.lastEvent.source).isEqualTo(InputEvent.SOURCE_HANDS)
+ assertThat(inputListener.lastEvent.pointerType).isEqualTo(InputEvent.POINTER_TYPE_LEFT)
+ assertThat(inputListener.lastEvent.timestamp).isEqualTo(inputEvent.timestamp)
+ assertThat(inputListener.lastEvent.action).isEqualTo(InputEvent.ACTION_DOWN)
+ assertThat(inputListener.lastEvent.hitInfo).isNotNull()
+ val hitInfo = inputListener.lastEvent.hitInfo!!
+ assertThat(hitInfo.inputEntity).isEqualTo(entity)
+ assertThat(hitInfo.hitPosition).isEqualTo(Vector3.One)
+ assertThat(hitInfo.transform).isEqualTo(Matrix4.Identity)
+ assertThat(inputListener.lastEvent.secondaryHitInfo).isNull()
+ }
+
+ @Test
+ fun removeComponent_removesRuntimeComponent() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+
+ val pointerCaptureComponent =
+ PointerCaptureComponent.create(session, directExecutor(), stateListener, inputListener)
+ assertThat(entity.addComponent(pointerCaptureComponent)).isTrue()
+
+ entity.removeComponent(pointerCaptureComponent)
+ verify(mockRtEntity).removeComponent(mockRtComponent)
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/ResizableComponentTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/ResizableComponentTest.kt
new file mode 100644
index 0000000..4ce3987
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/ResizableComponentTest.kt
@@ -0,0 +1,357 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import android.app.Activity
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.kotlin.any
+import org.mockito.kotlin.firstValue
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.secondValue
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class ResizableComponentTest {
+ private val activity = Robolectric.buildActivity(Activity::class.java).create().start().get()
+ private val mockRuntime = mock<JxrPlatformAdapter>()
+ private lateinit var session: Session
+ private val mockContentlessEntity = mock<JxrPlatformAdapter.Entity>()
+
+ @Before
+ fun setUp() {
+ whenever(mockRuntime.spatialEnvironment).thenReturn(mock())
+ whenever(mockRuntime.activitySpace).thenReturn(mock())
+ whenever(mockRuntime.activitySpaceRootImpl).thenReturn(mock())
+ whenever(mockRuntime.headActivityPose).thenReturn(mock())
+ whenever(mockRuntime.perceptionSpaceActivityPose).thenReturn(mock())
+ whenever(mockRuntime.getMainPanelEntity()).thenReturn(mock())
+ whenever(mockRuntime.createEntity(any(), any(), any())).thenReturn(mockContentlessEntity)
+ session = Session.create(activity, mockRuntime)
+ }
+
+ @Test
+ fun addResizableComponent_addsRuntimeResizableComponent() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+ whenever(mockRuntime.createResizableComponent(any(), any())).thenReturn(mock())
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val resizableComponent = ResizableComponent.create(mockRuntime, Dimensions(), Dimensions())
+
+ assertThat(entity.addComponent(resizableComponent)).isTrue()
+ verify(mockRuntime).createResizableComponent(any(), any())
+ verify(mockContentlessEntity).addComponent(any())
+ }
+
+ @Test
+ fun addResizableComponentDefaultArguments_addsRuntimeResizableComponentWithDefaults() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+ whenever(mockRuntime.createResizableComponent(any(), any())).thenReturn(mock())
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val resizableComponent = ResizableComponent.create(mockRuntime)
+
+ assertThat(entity.addComponent(resizableComponent)).isTrue()
+ val captor = ArgumentCaptor.forClass(JxrPlatformAdapter.Dimensions::class.java)
+ verify(mockRuntime).createResizableComponent(captor.capture(), captor.capture())
+ val rtMinDimensions = captor.firstValue
+ val rtMaxDimensions = captor.secondValue
+ assertThat(rtMinDimensions.width).isEqualTo(0f)
+ assertThat(rtMinDimensions.height).isEqualTo(0f)
+ assertThat(rtMinDimensions.depth).isEqualTo(0f)
+ assertThat(rtMaxDimensions.width).isEqualTo(10f)
+ assertThat(rtMaxDimensions.height).isEqualTo(10f)
+ assertThat(rtMaxDimensions.depth).isEqualTo(10f)
+ verify(mockContentlessEntity).addComponent(any())
+ }
+
+ @Test
+ fun removeResizableComponent_removesRuntimeResizableComponent() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+ whenever(mockRuntime.createResizableComponent(any(), any())).thenReturn(mock())
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val resizableComponent = ResizableComponent.create(mockRuntime)
+ assertThat(entity.addComponent(resizableComponent)).isTrue()
+
+ entity.removeComponent(resizableComponent)
+ verify(mockContentlessEntity).removeComponent(any())
+ }
+
+ @Test
+ fun resizableComponent_canAttachOnlyOnce() {
+ val entity = session.createEntity("test")
+ val entity2 = session.createEntity("test")
+ assertThat(entity).isNotNull()
+ whenever(mockRuntime.createResizableComponent(any(), any())).thenReturn(mock())
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val resizableComponent = ResizableComponent.create(mockRuntime)
+
+ assertThat(entity.addComponent(resizableComponent)).isTrue()
+ assertThat(entity2.addComponent(resizableComponent)).isFalse()
+ }
+
+ @Test
+ fun resizableComponent_setSizeInvokesRuntimeResizableComponentSetSize() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+
+ val mockRtResizableComponent = mock<JxrPlatformAdapter.ResizableComponent>()
+ whenever(mockRuntime.createResizableComponent(any(), any()))
+ .thenReturn(mockRtResizableComponent)
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val resizableComponent = ResizableComponent.create(mockRuntime)
+ assertThat(entity.addComponent(resizableComponent)).isTrue()
+
+ val testSize = Dimensions(2f, 2f, 0f)
+ resizableComponent.size = testSize
+
+ assertThat(resizableComponent.size).isEqualTo(testSize)
+ val captor = ArgumentCaptor.forClass(JxrPlatformAdapter.Dimensions::class.java)
+ verify(mockRtResizableComponent).setSize(captor.capture())
+ val rtDimensions = captor.value
+ assertThat(rtDimensions.width).isEqualTo(2f)
+ assertThat(rtDimensions.height).isEqualTo(2f)
+ assertThat(rtDimensions.depth).isEqualTo(0f)
+ }
+
+ @Test
+ fun resizableComponent_setMinimumSizeInvokesRuntimeResizableComponentSetMinimumSize() {
+ val entity = session.createEntity("test")
+ val captor = ArgumentCaptor.forClass(JxrPlatformAdapter.Dimensions::class.java)
+ assertThat(entity).isNotNull()
+
+ val mockRtResizableComponent = mock<JxrPlatformAdapter.ResizableComponent>()
+ whenever(mockRuntime.createResizableComponent(any(), any()))
+ .thenReturn(mockRtResizableComponent)
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val resizableComponent = ResizableComponent.create(mockRuntime)
+ assertThat(entity.addComponent(resizableComponent)).isTrue()
+
+ val testSize = Dimensions(0.5f, 0.6f, 0.7f)
+ resizableComponent.minimumSize = testSize
+
+ assertThat(resizableComponent.minimumSize).isEqualTo(testSize)
+ verify(mockRtResizableComponent).setMinimumSize(captor.capture())
+ val rtDimensions = captor.value
+ assertThat(rtDimensions.width).isEqualTo(0.5f)
+ assertThat(rtDimensions.height).isEqualTo(0.6f)
+ assertThat(rtDimensions.depth).isEqualTo(0.7f)
+ }
+
+ @Test
+ fun resizableComponent_setMaximumSizeInvokesRuntimeResizableComponentSetMaximumSize() {
+ val entity = session.createEntity("test")
+ val captor = ArgumentCaptor.forClass(JxrPlatformAdapter.Dimensions::class.java)
+ assertThat(entity).isNotNull()
+
+ val mockRtResizableComponent = mock<JxrPlatformAdapter.ResizableComponent>()
+ whenever(mockRuntime.createResizableComponent(any(), any()))
+ .thenReturn(mockRtResizableComponent)
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val resizableComponent = ResizableComponent.create(mockRuntime)
+ assertThat(entity.addComponent(resizableComponent)).isTrue()
+
+ val testSize = Dimensions(5f, 6f, 7f)
+ resizableComponent.maximumSize = testSize
+
+ assertThat(resizableComponent.maximumSize).isEqualTo(testSize)
+ verify(mockRtResizableComponent).setMaximumSize(captor.capture())
+ val rtDimensions = captor.value
+ assertThat(rtDimensions.width).isEqualTo(5f)
+ assertThat(rtDimensions.height).isEqualTo(6f)
+ assertThat(rtDimensions.depth).isEqualTo(7f)
+ }
+
+ @Test
+ fun addResizeListener_invokesRuntimeAddResizeEventListener() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+ val mockRtResizableComponent = mock<JxrPlatformAdapter.ResizableComponent>()
+ whenever(mockRuntime.createResizableComponent(any(), any()))
+ .thenReturn(mockRtResizableComponent)
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val resizableComponent = ResizableComponent.create(mockRuntime)
+ assertThat(entity.addComponent(resizableComponent)).isTrue()
+ val mockResizeListener = mock<ResizeListener>()
+ resizableComponent.addResizeListener(directExecutor(), mockResizeListener)
+
+ val captor = ArgumentCaptor.forClass(JxrPlatformAdapter.ResizeEventListener::class.java)
+ // Capture the runtime resize event listener that is provided to the runtime resizable
+ // component.
+ verify(mockRtResizableComponent).addResizeEventListener(any(), captor.capture())
+ val rtResizeEventListener = captor.value
+ var rtResizeEvent =
+ JxrPlatformAdapter.ResizeEvent(
+ JxrPlatformAdapter.ResizeEvent.RESIZE_STATE_START,
+ JxrPlatformAdapter.Dimensions(1f, 1f, 1f),
+ )
+ // Invoke the runtime resize event listener with a resize event.
+ rtResizeEventListener.onResizeEvent(rtResizeEvent)
+ verify(mockResizeListener).onResizeStart(any(), any())
+ rtResizeEvent =
+ JxrPlatformAdapter.ResizeEvent(
+ JxrPlatformAdapter.ResizeEvent.RESIZE_STATE_ONGOING,
+ JxrPlatformAdapter.Dimensions(2f, 2f, 2f),
+ )
+ rtResizeEventListener.onResizeEvent(rtResizeEvent)
+ rtResizeEventListener.onResizeEvent(rtResizeEvent)
+ verify(mockResizeListener, times(2)).onResizeUpdate(any(), any())
+ rtResizeEvent =
+ JxrPlatformAdapter.ResizeEvent(
+ JxrPlatformAdapter.ResizeEvent.RESIZE_STATE_END,
+ JxrPlatformAdapter.Dimensions(2f, 2f, 2f),
+ )
+ rtResizeEventListener.onResizeEvent(rtResizeEvent)
+ verify(mockResizeListener).onResizeEnd(any(), any())
+ }
+
+ @Test
+ fun addMultipleResizeEventListeners_invokesAllListeners() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+ val mockRtResizableComponent = mock<JxrPlatformAdapter.ResizableComponent>()
+ whenever(mockRuntime.createResizableComponent(any(), any()))
+ .thenReturn(mockRtResizableComponent)
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val resizableComponent = ResizableComponent.create(mockRuntime)
+ assertThat(entity.addComponent(resizableComponent)).isTrue()
+ val mockResizeListener = mock<ResizeListener>()
+ resizableComponent.addResizeListener(directExecutor(), mockResizeListener)
+ val mockResizeListener2 = mock<ResizeListener>()
+ resizableComponent.addResizeListener(directExecutor(), mockResizeListener2)
+
+ val captor = ArgumentCaptor.forClass(JxrPlatformAdapter.ResizeEventListener::class.java)
+ // Capture the runtime resize event listener that is provided to the runtime resizable
+ // component.
+ verify(mockRtResizableComponent, times(2)).addResizeEventListener(any(), captor.capture())
+ val rtResizeEventListener1 = captor.allValues[0]
+ val rtResizeEventListener2 = captor.allValues[1]
+ val rtResizeEvent =
+ JxrPlatformAdapter.ResizeEvent(
+ JxrPlatformAdapter.ResizeEvent.RESIZE_STATE_START,
+ JxrPlatformAdapter.Dimensions(1f, 1f, 1f),
+ )
+ // Invoke the runtime resize event listener with a resize event.
+ rtResizeEventListener1.onResizeEvent(rtResizeEvent)
+ rtResizeEventListener2.onResizeEvent(rtResizeEvent)
+ verify(mockResizeListener).onResizeStart(any(), any())
+ verify(mockResizeListener2).onResizeStart(any(), any())
+ }
+
+ @Test
+ fun removeResizeEventListener_invokesRuntimeRemoveResizeEventListener() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+ val mockRtResizableComponent = mock<JxrPlatformAdapter.ResizableComponent>()
+ whenever(mockRuntime.createResizableComponent(any(), any()))
+ .thenReturn(mockRtResizableComponent)
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val resizableComponent = ResizableComponent.create(mockRuntime)
+ assertThat(entity.addComponent(resizableComponent)).isTrue()
+ val mockResizeListener = mock<ResizeListener>()
+ resizableComponent.addResizeListener(directExecutor(), mockResizeListener)
+ val mockResizeListener2 = mock<ResizeListener>()
+ resizableComponent.addResizeListener(directExecutor(), mockResizeListener2)
+
+ val captor = ArgumentCaptor.forClass(JxrPlatformAdapter.ResizeEventListener::class.java)
+ // Capture the runtime resize event listener that is provided to the runtime resizable
+ // component.
+ verify(mockRtResizableComponent, times(2)).addResizeEventListener(any(), captor.capture())
+ val rtResizeEventListener1 = captor.allValues[0]
+ val rtResizeEventListener2 = captor.allValues[1]
+ val rtResizeEvent =
+ JxrPlatformAdapter.ResizeEvent(
+ JxrPlatformAdapter.ResizeEvent.RESIZE_STATE_START,
+ JxrPlatformAdapter.Dimensions(1f, 1f, 1f),
+ )
+ // Invoke the runtime resize event listener with a resize event.
+ rtResizeEventListener1.onResizeEvent(rtResizeEvent)
+ rtResizeEventListener2.onResizeEvent(rtResizeEvent)
+ verify(mockResizeListener).onResizeStart(any(), any())
+ verify(mockResizeListener2).onResizeStart(any(), any())
+
+ resizableComponent.removeResizeListener(mockResizeListener)
+ resizableComponent.removeResizeListener(mockResizeListener2)
+ verify(mockRtResizableComponent).removeResizeEventListener(rtResizeEventListener1)
+ verify(mockRtResizableComponent).removeResizeEventListener(rtResizeEventListener2)
+ }
+
+ @Test
+ fun resizableComponent_canAttachAgainAfterDetach() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+ whenever(mockRuntime.createResizableComponent(any(), any())).thenReturn(mock())
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val resizableComponent = ResizableComponent.create(mockRuntime)
+
+ assertThat(entity.addComponent(resizableComponent)).isTrue()
+ entity.removeComponent(resizableComponent)
+ assertThat(entity.addComponent(resizableComponent)).isTrue()
+ }
+
+ @Test
+ fun resizableComponent_attachAfterDetachPreservesListeners() {
+ val entity = session.createEntity("test")
+ assertThat(entity).isNotNull()
+ val mockRtResizableComponent = mock<JxrPlatformAdapter.ResizableComponent>()
+ whenever(mockRuntime.createResizableComponent(any(), any()))
+ .thenReturn(mockRtResizableComponent)
+ whenever(mockContentlessEntity.addComponent(any())).thenReturn(true)
+ val resizableComponent = ResizableComponent.create(mockRuntime)
+
+ assertThat(entity.addComponent(resizableComponent)).isTrue()
+ val mockResizeListener = mock<ResizeListener>()
+ resizableComponent.addResizeListener(directExecutor(), mockResizeListener)
+ val mockResizeListener2 = mock<ResizeListener>()
+ resizableComponent.addResizeListener(directExecutor(), mockResizeListener2)
+
+ val captor = ArgumentCaptor.forClass(JxrPlatformAdapter.ResizeEventListener::class.java)
+ // Capture the runtime resize event listener that is provided to the runtime resizable
+ // component.
+ verify(mockRtResizableComponent, times(2)).addResizeEventListener(any(), captor.capture())
+ val rtResizeEventListener1 = captor.allValues[0]
+ val rtResizeEventListener2 = captor.allValues[1]
+ val rtResizeEvent =
+ JxrPlatformAdapter.ResizeEvent(
+ JxrPlatformAdapter.ResizeEvent.RESIZE_STATE_START,
+ JxrPlatformAdapter.Dimensions(1f, 1f, 1f),
+ )
+ // Invoke the runtime resize event listener with a resize event.
+ rtResizeEventListener1.onResizeEvent(rtResizeEvent)
+ rtResizeEventListener2.onResizeEvent(rtResizeEvent)
+ verify(mockResizeListener).onResizeStart(any(), any())
+ verify(mockResizeListener2).onResizeStart(any(), any())
+
+ // Detach and reattach the resizable component.
+ entity.removeComponent(resizableComponent)
+ assertThat(entity.addComponent(resizableComponent)).isTrue()
+ // Invoke the runtime resize event listener with a resize event.
+ rtResizeEventListener1.onResizeEvent(rtResizeEvent)
+ rtResizeEventListener2.onResizeEvent(rtResizeEvent)
+ verify(mockResizeListener, times(2)).onResizeStart(any(), any())
+ verify(mockResizeListener2, times(2)).onResizeStart(any(), any())
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SessionTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SessionTest.kt
new file mode 100644
index 0000000..e46350b
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SessionTest.kt
@@ -0,0 +1,393 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+@file:Suppress("UNUSED_VARIABLE")
+
+package androidx.xr.scenecore
+
+import android.app.Activity
+import android.graphics.Rect
+import android.os.Bundle
+import android.widget.TextView
+import androidx.xr.scenecore.JxrPlatformAdapter.GltfModelResource
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.Futures
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import java.util.UUID
+import java.util.function.Consumer
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyString
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+
+/**
+ * Unit tests for the JXRCore SDK Session Interface.
+ *
+ * TODO(b/329902726): Add a TestRuntime and verify CPM Integration.
+ */
+@RunWith(RobolectricTestRunner::class)
+class SessionTest {
+ private val activityController = Robolectric.buildActivity(Activity::class.java)
+ private val activity = activityController.create().start().get()
+ private val mockRuntime = mock<JxrPlatformAdapter>()
+ private val mockAnchorEntity = mock<JxrPlatformAdapter.AnchorEntity>()
+ lateinit var session: Session
+
+ @Before
+ fun setUp() {
+ whenever(mockRuntime.spatialEnvironment).thenReturn(mock())
+ val mockActivitySpace = mock<JxrPlatformAdapter.ActivitySpace>()
+ whenever(mockRuntime.activitySpace).thenReturn(mockActivitySpace)
+ whenever(mockRuntime.headActivityPose).thenReturn(mock())
+ whenever(mockRuntime.activitySpaceRootImpl).thenReturn(mockActivitySpace)
+ whenever(mockRuntime.mainPanelEntity).thenReturn(mock())
+ whenever(mockRuntime.perceptionSpaceActivityPose).thenReturn(mock())
+ whenever(mockAnchorEntity.state)
+ .thenReturn(JxrPlatformAdapter.AnchorEntity.State.UNANCHORED)
+ whenever(mockAnchorEntity.persistState)
+ .thenReturn(JxrPlatformAdapter.AnchorEntity.PersistState.PERSIST_NOT_REQUESTED)
+ session = Session.create(activity, mockRuntime)
+ }
+
+ @Test
+ fun requestFullSpaceMode_callsThrough() {
+ session.requestFullSpaceMode()
+ verify(mockRuntime).requestFullSpaceMode()
+ }
+
+ @Test
+ fun requestHomeSpaceMode_callsThrough() {
+ session.requestHomeSpaceMode()
+ verify(mockRuntime).requestHomeSpaceMode()
+ }
+
+ @Test
+ fun createSession_runtimeProvided_createsSessionWithProvidedRuntime() {
+ assertThat(session).isNotNull()
+ assertThat(session.runtime).isNotNull()
+ assertThat(session.runtime).isEqualTo(mockRuntime)
+ }
+
+ @Test
+ fun createSession_twiceFromSameActivity_returnsSameInstance() {
+ val newSession = Session.create(activity, mockRuntime)
+ assertThat(session).isEqualTo(newSession)
+ }
+
+ @Test
+ fun createSession_differentActivities_returnsUniqueInstances() {
+ val newActivity = Robolectric.buildActivity(Activity::class.java).create().start().get()
+ val newSession = Session.create(newActivity, mockRuntime)
+ assertThat(session).isNotEqualTo(newSession)
+ }
+
+ @Test
+ fun createGltfResourceAsync_callsRuntimeLoadGltf() {
+ val mockGltfModelResource = mock<GltfModelResource>()
+ whenever(mockRuntime.loadGltfByAssetNameSplitEngine(anyString()))
+ .thenReturn(Futures.immediateFuture(mockGltfModelResource))
+ val unused = session.createGltfResourceAsync("test.glb")
+
+ verify(mockRuntime).loadGltfByAssetNameSplitEngine("test.glb")
+ }
+
+ @Test
+ fun createGltfEntity_callsRuntimeCreateGltfEntity() {
+ whenever(mockRuntime.loadGltfByAssetNameSplitEngine(anyString()))
+ .thenReturn(Futures.immediateFuture(mock()))
+ whenever(mockRuntime.createGltfEntity(any(), any(), any())).thenReturn(mock())
+ val gltfModelFuture = session.createGltfResourceAsync("test.glb")
+ val unused = session.createGltfEntity(gltfModelFuture.get())
+
+ verify(mockRuntime).loadGltfByAssetNameSplitEngine(eq("test.glb"))
+ verify(mockRuntime).createGltfEntity(any(), any(), any())
+ }
+
+ @Test
+ fun createPanelEntity_callsRuntimeCreatePanelEntity() {
+ val view = TextView(activity)
+ whenever(mockRuntime.createPanelEntity(any(), any(), any(), any(), any(), any(), any()))
+ .thenReturn(mock())
+ val unused =
+ session.createPanelEntity(
+ view,
+ Dimensions(720f, 480f),
+ Dimensions(0.1f, 0.1f, 0.1f),
+ "test"
+ )
+
+ verify(mockRuntime).createPanelEntity(any(), any(), any(), any(), any(), any(), any())
+ }
+
+ @Test
+ fun createAnchorEntity_callsRuntimeCreateAnchorEntity() {
+ whenever(mockRuntime.createAnchorEntity(any(), any(), any(), anyOrNull()))
+ .thenReturn(mockAnchorEntity)
+ val unused = session.createAnchorEntity(Dimensions(), PlaneType.ANY, PlaneSemantic.ANY)
+
+ verify(mockRuntime).createAnchorEntity(any(), any(), any(), anyOrNull())
+ }
+
+ @Test
+ fun getActivitySpace_returnsActivitySpace() {
+ val activitySpace = session.activitySpace
+
+ assertThat(activitySpace).isNotNull()
+ }
+
+ @Test
+ fun getActivitySpaceTwice_returnsSameSpace() {
+ val activitySpace1 = session.activitySpace
+ val activitySpace2 = session.activitySpace
+
+ assertThat(activitySpace1).isEqualTo(activitySpace2)
+ }
+
+ @Test
+ fun getActivitySpaceRoot_returnsActivitySpaceRoot() {
+ val activitySpaceRoot = session.activitySpaceRoot
+
+ assertThat(activitySpaceRoot).isNotNull()
+ }
+
+ @Test
+ fun getActivitySpaceRootTwice_returnsSameSpace() {
+ val activitySpaceRoot1 = session.activitySpaceRoot
+ val activitySpaceRoot2 = session.activitySpaceRoot
+
+ assertThat(activitySpaceRoot1).isEqualTo(activitySpaceRoot2)
+ }
+
+ @Test
+ fun getSpatialUser_returnsSpatialUser() {
+ val spatialUser = session.spatialUser
+
+ assertThat(spatialUser).isNotNull()
+ }
+
+ @Test
+ fun getSpatialUserTwice_returnsSameUser() {
+ val spatialUser1 = session.spatialUser
+ val spatialUser2 = session.spatialUser
+
+ assertThat(spatialUser1).isEqualTo(spatialUser2)
+ }
+
+ @Test
+ fun getPerceptionSpace_returnPerceptionSpace() {
+ val perceptionSpace = session.perceptionSpace
+
+ assertThat(perceptionSpace).isNotNull()
+ }
+
+ @Test
+ fun createActivityPanelEntity_callsRuntimeCreateActivityPanelEntity() {
+ whenever(mockRuntime.createActivityPanelEntity(any(), any(), any(), any(), any()))
+ .thenReturn(mock())
+ val unused = session.createActivityPanelEntity(Rect(0, 0, 640, 480), "test")
+
+ verify(mockRuntime).createActivityPanelEntity(any(), any(), any(), any(), any())
+ }
+
+ @Test
+ fun getMainPanelEntity_returnsPanelEntity() {
+ val unused = session.mainPanelEntity
+ val unusedAgain = session.mainPanelEntity
+
+ verify(mockRuntime, times(1)).mainPanelEntity
+ }
+
+ @Test
+ fun createPersistedAnchorEntity_callsRuntimecreatePersistedAnchorEntity() {
+ whenever(mockRuntime.createPersistedAnchorEntity(any(), any())).thenReturn(mockAnchorEntity)
+ val unused = session.createPersistedAnchorEntity(UUID.randomUUID())
+
+ verify(mockRuntime).createPersistedAnchorEntity(any(), any())
+ }
+
+ @Test
+ fun unpersistAnchor_callsRuntimeunpersistAnchor_returnsTrue() {
+ val uuid = UUID.randomUUID()
+ whenever(mockRuntime.unpersistAnchor(uuid)).thenReturn(true)
+ assertThat(session.unpersistAnchor(uuid)).isTrue()
+ verify(mockRuntime).unpersistAnchor(uuid)
+ }
+
+ fun unpersistAnchor_callsRuntimeunpersistAnchor_returnsFalse() {
+ val uuid = UUID.randomUUID()
+ whenever(mockRuntime.unpersistAnchor(uuid)).thenReturn(false)
+ assertThat(session.unpersistAnchor(uuid)).isFalse()
+ verify(mockRuntime).unpersistAnchor(uuid)
+ }
+
+ @Test
+ fun createInteractableComponent_callsRuntimeCreateInteractableComponent() {
+ whenever(mockRuntime.createInteractableComponent(any(), any())).thenReturn(mock())
+
+ val interactableComponent = session.createInteractableComponent(directExecutor(), mock())
+ val view = TextView(activity)
+ val mockPanelEntity = mock<JxrPlatformAdapter.PanelEntity>()
+ whenever(mockRuntime.createPanelEntity(any(), any(), any(), any(), any(), any(), any()))
+ .thenReturn(mockPanelEntity)
+ whenever(mockPanelEntity.addComponent(any())).thenReturn(true)
+ val panelEntity =
+ session.createPanelEntity(
+ view,
+ Dimensions(720f, 480f),
+ Dimensions(0.1f, 0.1f, 0.1f),
+ "test"
+ )
+ assertThat(panelEntity.addComponent(interactableComponent)).isTrue()
+
+ verify(mockRuntime).createInteractableComponent(any(), anyOrNull())
+ }
+
+ @Test
+ fun createMovableComponent_callsRuntimeCreateMovableComponent() {
+ whenever(mockRuntime.createMovableComponent(any(), any(), any(), any())).thenReturn(mock())
+
+ val movableComponent = session.createMovableComponent()
+ val view = TextView(activity)
+ val mockRtPanelEntity = mock<JxrPlatformAdapter.PanelEntity>()
+ whenever(mockRuntime.createPanelEntity(any(), any(), any(), any(), any(), any(), any()))
+ .thenReturn(mockRtPanelEntity)
+ whenever(mockRtPanelEntity.addComponent(any())).thenReturn(true)
+ val panelEntity =
+ session.createPanelEntity(
+ view,
+ Dimensions(720f, 480f),
+ Dimensions(0.1f, 0.1f, 0.1f),
+ "test"
+ )
+ assertThat(panelEntity.addComponent(movableComponent)).isTrue()
+
+ verify(mockRuntime).createMovableComponent(any(), any(), any(), any())
+ }
+
+ @Test
+ fun createResizableComponent_callsRuntimeCreateResizableComponent() {
+ whenever(mockRuntime.createResizableComponent(any(), any())).thenReturn(mock())
+
+ val resizableComponent = session.createResizableComponent()
+ val view = TextView(activity)
+ val mockRtPanelEntity = mock<JxrPlatformAdapter.PanelEntity>()
+ whenever(mockRtPanelEntity.getSize()).thenReturn(JxrPlatformAdapter.Dimensions(1f, 1f, 1f))
+ whenever(mockRuntime.createPanelEntity(any(), any(), any(), any(), any(), any(), any()))
+ .thenReturn(mockRtPanelEntity)
+ whenever(mockRtPanelEntity.addComponent(any())).thenReturn(true)
+ val panelEntity =
+ session.createPanelEntity(
+ view,
+ Dimensions(720f, 480f),
+ Dimensions(0.1f, 0.1f, 0.1f),
+ "test"
+ )
+ assertThat(panelEntity.addComponent(resizableComponent)).isTrue()
+
+ verify(mockRuntime).createResizableComponent(any(), any())
+ }
+
+ @Test
+ fun setFullSpaceMode_callsThrough() {
+ // Test that Session calls into the runtime.
+ val bundle = Bundle().apply { putString("testkey", "testval") }
+ whenever(mockRuntime.setFullSpaceMode(any())).thenReturn(bundle)
+ val unused = session.setFullSpaceMode(bundle)
+ verify(mockRuntime).setFullSpaceMode(bundle)
+ }
+
+ @Test
+ fun setFullSpaceModeWithEnvironmentInherited_callsThrough() {
+ // Test that Session calls into the runtime.
+ val bundle = Bundle().apply { putString("testkey", "testval") }
+ whenever(mockRuntime.setFullSpaceModeWithEnvironmentInherited(any())).thenReturn(bundle)
+ val unused = session.setFullSpaceModeWithEnvironmentInherited(bundle)
+ verify(mockRuntime).setFullSpaceModeWithEnvironmentInherited(bundle)
+ }
+
+ @Test
+ fun setPreferredAspectRatio_callsThrough() {
+ // Test that Session calls into the runtime.
+ session.setPreferredAspectRatio(activity, 1.23f)
+ verify(mockRuntime).setPreferredAspectRatio(activity, 1.23f)
+ }
+
+ @Test
+ fun getPanelEntityType_returnsAllPanelEntities() {
+ val mockPanelEntity1 = mock<JxrPlatformAdapter.PanelEntity>()
+ val mockActivityPanelEntity = mock<JxrPlatformAdapter.ActivityPanelEntity>()
+ whenever(mockRuntime.createPanelEntity(any(), any(), any(), any(), any(), any(), any()))
+ .thenReturn(mockPanelEntity1)
+ whenever(mockRuntime.createActivityPanelEntity(any(), any(), any(), any(), any()))
+ .thenReturn(mockActivityPanelEntity)
+ val panelEntity =
+ session.createPanelEntity(
+ TextView(activity),
+ Dimensions(720f, 480f),
+ Dimensions(0.1f, 0.1f, 0.1f),
+ "test1",
+ )
+ val activityPanelEntity = session.createActivityPanelEntity(Rect(0, 0, 640, 480), "test2")
+
+ assertThat(session.getEntitiesOfType(PanelEntity::class.java))
+ .containsAtLeast(panelEntity, activityPanelEntity)
+ }
+
+ @Test
+ fun getEntitiesBaseType_returnsAllEntities() {
+ val mockPanelEntity = mock<JxrPlatformAdapter.PanelEntity>()
+ whenever(mockRuntime.createPanelEntity(any(), any(), any(), any(), any(), any(), any()))
+ .thenReturn(mockPanelEntity)
+ whenever(mockRuntime.createAnchorEntity(any(), any(), any(), any()))
+ .thenReturn(mockAnchorEntity)
+ val panelEntity =
+ session.createPanelEntity(
+ TextView(activity),
+ Dimensions(720f, 480f),
+ Dimensions(0.1f, 0.1f, 0.1f),
+ "test1",
+ )
+ val anchorEntity =
+ session.createAnchorEntity(Dimensions(), PlaneType.ANY, PlaneSemantic.ANY)
+
+ assertThat(session.getEntitiesOfType(Entity::class.java))
+ .containsAtLeast(panelEntity, anchorEntity)
+ }
+
+ @Test
+ fun addAndRemoveSpatialCapabilitiesChangedListener_callsRuntimeAddAndRemove() {
+ val listener = Consumer<SpatialCapabilities> { _ -> }
+ session.addSpatialCapabilitiesChangedListener(listener = listener)
+ verify(mockRuntime).addSpatialCapabilitiesChangedListener(any(), any())
+ session.removeSpatialCapabilitiesChangedListener(listener)
+ verify(mockRuntime).removeSpatialCapabilitiesChangedListener(any())
+ }
+
+ @Test
+ fun onDestroy_callsRuntimeDispose() {
+ activityController.destroy()
+ verify(mockRuntime).dispose()
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SoundFieldAttributesTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SoundFieldAttributesTest.kt
new file mode 100644
index 0000000..e2642e0
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SoundFieldAttributesTest.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class SoundFieldAttributesTest {
+
+ @Test
+ fun init_createsCorrectRuntimeAmbisonicsIntDef() {
+ val firstOrderAttributes =
+ SoundFieldAttributes(SpatializerConstants.AMBISONICS_ORDER_FIRST_ORDER)
+ val firstOrderRtAttributes = firstOrderAttributes.rtSoundFieldAttributes
+ assertThat(firstOrderRtAttributes.ambisonicsOrder)
+ .isEqualTo(JxrPlatformAdapter.SpatializerConstants.AMBISONICS_ORDER_FIRST_ORDER)
+
+ val secondOrderAttributes =
+ SoundFieldAttributes(SpatializerConstants.AMBISONICS_ORDER_SECOND_ORDER)
+ val secondOrderRtAttributes = secondOrderAttributes.rtSoundFieldAttributes
+ assertThat(secondOrderRtAttributes.ambisonicsOrder)
+ .isEqualTo(JxrPlatformAdapter.SpatializerConstants.AMBISONICS_ORDER_SECOND_ORDER)
+
+ val thirdOrderAttributes =
+ SoundFieldAttributes(SpatializerConstants.AMBISONICS_ORDER_THIRD_ORDER)
+ val thirdOrderRtAttributes = thirdOrderAttributes.rtSoundFieldAttributes
+ assertThat(thirdOrderRtAttributes.ambisonicsOrder)
+ .isEqualTo(JxrPlatformAdapter.SpatializerConstants.AMBISONICS_ORDER_THIRD_ORDER)
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SpatialAudioTrackTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SpatialAudioTrackTest.kt
new file mode 100644
index 0000000..35d99c0
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SpatialAudioTrackTest.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import android.app.Activity
+import android.media.AudioTrack
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argWhere
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class SpatialAudioTrackTest {
+
+ private var mockRuntime: JxrPlatformAdapter = mock()
+ private var mockRtAudioTrackExtensions: JxrPlatformAdapter.AudioTrackExtensionsWrapper = mock()
+
+ private val mockContentlessEntity = mock<JxrPlatformAdapter.Entity>()
+ private val activity = Robolectric.buildActivity(Activity::class.java).create().start().get()
+
+ private lateinit var session: Session
+
+ @Before
+ fun setUp() {
+ mockRuntime.stub {
+ on { spatialEnvironment } doReturn mock()
+ on { activitySpace } doReturn mock()
+ on { activitySpaceRootImpl } doReturn mock()
+ on { headActivityPose } doReturn mock()
+ on { perceptionSpaceActivityPose } doReturn mock()
+ on { mainPanelEntity } doReturn mock()
+ on { createEntity(any(), any(), any()) } doReturn mockContentlessEntity
+ }
+
+ mockRtAudioTrackExtensions = mock()
+ whenever(mockRuntime.audioTrackExtensionsWrapper).thenReturn(mockRtAudioTrackExtensions)
+ session = Session.create(activity, mockRuntime)
+ }
+
+ @Test
+ fun setWithPointSource_callsRuntimeAudioTrackBuilderSetPointSource() {
+ val builder = AudioTrack.Builder()
+
+ val entity = session.createEntity("test")
+ val pointSourceAttributes = PointSourceAttributes(entity)
+
+ whenever(
+ mockRtAudioTrackExtensions.setPointSourceAttributes(
+ eq(builder),
+ any<JxrPlatformAdapter.PointSourceAttributes>(),
+ )
+ )
+ .thenReturn(builder)
+
+ val actualBuilder =
+ SpatialAudioTrackBuilder.setPointSourceAttributes(
+ session,
+ builder,
+ pointSourceAttributes
+ )
+
+ verify(mockRtAudioTrackExtensions)
+ .setPointSourceAttributes(
+ eq(builder),
+ argWhere<JxrPlatformAdapter.PointSourceAttributes> {
+ it.entity == mockContentlessEntity
+ },
+ )
+ assertThat(actualBuilder).isEqualTo(builder)
+ }
+
+ @Test
+ fun setWithSoundField_callsRuntimeAudioTrackBuilderSetSoundField() {
+ val builder = AudioTrack.Builder()
+ val soundFieldAttributes =
+ SoundFieldAttributes(SpatializerConstants.AMBISONICS_ORDER_FIRST_ORDER)
+
+ whenever(
+ mockRtAudioTrackExtensions.setSoundFieldAttributes(
+ eq(builder),
+ any<JxrPlatformAdapter.SoundFieldAttributes>(),
+ )
+ )
+ .thenReturn(builder)
+
+ val actualBuilder =
+ SpatialAudioTrackBuilder.setSoundFieldAttributes(session, builder, soundFieldAttributes)
+
+ verify(mockRtAudioTrackExtensions)
+ .setSoundFieldAttributes(
+ eq(builder),
+ argWhere<JxrPlatformAdapter.SoundFieldAttributes> {
+ it.ambisonicsOrder == SpatializerConstants.AMBISONICS_ORDER_FIRST_ORDER
+ },
+ )
+ assertThat(actualBuilder).isEqualTo(builder)
+ }
+
+ @Test
+ fun getSourceType_callsRuntimeAudioTrackGetSourceType() {
+ val audioTrack = AudioTrack.Builder().build()
+ val expectedSourceType = JxrPlatformAdapter.SpatializerConstants.SOURCE_TYPE_POINT_SOURCE
+
+ whenever(mockRtAudioTrackExtensions.getSpatialSourceType(eq(audioTrack)))
+ .thenReturn(expectedSourceType)
+
+ val sourceType = SpatialAudioTrack.getSpatialSourceType(session, audioTrack)
+
+ verify(mockRtAudioTrackExtensions).getSpatialSourceType(eq(audioTrack))
+ assertThat(sourceType).isEqualTo(expectedSourceType)
+ }
+
+ @Test
+ fun getPointSourceAttributes_callsRuntimeAudioTrackGetPointSourceAttributes() {
+ val audioTrack = AudioTrack.Builder().build()
+ val entity = session.createEntity("test")
+
+ val temp: BaseEntity<*> = entity as BaseEntity<*>
+ val rtEntity = temp.rtEntity
+ val rtPointSourceAttributes = JxrPlatformAdapter.PointSourceAttributes(rtEntity)
+
+ whenever(mockRtAudioTrackExtensions.getPointSourceAttributes(eq(audioTrack)))
+ .thenReturn(rtPointSourceAttributes)
+
+ val pointSourceAttributes = SpatialAudioTrack.getPointSourceAttributes(session, audioTrack)
+
+ verify(mockRtAudioTrackExtensions).getPointSourceAttributes(eq(audioTrack))
+ assertThat((pointSourceAttributes!!.entity as BaseEntity<*>).rtEntity).isEqualTo(rtEntity)
+ }
+
+ @Test
+ fun getPointSourceAttributes_returnsNullIfNotInRuntime() {
+ val audioTrack = AudioTrack.Builder().build()
+
+ whenever(mockRtAudioTrackExtensions.getPointSourceAttributes(eq(audioTrack)))
+ .thenReturn(null)
+
+ val pointSourceAttributes = SpatialAudioTrack.getPointSourceAttributes(session, audioTrack)
+
+ assertThat(pointSourceAttributes).isNull()
+ }
+
+ @Test
+ fun getSoundFieldAttributes_callsRuntimeAudioTrackGetPointSourceAttributes() {
+ val audioTrack = AudioTrack.Builder().build()
+ val expectedAmbisonicsOrder = SpatializerConstants.AMBISONICS_ORDER_THIRD_ORDER
+ val rtSoundFieldAttributes =
+ JxrPlatformAdapter.SoundFieldAttributes(expectedAmbisonicsOrder)
+
+ whenever(mockRtAudioTrackExtensions.getSoundFieldAttributes(eq(audioTrack)))
+ .thenReturn(rtSoundFieldAttributes)
+
+ val soundFieldAttributes = SpatialAudioTrack.getSoundFieldAttributes(session, audioTrack)
+
+ verify(mockRtAudioTrackExtensions).getSoundFieldAttributes(eq(audioTrack))
+ assertThat(soundFieldAttributes?.order).isEqualTo(expectedAmbisonicsOrder)
+ }
+
+ @Test
+ fun getSoundFieldAttributes_returnsNullIfNotInRuntime() {
+ val audioTrack = AudioTrack.Builder().build()
+
+ whenever(mockRtAudioTrackExtensions.getSoundFieldAttributes(eq(audioTrack)))
+ .thenReturn(null)
+
+ val soundFieldAttributes = SpatialAudioTrack.getSoundFieldAttributes(session, audioTrack)
+
+ assertThat(soundFieldAttributes?.order).isNull()
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SpatialEnvironmentTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SpatialEnvironmentTest.kt
new file mode 100644
index 0000000..1297b37
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SpatialEnvironmentTest.kt
@@ -0,0 +1,318 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import com.google.common.truth.Truth.assertThat
+import java.util.function.Consumer
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/**
+ * Unit tests for the JXRCore SDK SpatialEnvironment Interface.
+ *
+ * TODO(b/329902726): Add a TestRuntime and verify CPM Integration.
+ */
+@RunWith(JUnit4::class)
+class SpatialEnvironmentTest {
+
+ private var mockRuntime: JxrPlatformAdapter = mock<JxrPlatformAdapter>()
+ private var mockRtEnvironment: JxrPlatformAdapter.SpatialEnvironment? = null
+ private var environment: SpatialEnvironment? = null
+
+ @Before
+ fun setUp() {
+ mockRtEnvironment = mock<JxrPlatformAdapter.SpatialEnvironment>()
+ whenever(mockRuntime.spatialEnvironment).thenReturn(mockRtEnvironment)
+
+ environment = SpatialEnvironment(mockRuntime)
+ }
+
+ @Test
+ fun getCurrentPassthroughOpacity_getsRuntimePassthroughOpacity() {
+ val rtOpacity = 0.3f
+ whenever(mockRtEnvironment!!.currentPassthroughOpacity).thenReturn(rtOpacity)
+ assertThat(environment!!.getCurrentPassthroughOpacity()).isEqualTo(rtOpacity)
+ verify(mockRtEnvironment!!).currentPassthroughOpacity
+ }
+
+ @Test
+ fun getPassthroughOpacityPreference_getsRuntimePassthroughOpacityPreference() {
+ val rtPreference = 0.3f
+ whenever(mockRtEnvironment!!.passthroughOpacityPreference).thenReturn(rtPreference)
+
+ assertThat(environment!!.getPassthroughOpacityPreference()).isEqualTo(rtPreference)
+ verify(mockRtEnvironment!!).passthroughOpacityPreference
+ }
+
+ @Test
+ fun getPassthroughOpacityPreferenceNull_getsRuntimePassthroughOpacityPreference() {
+ val rtPreference = null as Float?
+ whenever(mockRtEnvironment!!.passthroughOpacityPreference).thenReturn(rtPreference)
+
+ assertThat(environment!!.getPassthroughOpacityPreference()).isEqualTo(rtPreference)
+ verify(mockRtEnvironment!!).passthroughOpacityPreference
+ }
+
+ @Test
+ fun setPassthroughOpacityPreference_callsRuntimeSetPassthroughOpacityPreference() {
+ val preference = 0.3f
+
+ whenever(mockRtEnvironment!!.setPassthroughOpacityPreference(any()))
+ .thenReturn(
+ JxrPlatformAdapter.SpatialEnvironment.SetPassthroughOpacityPreferenceResult
+ .CHANGE_APPLIED
+ )
+ assertThat(environment!!.setPassthroughOpacityPreference(preference))
+ .isInstanceOf(
+ SpatialEnvironment.SetPassthroughOpacityPreferenceChangeApplied::class.java
+ )
+
+ whenever(mockRtEnvironment!!.setPassthroughOpacityPreference(any()))
+ .thenReturn(
+ JxrPlatformAdapter.SpatialEnvironment.SetPassthroughOpacityPreferenceResult
+ .CHANGE_PENDING
+ )
+ assertThat(environment!!.setPassthroughOpacityPreference(preference))
+ .isInstanceOf(
+ SpatialEnvironment.SetPassthroughOpacityPreferenceChangePending::class.java
+ )
+
+ verify(mockRtEnvironment!!, times(2)).setPassthroughOpacityPreference(preference)
+ }
+
+ @Test
+ fun setPassthroughOpacityPreferenceNull_callsRuntimeSetPassthroughOpacityPreference() {
+ val preference = null as Float?
+
+ whenever(mockRtEnvironment!!.setPassthroughOpacityPreference(anyOrNull()))
+ .thenReturn(
+ JxrPlatformAdapter.SpatialEnvironment.SetPassthroughOpacityPreferenceResult
+ .CHANGE_APPLIED
+ )
+ assertThat(environment!!.setPassthroughOpacityPreference(preference))
+ .isInstanceOf(
+ SpatialEnvironment.SetPassthroughOpacityPreferenceChangeApplied::class.java
+ )
+
+ whenever(mockRtEnvironment!!.setPassthroughOpacityPreference(anyOrNull()))
+ .thenReturn(
+ JxrPlatformAdapter.SpatialEnvironment.SetPassthroughOpacityPreferenceResult
+ .CHANGE_PENDING
+ )
+ assertThat(environment!!.setPassthroughOpacityPreference(preference))
+ .isInstanceOf(
+ SpatialEnvironment.SetPassthroughOpacityPreferenceChangePending::class.java
+ )
+
+ verify(mockRtEnvironment!!, times(2)).setPassthroughOpacityPreference(preference)
+ }
+
+ @Test
+ fun addOnPassthroughOpacityChangedListener_ReceivesRuntimeOnPassthroughOpacityChangedEvents() {
+ var listenerCalledWithValue = 0.0f
+ val captor = argumentCaptor<Consumer<Float>>()
+ val listener = Consumer<Float> { floatValue: Float -> listenerCalledWithValue = floatValue }
+ environment!!.addOnPassthroughOpacityChangedListener(listener)
+ verify(mockRtEnvironment!!).addOnPassthroughOpacityChangedListener(captor.capture())
+ captor.firstValue.accept(0.3f)
+ assertThat(listenerCalledWithValue).isEqualTo(0.3f)
+ }
+
+ @Test
+ fun removeOnPassthroughOpacityChangedListener_callsRuntimeRemoveOnPassthroughOpacityChangedListener() {
+ val captor = argumentCaptor<Consumer<Float>>()
+ val listener = Consumer<Float> {}
+ environment!!.removeOnPassthroughOpacityChangedListener(listener)
+ verify(mockRtEnvironment!!).removeOnPassthroughOpacityChangedListener(captor.capture())
+ assertThat(captor.firstValue).isEqualTo(listener)
+ }
+
+ @Test
+ fun spatialEnvironmentPreferenceEqualsHashcode_returnsTrueIfAllPropertiesAreEqual() {
+ val rtImageMock = mock<JxrPlatformAdapter.ExrImageResource>()
+ val rtModelMock = mock<JxrPlatformAdapter.GltfModelResource>()
+ val rtPreference =
+ JxrPlatformAdapter.SpatialEnvironment.SpatialEnvironmentPreference(
+ rtImageMock,
+ rtModelMock
+ )
+ val preference =
+ SpatialEnvironment.SpatialEnvironmentPreference(
+ ExrImage(rtImageMock),
+ GltfModel(rtModelMock)
+ )
+
+ assertThat(preference).isEqualTo(rtPreference.toSpatialEnvironmentPreference())
+ assertThat(preference.hashCode())
+ .isEqualTo(rtPreference.toSpatialEnvironmentPreference().hashCode())
+ }
+
+ @Test
+ fun spatialEnvironmentPreferenceEqualsHashcode_returnsFalseIfAnyPropertiesAreNotEqual() {
+ val rtImageMock = mock<JxrPlatformAdapter.ExrImageResource>()
+ val rtModelMock = mock<JxrPlatformAdapter.GltfModelResource>()
+ val rtImageMock2 = mock<JxrPlatformAdapter.ExrImageResource>()
+ val rtModelMock2 = mock<JxrPlatformAdapter.GltfModelResource>()
+ val rtPreference =
+ JxrPlatformAdapter.SpatialEnvironment.SpatialEnvironmentPreference(
+ rtImageMock,
+ rtModelMock
+ )
+
+ val preferenceDiffGeometry =
+ SpatialEnvironment.SpatialEnvironmentPreference(
+ ExrImage(rtImageMock),
+ GltfModel(rtModelMock2),
+ )
+ assertThat(preferenceDiffGeometry)
+ .isNotEqualTo(rtPreference.toSpatialEnvironmentPreference())
+ assertThat(preferenceDiffGeometry.hashCode())
+ .isNotEqualTo(rtPreference.toSpatialEnvironmentPreference().hashCode())
+
+ val preferenceDiffSkybox =
+ SpatialEnvironment.SpatialEnvironmentPreference(
+ ExrImage(rtImageMock2),
+ GltfModel(rtModelMock),
+ )
+ assertThat(preferenceDiffSkybox).isNotEqualTo(rtPreference.toSpatialEnvironmentPreference())
+ assertThat(preferenceDiffSkybox.hashCode())
+ .isNotEqualTo(rtPreference.toSpatialEnvironmentPreference().hashCode())
+ }
+
+ @Test
+ fun setSpatialEnvironmentPreference_returnsRuntimeEnvironmentResultObject() {
+ val rtImageMock = mock<JxrPlatformAdapter.ExrImageResource>()
+ val rtModelMock = mock<JxrPlatformAdapter.GltfModelResource>()
+
+ val preference =
+ SpatialEnvironment.SpatialEnvironmentPreference(
+ ExrImage(rtImageMock),
+ GltfModel(rtModelMock)
+ )
+
+ whenever(mockRtEnvironment!!.setSpatialEnvironmentPreference(any()))
+ .thenReturn(
+ JxrPlatformAdapter.SpatialEnvironment.SetSpatialEnvironmentPreferenceResult
+ .CHANGE_APPLIED
+ )
+ assertThat(environment!!.setSpatialEnvironmentPreference(preference))
+ .isInstanceOf(
+ SpatialEnvironment.SetSpatialEnvironmentPreferenceChangeApplied::class.java
+ )
+
+ whenever(mockRtEnvironment!!.setSpatialEnvironmentPreference(any()))
+ .thenReturn(
+ JxrPlatformAdapter.SpatialEnvironment.SetSpatialEnvironmentPreferenceResult
+ .CHANGE_PENDING
+ )
+ assertThat(environment!!.setSpatialEnvironmentPreference(preference))
+ .isInstanceOf(
+ SpatialEnvironment.SetSpatialEnvironmentPreferenceChangePending::class.java
+ )
+
+ verify(mockRtEnvironment!!, times(2)).setSpatialEnvironmentPreference(any())
+ }
+
+ @Test
+ fun setSpatialEnvironmentPreferenceNull_returnsRuntimeEnvironmentResultObject() {
+ val preference = null as SpatialEnvironment.SpatialEnvironmentPreference?
+
+ whenever(mockRtEnvironment!!.setSpatialEnvironmentPreference(anyOrNull()))
+ .thenReturn(
+ JxrPlatformAdapter.SpatialEnvironment.SetSpatialEnvironmentPreferenceResult
+ .CHANGE_APPLIED
+ )
+ assertThat(environment!!.setSpatialEnvironmentPreference(preference))
+ .isInstanceOf(
+ SpatialEnvironment.SetSpatialEnvironmentPreferenceChangeApplied::class.java
+ )
+
+ whenever(mockRtEnvironment!!.setSpatialEnvironmentPreference(anyOrNull()))
+ .thenReturn(
+ JxrPlatformAdapter.SpatialEnvironment.SetSpatialEnvironmentPreferenceResult
+ .CHANGE_PENDING
+ )
+ assertThat(environment!!.setSpatialEnvironmentPreference(preference))
+ .isInstanceOf(
+ SpatialEnvironment.SetSpatialEnvironmentPreferenceChangePending::class.java
+ )
+
+ verify(mockRtEnvironment!!, times(2)).setSpatialEnvironmentPreference(anyOrNull())
+ }
+
+ @Test
+ fun getSpatialEnvironmentPreference_getsRuntimeEnvironmentSpatialEnvironmentPreference() {
+ val rtImageMock = mock<JxrPlatformAdapter.ExrImageResource>()
+ val rtModelMock = mock<JxrPlatformAdapter.GltfModelResource>()
+ val rtPreference =
+ JxrPlatformAdapter.SpatialEnvironment.SpatialEnvironmentPreference(
+ rtImageMock,
+ rtModelMock
+ )
+ whenever(mockRtEnvironment!!.spatialEnvironmentPreference).thenReturn(rtPreference)
+
+ assertThat(environment!!.getSpatialEnvironmentPreference())
+ .isEqualTo(rtPreference.toSpatialEnvironmentPreference())
+ verify(mockRtEnvironment!!).spatialEnvironmentPreference
+ }
+
+ @Test
+ fun getSpatialEnvironmentPreferenceNull_getsRuntimeEnvironmentSpatialEnvironmentPreference() {
+ val rtPreference =
+ null as JxrPlatformAdapter.SpatialEnvironment.SpatialEnvironmentPreference?
+ whenever(mockRtEnvironment!!.spatialEnvironmentPreference).thenReturn(rtPreference)
+
+ assertThat(environment!!.getSpatialEnvironmentPreference()).isEqualTo(null)
+ verify(mockRtEnvironment!!).spatialEnvironmentPreference
+ }
+
+ @Test
+ fun isSpatialEnvironmentPreferenceActive_callsRuntimeEnvironmentisSpatialEnvironmentPreferenceActive() {
+ whenever(mockRtEnvironment!!.isSpatialEnvironmentPreferenceActive).thenReturn(true)
+ assertThat(environment!!.isSpatialEnvironmentPreferenceActive()).isTrue()
+ verify(mockRtEnvironment!!).isSpatialEnvironmentPreferenceActive
+ }
+
+ @Test
+ fun addOnSpatialEnvironmentChangedListener_ReceivesRuntimeEnvironmentOnEnvironmentChangedEvents() {
+ var listenerCalled = false
+ val captor = argumentCaptor<Consumer<Boolean>>()
+ val listener = Consumer<Boolean> { called: Boolean -> listenerCalled = called }
+ environment!!.addOnSpatialEnvironmentChangedListener(listener)
+ verify(mockRtEnvironment!!).addOnSpatialEnvironmentChangedListener(captor.capture())
+ captor.firstValue.accept(true)
+ assertThat(listenerCalled).isTrue()
+ }
+
+ @Test
+ fun removeOnSpatialEnvironmentChangedListener_callsRuntimeRemoveOnSpatialEnvironmentChangedListener() {
+ val captor = argumentCaptor<Consumer<Boolean>>()
+ val listener = Consumer<Boolean> {}
+ environment!!.removeOnSpatialEnvironmentChangedListener(listener)
+ verify(mockRtEnvironment!!).removeOnSpatialEnvironmentChangedListener(captor.capture())
+ assertThat(listener).isEqualTo(captor.firstValue)
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SpatialMediaPlayerTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SpatialMediaPlayerTest.kt
new file mode 100644
index 0000000..405c604
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SpatialMediaPlayerTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import android.app.Activity
+import android.media.MediaPlayer
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argWhere
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class SpatialMediaPlayerTest {
+
+ private var mockRuntime: JxrPlatformAdapter = mock()
+ private var mockRtMediaPlayerExtensions: JxrPlatformAdapter.MediaPlayerExtensionsWrapper =
+ mock()
+
+ private val mockContentlessEntity = mock<JxrPlatformAdapter.Entity>()
+ private val activity = Robolectric.buildActivity(Activity::class.java).create().start().get()
+
+ private lateinit var session: Session
+
+ @Before
+ fun setUp() {
+ mockRuntime.stub {
+ on { spatialEnvironment } doReturn mock()
+ on { activitySpace } doReturn mock()
+ on { activitySpaceRootImpl } doReturn mock()
+ on { headActivityPose } doReturn mock()
+ on { perceptionSpaceActivityPose } doReturn mock()
+ on { mainPanelEntity } doReturn mock()
+ on { createEntity(any(), any(), any()) } doReturn mockContentlessEntity
+ }
+
+ mockRtMediaPlayerExtensions = mock()
+ whenever(mockRuntime.mediaPlayerExtensionsWrapper).thenReturn(mockRtMediaPlayerExtensions)
+ session = Session.create(activity, mockRuntime)
+ }
+
+ @Test
+ fun setWithPointSource_callsRuntimeMediaPlayerSetPointSource() {
+ val mediaPlayer = MediaPlayer()
+
+ val entity = session.createEntity("test")
+ val pointSourceAttributes = PointSourceAttributes(entity)
+
+ SpatialMediaPlayer.setPointSourceAttributes(session, mediaPlayer, pointSourceAttributes)
+
+ verify(mockRtMediaPlayerExtensions)
+ .setPointSourceAttributes(
+ eq(mediaPlayer),
+ argWhere<JxrPlatformAdapter.PointSourceAttributes> {
+ it.entity == mockContentlessEntity
+ },
+ )
+ }
+
+ @Test
+ fun setWithSoundField_callsRuntimeMediaPlayerSetSoundField() {
+ val mediaPlayer = MediaPlayer()
+
+ val soundFieldAttributes =
+ SoundFieldAttributes(SpatializerConstants.AMBISONICS_ORDER_THIRD_ORDER)
+
+ SpatialMediaPlayer.setSoundFieldAttributes(session, mediaPlayer, soundFieldAttributes)
+
+ verify(mockRtMediaPlayerExtensions)
+ .setSoundFieldAttributes(
+ eq(mediaPlayer),
+ argWhere<JxrPlatformAdapter.SoundFieldAttributes> {
+ it.ambisonicsOrder == SpatializerConstants.AMBISONICS_ORDER_THIRD_ORDER
+ },
+ )
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SpatialSoundPoolTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SpatialSoundPoolTest.kt
new file mode 100644
index 0000000..991db52
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SpatialSoundPoolTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import android.app.Activity
+import android.media.SoundPool
+import androidx.xr.scenecore.JxrPlatformAdapter.SoundPoolExtensionsWrapper
+import androidx.xr.scenecore.SpatializerConstants.Companion.AMBISONICS_ORDER_FIRST_ORDER
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argWhere
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+
+/** Unit tests for the JXRCore SDK SpatialSoundPool Interface. */
+@RunWith(RobolectricTestRunner::class)
+class SpatialSoundPoolTest {
+
+ private var mockRuntime: JxrPlatformAdapter = mock()
+ private var mockRtSoundPoolExtensions: SoundPoolExtensionsWrapper = mock()
+
+ private val mockContentlessEntity = mock<JxrPlatformAdapter.Entity>()
+ private val activity = Robolectric.buildActivity(Activity::class.java).create().start().get()
+
+ private lateinit var session: Session
+
+ @Before
+ fun setUp() {
+ mockRuntime.stub {
+ on { spatialEnvironment } doReturn mock()
+ on { activitySpace } doReturn mock()
+ on { activitySpaceRootImpl } doReturn mock()
+ on { headActivityPose } doReturn mock()
+ on { perceptionSpaceActivityPose } doReturn mock()
+ on { mainPanelEntity } doReturn mock()
+ on { createEntity(any(), any(), any()) } doReturn mockContentlessEntity
+ }
+
+ mockRtSoundPoolExtensions = mock()
+ whenever(mockRuntime.soundPoolExtensionsWrapper).thenReturn(mockRtSoundPoolExtensions)
+ session = Session.create(activity, mockRuntime)
+ }
+
+ @Test
+ fun playWithPointSource_callsRuntimeSoundPoolPlayPointSource() {
+ val expectedStreamId = 1234
+
+ val soundPool = SoundPool.Builder().build()
+ val entity = session.createEntity("test")
+ val pointSourceAttributes = PointSourceAttributes(entity)
+ whenever(
+ mockRtSoundPoolExtensions.play(
+ eq(soundPool),
+ any(),
+ any<JxrPlatformAdapter.PointSourceAttributes>(),
+ any(),
+ any(),
+ any(),
+ any(),
+ )
+ )
+ .thenReturn(expectedStreamId)
+
+ val actualStreamId =
+ SpatialSoundPool.play(
+ session,
+ soundPool,
+ TEST_SOUND_ID,
+ pointSourceAttributes,
+ TEST_VOLUME,
+ TEST_PRIORITY,
+ TEST_LOOP,
+ TEST_RATE,
+ )
+ verify(mockRtSoundPoolExtensions)
+ .play(
+ eq(soundPool),
+ eq(TEST_SOUND_ID),
+ argWhere<JxrPlatformAdapter.PointSourceAttributes> {
+ it.entity == mockContentlessEntity
+ },
+ eq(TEST_VOLUME),
+ eq(TEST_PRIORITY),
+ eq(TEST_LOOP),
+ eq(TEST_RATE),
+ )
+ assertThat(actualStreamId).isEqualTo(expectedStreamId)
+ }
+
+ @Test
+ fun playWithSoundField_callsRuntimeSoundPoolPlaySoundField() {
+ val soundPool = SoundPool.Builder().build()
+ val soundFieldAttributes = SoundFieldAttributes(AMBISONICS_ORDER_FIRST_ORDER)
+
+ // TODO(b/317112315): Update test when implementation is finished.
+ assertThat(
+ SpatialSoundPool.play(
+ session,
+ soundPool,
+ TEST_SOUND_ID,
+ soundFieldAttributes,
+ TEST_VOLUME,
+ TEST_PRIORITY,
+ TEST_LOOP,
+ TEST_RATE,
+ )
+ )
+ .isEqualTo(0)
+ }
+
+ @Test
+ fun getSourceType_returnsRuntimeSoundPoolGetSourceType() {
+ val expected = SpatializerConstants.SOURCE_TYPE_SOUND_FIELD
+ val soundPool = SoundPool.Builder().build()
+
+ whenever(mockRtSoundPoolExtensions.getSpatialSourceType(any(), any()))
+ .thenReturn(JxrPlatformAdapter.SpatializerConstants.SOURCE_TYPE_SOUND_FIELD)
+
+ assertThat(SpatialSoundPool.getSpatialSourceType(session, soundPool, TEST_STREAM_ID))
+ .isEqualTo(expected)
+ }
+
+ companion object {
+ const val TEST_SOUND_ID = 0
+ const val TEST_VOLUME = 1F
+ const val TEST_PRIORITY = 0
+ const val TEST_LOOP = 0
+ const val TEST_RATE = 1F
+ const val TEST_STREAM_ID = 10
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SpatialUserTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SpatialUserTest.kt
new file mode 100644
index 0000000..aeec37e
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SpatialUserTest.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class SpatialUserTest {
+
+ private val mockRuntime = mock<JxrPlatformAdapter>()
+ lateinit var spatialUser: SpatialUser
+
+ @Before
+ fun setUp() {
+ whenever(mockRuntime.headActivityPose).thenReturn(mock())
+ whenever(mockRuntime.getCameraViewActivityPose(anyInt())).thenReturn(mock())
+ spatialUser = SpatialUser.create(mockRuntime)
+ }
+
+ @Test
+ fun getHeadActivityPose_returnsNullIfNoRtActivityPose() {
+ whenever(mockRuntime.headActivityPose).thenReturn(null)
+ val head = spatialUser.head
+ assertThat(head).isNull()
+ }
+
+ @Test
+ fun getHeadActivityPose_returnsNullThenHeadWhenAvailable() {
+ whenever(mockRuntime.headActivityPose).thenReturn(null)
+ var head = spatialUser.head
+ assertThat(head).isNull()
+
+ whenever(mockRuntime.headActivityPose).thenReturn(mock())
+ head = spatialUser.head
+ assertThat(head).isNotNull()
+ }
+
+ @Test
+ fun getHeadActivityPose_returnsHeadActivityPose() {
+ val head = spatialUser.head
+ assertThat(head).isNotNull()
+ }
+
+ @Test
+ fun getHeadActivityPoseTwice_returnsSameHeadActivityPose() {
+ val head1 = spatialUser.head
+ val head2 = spatialUser.head
+
+ assertThat(head1).isEqualTo(head2)
+ }
+
+ @Test
+ fun getNullCameraViews_returnsNullCameraViews() {
+ whenever(mockRuntime.getCameraViewActivityPose(anyInt())).thenReturn(null)
+ val leftView = spatialUser.getCameraView(CameraView.CameraType.LEFT_EYE)
+ assertThat(leftView).isNull()
+
+ val rightView = spatialUser.getCameraView(CameraView.CameraType.RIGHT_EYE)
+ assertThat(rightView).isNull()
+ }
+
+ @Test
+ fun getCameraViews_returnsNullThenCameraViewsWhenAvailable() {
+ whenever(mockRuntime.getCameraViewActivityPose(anyInt())).thenReturn(null)
+ var leftView = spatialUser.getCameraView(CameraView.CameraType.LEFT_EYE)
+ assertThat(leftView).isNull()
+
+ var rightView = spatialUser.getCameraView(CameraView.CameraType.RIGHT_EYE)
+ assertThat(rightView).isNull()
+
+ whenever(mockRuntime.getCameraViewActivityPose(anyInt())).thenReturn(mock())
+ leftView = spatialUser.getCameraView(CameraView.CameraType.LEFT_EYE)
+ assertThat(leftView).isNotNull()
+
+ rightView = spatialUser.getCameraView(CameraView.CameraType.RIGHT_EYE)
+ assertThat(rightView).isNotNull()
+ }
+
+ @Test
+ fun getCameraViews_returnsCameraView() {
+ val leftView = spatialUser.getCameraView(CameraView.CameraType.LEFT_EYE)
+ assertThat(leftView).isNotNull()
+
+ val rightView = spatialUser.getCameraView(CameraView.CameraType.RIGHT_EYE)
+ assertThat(rightView).isNotNull()
+ }
+
+ @Test
+ fun getCameraViewsTwice_returnsSameCameraView() {
+ val leftView1 = spatialUser.getCameraView(CameraView.CameraType.LEFT_EYE)
+ val leftView2 = spatialUser.getCameraView(CameraView.CameraType.LEFT_EYE)
+
+ assertThat(leftView1).isEqualTo(leftView2)
+
+ val rightView1 = spatialUser.getCameraView(CameraView.CameraType.RIGHT_EYE)
+ val rightView2 = spatialUser.getCameraView(CameraView.CameraType.RIGHT_EYE)
+
+ assertThat(rightView1).isEqualTo(rightView2)
+ }
+
+ @Test
+ fun getCameraViews_returnsCameraViews() {
+ val cameraViews = spatialUser.getCameraViews()
+
+ val leftView = spatialUser.getCameraView(CameraView.CameraType.LEFT_EYE)
+ val rightView = spatialUser.getCameraView(CameraView.CameraType.RIGHT_EYE)
+
+ assertThat(cameraViews).containsExactly(leftView, rightView)
+ }
+
+ @Test
+ fun getCameraViews_returnsEmptyListIfNullCamera() {
+ val mockRuntimeNoCamera = mock<JxrPlatformAdapter>()
+ whenever(mockRuntimeNoCamera.headActivityPose).thenReturn(mock())
+ whenever(mockRuntimeNoCamera.getCameraViewActivityPose(anyInt())).thenReturn(null)
+ val spatialUserNoCamera = SpatialUser.create(mockRuntimeNoCamera)
+
+ val cameraViews = spatialUserNoCamera.getCameraViews()
+
+ assertThat(cameraViews).isEmpty()
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SpatializerConstantsTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SpatializerConstantsTest.kt
new file mode 100644
index 0000000..a75fc2b
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/SpatializerConstantsTest.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class SpatializerConstantsTest {
+
+ @Test
+ fun sourceTypeToJXRExtension_createsCorrectIntDefType() {
+ val rtBypass = JxrPlatformAdapter.SpatializerConstants.SOURCE_TYPE_BYPASS
+ assertThat(rtBypass.sourceTypeToJxr()).isEqualTo(SpatializerConstants.SOURCE_TYPE_BYPASS)
+
+ val rtPointSource = JxrPlatformAdapter.SpatializerConstants.SOURCE_TYPE_POINT_SOURCE
+ assertThat(rtPointSource.sourceTypeToJxr())
+ .isEqualTo(SpatializerConstants.SOURCE_TYPE_POINT_SOURCE)
+
+ val rtSoundField = JxrPlatformAdapter.SpatializerConstants.SOURCE_TYPE_SOUND_FIELD
+ assertThat(rtSoundField.sourceTypeToJxr())
+ .isEqualTo(SpatializerConstants.SOURCE_TYPE_SOUND_FIELD)
+ }
+
+ @Test
+ fun ambisonicsOrderToJXR_createCorrectIntDefType() {
+ val rtFirstOrder = JxrPlatformAdapter.SpatializerConstants.AMBISONICS_ORDER_FIRST_ORDER
+ assertThat(rtFirstOrder.ambisonicsOrderToJxr())
+ .isEqualTo(SpatializerConstants.AMBISONICS_ORDER_FIRST_ORDER)
+
+ val rtSecondOrder = JxrPlatformAdapter.SpatializerConstants.AMBISONICS_ORDER_SECOND_ORDER
+ assertThat(rtSecondOrder.ambisonicsOrderToJxr())
+ .isEqualTo(SpatializerConstants.AMBISONICS_ORDER_SECOND_ORDER)
+
+ val rtThirdOrder = JxrPlatformAdapter.SpatializerConstants.AMBISONICS_ORDER_THIRD_ORDER
+ assertThat(rtThirdOrder.ambisonicsOrderToJxr())
+ .isEqualTo(SpatializerConstants.AMBISONICS_ORDER_THIRD_ORDER)
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/UtilsTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/UtilsTest.kt
new file mode 100644
index 0000000..9e03ea2
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/UtilsTest.kt
@@ -0,0 +1,563 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore
+
+import androidx.xr.runtime.math.Matrix4
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Vector3
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions as RuntimeDimensions
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity as RuntimeEntity
+import androidx.xr.scenecore.JxrPlatformAdapter.InputEvent as RuntimeInputEvent
+import androidx.xr.scenecore.JxrPlatformAdapter.InputEvent.HitInfo as RuntimeHitInfo
+import androidx.xr.scenecore.JxrPlatformAdapter.MoveEvent as RuntimeMoveEvent
+import androidx.xr.scenecore.JxrPlatformAdapter.PixelDimensions as RuntimePixelDimensions
+import androidx.xr.scenecore.JxrPlatformAdapter.ResizeEvent as RuntimeResizeEvent
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatialCapabilities as RuntimeSpatialCapabilities
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class UtilsTest {
+ @Test
+ fun verifyDefaultPoseIsIdentity() {
+ val rtPose = Pose.Identity
+
+ assertThat(rtPose.translation.x).isZero()
+ assertThat(rtPose.translation.y).isZero()
+ assertThat(rtPose.translation.z).isZero()
+ assertThat(rtPose.rotation.x).isZero()
+ assertThat(rtPose.rotation.y).isZero()
+ assertThat(rtPose.rotation.z).isZero()
+ assertThat(rtPose.rotation.w).isEqualTo(1f)
+ }
+
+ @Test
+ fun verifyPoseToRtPoseConversion() {
+ val rtPose = Pose(Vector3(1f, 2f, 3f), Quaternion(1f, 2f, 3f, 4f).toNormalized())
+
+ assertThat(rtPose.translation.x).isEqualTo(1f)
+ assertThat(rtPose.translation.y).isEqualTo(2f)
+ assertThat(rtPose.translation.z).isEqualTo(3f)
+
+ // The quaternion is always normalized, so the retrieved values differ from the original
+ // ones.
+ assertThat(rtPose.rotation.x).isWithin(1e-5f).of(0.18257418f)
+ assertThat(rtPose.rotation.y).isWithin(1e-5f).of(0.36514837f)
+ assertThat(rtPose.rotation.z).isWithin(1e-5f).of(0.5477225f)
+ assertThat(rtPose.rotation.w).isWithin(1e-5f).of(0.73029673f)
+ }
+
+ @Test
+ fun verifyRtPoseToPoseConversion() {
+ val pose = Pose(Vector3(1f, 2f, 3f), Quaternion(1f, 2f, 3f, 4f).toNormalized())
+
+ assertThat(pose.translation.x).isEqualTo(1f)
+ assertThat(pose.translation.y).isEqualTo(2f)
+ assertThat(pose.translation.z).isEqualTo(3f)
+
+ // The quaternion is always normalized, so the retrieved values differ from the original
+ // ones.
+ assertThat(pose.rotation.x).isWithin(1e-5f).of(0.18257418f)
+ assertThat(pose.rotation.y).isWithin(1e-5f).of(0.36514837f)
+ assertThat(pose.rotation.z).isWithin(1e-5f).of(0.5477225f)
+ assertThat(pose.rotation.w).isWithin(1e-5f).of(0.73029673f)
+ }
+
+ @Test
+ fun verifyRtVector3toVector3() {
+ val vector3: Vector3 = Vector3(1f, 2f, 3f)
+ assertThat(vector3.x).isEqualTo(1f)
+ assertThat(vector3.y).isEqualTo(2f)
+ assertThat(vector3.z).isEqualTo(3f)
+ }
+
+ @Test
+ fun verifyVector3toRtVector3() {
+ val rtVector3: Vector3 = Vector3(1f, 2f, 3f)
+ assertThat(rtVector3.x).isEqualTo(1f)
+ assertThat(rtVector3.y).isEqualTo(2f)
+ assertThat(rtVector3.z).isEqualTo(3f)
+ }
+
+ @Test
+ fun verifyRtMoveEventToMoveEvent() {
+ val vector0 = Vector3(0f, 0f, 0f)
+ val vector1 = Vector3(1f, 1f, 1f)
+ val vector2 = Vector3(2f, 2f, 2f)
+
+ val initialInputRay = JxrPlatformAdapter.Ray(vector0, vector1)
+ val currentInputRay = JxrPlatformAdapter.Ray(vector1, vector2)
+ val entityManager = EntityManager()
+ val activitySpace = mock<JxrPlatformAdapter.ActivitySpace>()
+ entityManager.setEntityForRtEntity(activitySpace, mock<Entity>())
+ val moveEvent =
+ RuntimeMoveEvent(
+ RuntimeMoveEvent.MOVE_STATE_ONGOING,
+ initialInputRay,
+ currentInputRay,
+ Pose(),
+ Pose(vector1, Quaternion.Identity),
+ vector1,
+ vector1,
+ activitySpace,
+ null,
+ null,
+ )
+ .toMoveEvent(entityManager)
+
+ assertThat(moveEvent.moveState).isEqualTo(MoveEvent.MOVE_STATE_ONGOING)
+
+ assertThat(moveEvent.initialInputRay.origin.x).isEqualTo(0f)
+ assertThat(moveEvent.initialInputRay.origin.y).isEqualTo(0f)
+ assertThat(moveEvent.initialInputRay.origin.z).isEqualTo(0f)
+ assertThat(moveEvent.initialInputRay.direction.x).isEqualTo(1f)
+ assertThat(moveEvent.initialInputRay.direction.y).isEqualTo(1f)
+ assertThat(moveEvent.initialInputRay.direction.z).isEqualTo(1f)
+
+ assertThat(moveEvent.currentInputRay.origin.x).isEqualTo(1f)
+ assertThat(moveEvent.currentInputRay.origin.y).isEqualTo(1f)
+ assertThat(moveEvent.currentInputRay.origin.z).isEqualTo(1f)
+ assertThat(moveEvent.currentInputRay.direction.x).isEqualTo(2f)
+ assertThat(moveEvent.currentInputRay.direction.y).isEqualTo(2f)
+ assertThat(moveEvent.currentInputRay.direction.z).isEqualTo(2f)
+
+ assertThat(moveEvent.previousPose.translation.x).isEqualTo(0f)
+ assertThat(moveEvent.previousPose.translation.y).isEqualTo(0f)
+ assertThat(moveEvent.previousPose.translation.z).isEqualTo(0f)
+ assertThat(moveEvent.previousPose.rotation.x).isEqualTo(0f)
+ assertThat(moveEvent.previousPose.rotation.y).isEqualTo(0f)
+ assertThat(moveEvent.previousPose.rotation.z).isEqualTo(0f)
+ assertThat(moveEvent.previousPose.rotation.w).isEqualTo(1f)
+
+ assertThat(moveEvent.currentPose.translation.x).isEqualTo(1f)
+ assertThat(moveEvent.currentPose.translation.y).isEqualTo(1f)
+ assertThat(moveEvent.currentPose.translation.z).isEqualTo(1f)
+ assertThat(moveEvent.currentPose.rotation.x).isEqualTo(0f)
+ assertThat(moveEvent.currentPose.rotation.y).isEqualTo(0f)
+ assertThat(moveEvent.currentPose.rotation.z).isEqualTo(0f)
+ assertThat(moveEvent.currentPose.rotation.w).isEqualTo(1f)
+
+ assertThat(moveEvent.previousScale).isEqualTo(1f)
+
+ assertThat(moveEvent.currentScale).isEqualTo(1f)
+ }
+
+ @Test
+ fun verifyRtInputEventToInputEventConversion() {
+ val entityManager = EntityManager()
+ val activitySpace = mock<JxrPlatformAdapter.ActivitySpace>()
+ entityManager.setEntityForRtEntity(activitySpace, mock<Entity>())
+ val inputEvent =
+ RuntimeInputEvent(
+ RuntimeInputEvent.SOURCE_HANDS,
+ RuntimeInputEvent.POINTER_TYPE_LEFT,
+ 123456789,
+ Vector3(1f, 2f, 3f),
+ Vector3(4f, 5f, 6f),
+ RuntimeInputEvent.ACTION_DOWN,
+ null,
+ null,
+ )
+ .toInputEvent(entityManager)
+ assertThat(inputEvent.source).isEqualTo(InputEvent.SOURCE_HANDS)
+ assertThat(inputEvent.pointerType).isEqualTo(InputEvent.POINTER_TYPE_LEFT)
+ assertThat(inputEvent.timestamp).isEqualTo(123456789)
+ assertThat(inputEvent.origin.x).isEqualTo(1f)
+ assertThat(inputEvent.origin.y).isEqualTo(2f)
+ assertThat(inputEvent.origin.z).isEqualTo(3f)
+ assertThat(inputEvent.direction.x).isEqualTo(4f)
+ assertThat(inputEvent.direction.y).isEqualTo(5f)
+ assertThat(inputEvent.direction.z).isEqualTo(6f)
+ assertThat(inputEvent.action).isEqualTo(InputEvent.ACTION_DOWN)
+ }
+
+ @Test
+ fun verifyRtHitInfoToHitInfoConversion() {
+ val entityManager = EntityManager()
+ val rtMockEntity = mock<RuntimeEntity>()
+ val mockEntity = mock<Entity>()
+ entityManager.setEntityForRtEntity(rtMockEntity, mockEntity)
+ val hitPosition = Vector3(1f, 2f, 3f)
+ val transform = Matrix4.Identity
+ val hitInfo = RuntimeHitInfo(rtMockEntity, hitPosition, transform).toHitInfo(entityManager)
+
+ assertThat(hitInfo).isNotNull()
+ assertThat(hitInfo!!.inputEntity).isEqualTo(mockEntity)
+ assertThat(hitInfo.hitPosition).isEqualTo(hitPosition)
+ assertThat(hitInfo.transform).isEqualTo(transform)
+ }
+
+ @Test
+ fun verifyRtHitInfoToHitInfoConversionWhenEntityNotFound() {
+ val entityManager = EntityManager()
+ val rtMockEntity = mock<RuntimeEntity>()
+ val hitPosition = Vector3(1f, 2f, 3f)
+ val transform = Matrix4.Identity
+ val hitInfo = RuntimeHitInfo(rtMockEntity, hitPosition, transform).toHitInfo(entityManager)
+
+ // EntityManager does not have the entity for the given RuntimeEntity, so the hit info is
+ // null.
+ assertThat(hitInfo).isNull()
+ }
+
+ @Test
+ fun verifyRtDimensionsToDimensions() {
+ val dimensions: Dimensions = RuntimeDimensions(2f, 4f, 6f).toDimensions()
+ assertThat(dimensions.width).isEqualTo(2f)
+ assertThat(dimensions.height).isEqualTo(4f)
+ assertThat(dimensions.depth).isEqualTo(6f)
+ }
+
+ @Test
+ fun verifyRtPixelDimensionsToPixelDimensions() {
+ val pixelDimensions: PixelDimensions = RuntimePixelDimensions(14, 15).toPixelDimensions()
+ assertThat(pixelDimensions.width).isEqualTo(14)
+ assertThat(pixelDimensions.height).isEqualTo(15)
+ }
+
+ @Test
+ fun verifyPixelDimensionsToRtPixelDimensions() {
+ val pixelDimensions: PixelDimensions = RuntimePixelDimensions(17, 18).toPixelDimensions()
+ assertThat(pixelDimensions.width).isEqualTo(17)
+ assertThat(pixelDimensions.height).isEqualTo(18)
+ }
+
+ @Test
+ fun verifyRuntimeResizeEventToResizeEvent() {
+ val resizeEvent: ResizeEvent =
+ RuntimeResizeEvent(RuntimeResizeEvent.RESIZE_STATE_START, RuntimeDimensions(1f, 3f, 5f))
+ .toResizeEvent()
+ assertThat(resizeEvent.resizeState).isEqualTo(ResizeEvent.RESIZE_STATE_START)
+ assertThat(resizeEvent.newSize.width).isEqualTo(1f)
+ assertThat(resizeEvent.newSize.height).isEqualTo(3f)
+ assertThat(resizeEvent.newSize.depth).isEqualTo(5f)
+ }
+
+ @Test
+ fun runtimeSpatialCapabilitiesToSpatialCapabilities_noCapabilities() {
+ val caps = RuntimeSpatialCapabilities(0).toSpatialCapabilities()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_UI)).isFalse()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT)).isFalse()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL))
+ .isFalse()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT))
+ .isFalse()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO))
+ .isFalse()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY))
+ .isFalse()
+ }
+
+ @Test
+ fun runtimeSpatialCapabilitiesToSpatialCapabilities_singleCapability() {
+ var caps =
+ RuntimeSpatialCapabilities(RuntimeSpatialCapabilities.SPATIAL_CAPABILITY_UI)
+ .toSpatialCapabilities()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_UI)).isTrue()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT)).isFalse()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL))
+ .isFalse()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT))
+ .isFalse()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO))
+ .isFalse()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY))
+ .isFalse()
+
+ caps =
+ RuntimeSpatialCapabilities(RuntimeSpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO)
+ .toSpatialCapabilities()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_UI)).isFalse()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT)).isFalse()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL))
+ .isFalse()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT))
+ .isFalse()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO))
+ .isTrue()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY))
+ .isFalse()
+ }
+
+ @Test
+ fun runtimeSpatialCapabilitiesToSpatialCapabilities_allCapabilities() {
+ val caps =
+ RuntimeSpatialCapabilities(
+ RuntimeSpatialCapabilities.SPATIAL_CAPABILITY_UI or
+ RuntimeSpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT or
+ RuntimeSpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL or
+ RuntimeSpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT or
+ RuntimeSpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO or
+ RuntimeSpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY
+ )
+ .toSpatialCapabilities()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_UI)).isTrue()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT)).isTrue()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL))
+ .isTrue()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT))
+ .isTrue()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO))
+ .isTrue()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY))
+ .isTrue()
+ }
+
+ @Test
+ fun runtimeSpatialCapabilitiesToSpatialCapabilities_mixedCapabilities() {
+ var caps =
+ RuntimeSpatialCapabilities(
+ RuntimeSpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT or
+ RuntimeSpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO
+ )
+ .toSpatialCapabilities()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_UI)).isFalse()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT)).isTrue()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL))
+ .isFalse()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT))
+ .isFalse()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO))
+ .isTrue()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY))
+ .isFalse()
+
+ caps =
+ RuntimeSpatialCapabilities(
+ RuntimeSpatialCapabilities.SPATIAL_CAPABILITY_UI or
+ RuntimeSpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL or
+ RuntimeSpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT or
+ RuntimeSpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY
+ )
+ .toSpatialCapabilities()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_UI)).isTrue()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT)).isFalse()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL))
+ .isTrue()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT))
+ .isTrue()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO))
+ .isFalse()
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY))
+ .isTrue()
+ }
+
+ @Test
+ fun intToMoveState_convertsCorrectly() {
+ assertThat(
+ listOf(
+ JxrPlatformAdapter.MoveEvent.MOVE_STATE_START,
+ JxrPlatformAdapter.MoveEvent.MOVE_STATE_ONGOING,
+ JxrPlatformAdapter.MoveEvent.MOVE_STATE_END,
+ )
+ .map { it.toMoveState() }
+ )
+ .containsExactly(
+ MoveEvent.MOVE_STATE_START,
+ MoveEvent.MOVE_STATE_ONGOING,
+ MoveEvent.MOVE_STATE_END,
+ )
+ .inOrder()
+ }
+
+ @Test
+ fun intToMoveState_invalidValue_throwsError() {
+ assertFailsWith<IllegalStateException> { 100.toMoveState() }
+ }
+
+ @Test
+ fun intToResizeState_convertsCorrectly() {
+ assertThat(
+ listOf(
+ JxrPlatformAdapter.ResizeEvent.RESIZE_STATE_UNKNOWN,
+ JxrPlatformAdapter.ResizeEvent.RESIZE_STATE_START,
+ JxrPlatformAdapter.ResizeEvent.RESIZE_STATE_ONGOING,
+ JxrPlatformAdapter.ResizeEvent.RESIZE_STATE_END,
+ )
+ .map { it.toResizeState() }
+ )
+ .containsExactly(
+ ResizeEvent.RESIZE_STATE_UNKNOWN,
+ ResizeEvent.RESIZE_STATE_START,
+ ResizeEvent.RESIZE_STATE_ONGOING,
+ ResizeEvent.RESIZE_STATE_END,
+ )
+ .inOrder()
+ }
+
+ @Test
+ fun intToResizeState_invalidValue_throwsError() {
+ assertFailsWith<IllegalStateException> { 100.toResizeState() }
+ }
+
+ @Test
+ fun intToInputEventSource_convertsCorrectly() {
+ assertThat(
+ listOf(
+ JxrPlatformAdapter.InputEvent.SOURCE_UNKNOWN,
+ JxrPlatformAdapter.InputEvent.SOURCE_HEAD,
+ JxrPlatformAdapter.InputEvent.SOURCE_CONTROLLER,
+ JxrPlatformAdapter.InputEvent.SOURCE_HANDS,
+ JxrPlatformAdapter.InputEvent.SOURCE_MOUSE,
+ JxrPlatformAdapter.InputEvent.SOURCE_GAZE_AND_GESTURE,
+ )
+ .map { it.toInputEventSource() }
+ )
+ .containsExactly(
+ InputEvent.SOURCE_UNKNOWN,
+ InputEvent.SOURCE_HEAD,
+ InputEvent.SOURCE_CONTROLLER,
+ InputEvent.SOURCE_HANDS,
+ InputEvent.SOURCE_MOUSE,
+ InputEvent.SOURCE_GAZE_AND_GESTURE,
+ )
+ .inOrder()
+ }
+
+ @Test
+ fun intToInputEventSource_invalidValue_throwsError() {
+ assertFailsWith<IllegalStateException> { 100.toInputEventSource() }
+ }
+
+ @Test
+ fun intToInputEventPointerType_convertsCorrectly() {
+ assertThat(
+ listOf(
+ JxrPlatformAdapter.InputEvent.POINTER_TYPE_DEFAULT,
+ JxrPlatformAdapter.InputEvent.POINTER_TYPE_LEFT,
+ JxrPlatformAdapter.InputEvent.POINTER_TYPE_RIGHT,
+ )
+ .map { it.toInputEventPointerType() }
+ )
+ .containsExactly(
+ InputEvent.POINTER_TYPE_DEFAULT,
+ InputEvent.POINTER_TYPE_LEFT,
+ InputEvent.POINTER_TYPE_RIGHT,
+ )
+ .inOrder()
+ }
+
+ @Test
+ fun intToInputEventPointerType_invalidValue_throwsError() {
+ assertFailsWith<IllegalStateException> { 100.toInputEventPointerType() }
+ }
+
+ @Test
+ fun intToSpatialCapability_convertsCorrectly() {
+ assertThat(
+ listOf(
+ JxrPlatformAdapter.SpatialCapabilities.SPATIAL_CAPABILITY_UI,
+ JxrPlatformAdapter.SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT,
+ JxrPlatformAdapter.SpatialCapabilities
+ .SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL,
+ JxrPlatformAdapter.SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT,
+ JxrPlatformAdapter.SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO,
+ JxrPlatformAdapter.SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY,
+ )
+ .map { it.toSpatialCapability() }
+ )
+ .containsExactly(
+ SpatialCapabilities.SPATIAL_CAPABILITY_UI,
+ SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT,
+ SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL,
+ SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT,
+ SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO,
+ SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY,
+ )
+ .inOrder()
+ }
+
+ @Test
+ fun intToInputEventAction_convertsCorrectly() {
+ assertThat(
+ listOf(
+ JxrPlatformAdapter.InputEvent.ACTION_DOWN,
+ JxrPlatformAdapter.InputEvent.ACTION_UP,
+ JxrPlatformAdapter.InputEvent.ACTION_MOVE,
+ JxrPlatformAdapter.InputEvent.ACTION_CANCEL,
+ JxrPlatformAdapter.InputEvent.ACTION_HOVER_MOVE,
+ JxrPlatformAdapter.InputEvent.ACTION_HOVER_ENTER,
+ JxrPlatformAdapter.InputEvent.ACTION_HOVER_EXIT,
+ )
+ .map { it.toInputEventAction() }
+ )
+ .containsExactly(
+ InputEvent.ACTION_DOWN,
+ InputEvent.ACTION_UP,
+ InputEvent.ACTION_MOVE,
+ InputEvent.ACTION_CANCEL,
+ InputEvent.ACTION_HOVER_MOVE,
+ InputEvent.ACTION_HOVER_ENTER,
+ InputEvent.ACTION_HOVER_EXIT,
+ )
+ .inOrder()
+ }
+
+ @Test
+ fun intToInputEventAction_invalidValue_throwsError() {
+ assertFailsWith<IllegalStateException> { 100.toInputEventAction() }
+ }
+
+ @Test
+ fun anchorPlacementToRuntimeAnchorPlacement_setsCorrectly() {
+ val mockRuntime = mock<JxrPlatformAdapter>()
+ val mockAnchorPlacement1 = mock<JxrPlatformAdapter.AnchorPlacement>()
+ val mockAnchorPlacement2 = mock<JxrPlatformAdapter.AnchorPlacement>()
+ whenever(
+ mockRuntime.createAnchorPlacementForPlanes(
+ setOf(JxrPlatformAdapter.PlaneType.HORIZONTAL),
+ setOf(JxrPlatformAdapter.PlaneSemantic.ANY),
+ )
+ )
+ .thenReturn(mockAnchorPlacement1)
+ whenever(
+ mockRuntime.createAnchorPlacementForPlanes(
+ setOf(JxrPlatformAdapter.PlaneType.ANY),
+ setOf(
+ JxrPlatformAdapter.PlaneSemantic.WALL,
+ JxrPlatformAdapter.PlaneSemantic.FLOOR
+ ),
+ )
+ )
+ .thenReturn(mockAnchorPlacement2)
+
+ val anchorPlacement1 =
+ AnchorPlacement.createForPlanes(planeTypeFilter = setOf(PlaneType.HORIZONTAL))
+ val anchorPlacement2 =
+ AnchorPlacement.createForPlanes(
+ planeSemanticFilter = setOf(PlaneSemantic.WALL, PlaneSemantic.FLOOR)
+ )
+
+ val rtPlacementSet =
+ setOf(anchorPlacement1, anchorPlacement2).toRtAnchorPlacement(mockRuntime)
+
+ assertThat(rtPlacementSet.size).isEqualTo(2)
+ assertThat(rtPlacementSet).containsExactly(mockAnchorPlacement1, mockAnchorPlacement2)
+ }
+
+ @Test
+ fun anchorPlacementToRuntimeAnchotPlacementEmptySet_returnsEmptySet() {
+ val mockRuntime = mock<JxrPlatformAdapter>()
+
+ val rtPlacementSet = emptySet<AnchorPlacement>().toRtAnchorPlacement(mockRuntime)
+
+ assertThat(rtPlacementSet).isEmpty()
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/ActivityPanelEntityImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/ActivityPanelEntityImplTest.java
new file mode 100644
index 0000000..d7c711b
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/ActivityPanelEntityImplTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Rect;
+
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.scenecore.JxrPlatformAdapter;
+import androidx.xr.scenecore.JxrPlatformAdapter.ActivityPanelEntity;
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.PixelDimensions;
+import androidx.xr.scenecore.impl.perception.PerceptionLibrary;
+import androidx.xr.scenecore.impl.perception.Session;
+import androidx.xr.scenecore.testing.FakeImpressApi;
+import androidx.xr.scenecore.testing.FakeScheduledExecutorService;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeActivityPanel;
+
+import com.google.androidxr.splitengine.SplitEngineSubspaceManager;
+import com.google.ar.imp.view.splitengine.ImpSplitEngineRenderer;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.android.controller.ActivityController;
+
+@RunWith(RobolectricTestRunner.class)
+public class ActivityPanelEntityImplTest {
+ private final FakeXrExtensions fakeExtensions = new FakeXrExtensions();
+ private final FakeImpressApi fakeImpressApi = new FakeImpressApi();
+ private final ActivityController<Activity> activityController =
+ Robolectric.buildActivity(Activity.class);
+ private final Activity hostActivity = activityController.create().start().get();
+ private final PixelDimensions windowBoundsPx = new PixelDimensions(640, 480);
+ private final FakeScheduledExecutorService fakeExecutor = new FakeScheduledExecutorService();
+ private final PerceptionLibrary perceptionLibrary = Mockito.mock(PerceptionLibrary.class);
+ private final SplitEngineSubspaceManager splitEngineSubspaceManager =
+ Mockito.mock(SplitEngineSubspaceManager.class);
+ private final ImpSplitEngineRenderer splitEngineRenderer =
+ Mockito.mock(ImpSplitEngineRenderer.class);
+
+ private ActivityPanelEntity createActivityPanelEntity() {
+ when(perceptionLibrary.initSession(eq(hostActivity), anyInt(), eq(fakeExecutor)))
+ .thenReturn(immediateFuture(Mockito.mock(Session.class)));
+
+ JxrPlatformAdapter fakeRuntime =
+ JxrPlatformAdapterAxr.create(
+ hostActivity,
+ fakeExecutor,
+ fakeExtensions,
+ fakeImpressApi,
+ new EntityManager(),
+ perceptionLibrary,
+ splitEngineSubspaceManager,
+ splitEngineRenderer,
+ /* useSplitEngine= */ false);
+ Pose pose = new Pose();
+
+ return fakeRuntime.createActivityPanelEntity(
+ pose, windowBoundsPx, "test", hostActivity, fakeRuntime.getActivitySpaceRootImpl());
+ }
+
+ @Test
+ public void createActivityPanelEntity_returnsActivityPanelEntity() {
+ ActivityPanelEntity activityPanelEntity = createActivityPanelEntity();
+
+ assertThat(activityPanelEntity).isNotNull();
+ }
+
+ @Test
+ public void activityPanelEntityLaunchActivity_callsActivityPanel() {
+ ActivityPanelEntity activityPanelEntity = createActivityPanelEntity();
+ Intent launchIntent = activityController.getIntent();
+ activityPanelEntity.launchActivity(launchIntent, null);
+
+ FakeActivityPanel fakePanel = fakeExtensions.getActivityPanelForHost(hostActivity);
+
+ assertThat(fakePanel.getLaunchIntent()).isEqualTo(launchIntent);
+ assertThat(fakePanel.getBundle()).isNull();
+ assertThat(fakePanel.getBounds())
+ .isEqualTo(new Rect(0, 0, windowBoundsPx.width, windowBoundsPx.height));
+ }
+
+ @Test
+ public void activityPanelEntityMoveActivity_callActivityPanel() {
+ ActivityPanelEntity activityPanelEntity = createActivityPanelEntity();
+ activityPanelEntity.moveActivity(hostActivity);
+
+ FakeActivityPanel fakePanel = fakeExtensions.getActivityPanelForHost(hostActivity);
+
+ assertThat(fakePanel.getActivity()).isEqualTo(hostActivity);
+
+ assertThat(fakePanel.getBounds())
+ .isEqualTo(new Rect(0, 0, windowBoundsPx.width, windowBoundsPx.height));
+ }
+
+ @Test
+ public void activityPanelEntitySetSize_callsSetPixelDimensions() {
+ ActivityPanelEntity activityPanelEntity = createActivityPanelEntity();
+ Dimensions dimensions = new Dimensions(400f, 300f, 0f);
+ activityPanelEntity.setSize(dimensions);
+
+ FakeActivityPanel fakePanel = fakeExtensions.getActivityPanelForHost(hostActivity);
+
+ assertThat(fakePanel.getBounds())
+ .isEqualTo(new Rect(0, 0, (int) dimensions.width, (int) dimensions.height));
+
+ // SetSize redirects to setPixelDimensions, so we check the same thing here.
+ PixelDimensions viewDimensions = activityPanelEntity.getPixelDimensions();
+ assertThat(viewDimensions.width).isEqualTo((int) dimensions.width);
+ assertThat(viewDimensions.height).isEqualTo((int) dimensions.height);
+ }
+
+ @Test
+ public void activityPanelEntitySetPixelDimensions_callActivityPanel() {
+ ActivityPanelEntity activityPanelEntity = createActivityPanelEntity();
+ PixelDimensions dimensions = new PixelDimensions(400, 300);
+ activityPanelEntity.setPixelDimensions(dimensions);
+
+ FakeActivityPanel fakePanel = fakeExtensions.getActivityPanelForHost(hostActivity);
+
+ assertThat(fakePanel.getBounds())
+ .isEqualTo(new Rect(0, 0, dimensions.width, dimensions.height));
+
+ PixelDimensions viewDimensions = activityPanelEntity.getPixelDimensions();
+ assertThat(viewDimensions.width).isEqualTo(dimensions.width);
+ assertThat(viewDimensions.height).isEqualTo(dimensions.height);
+ }
+
+ @Test
+ public void activityPanelEntityDispose_callsActivityPanelDelete() {
+ ActivityPanelEntity activityPanelEntity = createActivityPanelEntity();
+ activityPanelEntity.dispose();
+
+ FakeActivityPanel fakePanel = fakeExtensions.getActivityPanelForHost(hostActivity);
+
+ assertThat(fakePanel.isDeleted()).isTrue();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/ActivitySpaceImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/ActivitySpaceImplTest.java
new file mode 100644
index 0000000..5238b8fb
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/ActivitySpaceImplTest.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static androidx.xr.runtime.testing.math.MathAssertions.assertPose;
+import static androidx.xr.runtime.testing.math.MathAssertions.assertVector3;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+
+import androidx.xr.extensions.space.Bounds;
+import androidx.xr.runtime.math.Matrix4;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter;
+import androidx.xr.scenecore.JxrPlatformAdapter.ActivitySpace;
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.impl.perception.PerceptionLibrary;
+import androidx.xr.scenecore.impl.perception.Session;
+import androidx.xr.scenecore.testing.FakeImpressApi;
+import androidx.xr.scenecore.testing.FakeScheduledExecutorService;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeSpatialState;
+
+import com.google.androidxr.splitengine.SplitEngineSubspaceManager;
+import com.google.ar.imp.view.splitengine.ImpSplitEngineRenderer;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.android.controller.ActivityController;
+
+@RunWith(RobolectricTestRunner.class)
+public final class ActivitySpaceImplTest extends SystemSpaceEntityImplTest {
+ // TODO(b/329902726): Move this boilerplate for creating a TestJxrPlatformAdapter into a test
+ // util
+ private final ActivityController<Activity> activityController =
+ Robolectric.buildActivity(Activity.class);
+ private final Activity activity = activityController.create().start().get();
+ private final FakeScheduledExecutorService fakeExecutor = new FakeScheduledExecutorService();
+ private final PerceptionLibrary perceptionLibrary = Mockito.mock(PerceptionLibrary.class);
+ private final SplitEngineSubspaceManager splitEngineSubspaceManager =
+ Mockito.mock(SplitEngineSubspaceManager.class);
+ private final ImpSplitEngineRenderer splitEngineRenderer =
+ Mockito.mock(ImpSplitEngineRenderer.class);
+
+ private FakeXrExtensions fakeExtensions;
+ private FakeImpressApi fakeImpressApi;
+ private JxrPlatformAdapter testRuntime;
+ private ActivitySpace activitySpace;
+
+ @Before
+ public void setUp() {
+ fakeExtensions = new FakeXrExtensions();
+ fakeImpressApi = new FakeImpressApi();
+ when(perceptionLibrary.initSession(eq(activity), anyInt(), eq(fakeExecutor)))
+ .thenReturn(immediateFuture(Mockito.mock(Session.class)));
+
+ testRuntime =
+ JxrPlatformAdapterAxr.create(
+ activity,
+ fakeExecutor,
+ fakeExtensions,
+ fakeImpressApi,
+ new EntityManager(),
+ perceptionLibrary,
+ splitEngineSubspaceManager,
+ splitEngineRenderer,
+ /* useSplitEngine= */ false);
+
+ activitySpace = testRuntime.getActivitySpace();
+
+ // This is slightly hacky. We're grabbing the singleton instance of the ActivitySpaceImpl
+ // that
+ // was created by the RuntimeImpl. Ideally we'd have an interface to inject the
+ // ActivitySpace
+ // for testing. For now this is fine since there isn't an interface difference (yet).
+ assertThat(activitySpace).isInstanceOf(ActivitySpaceImpl.class);
+ assertThat(activitySpace).isNotNull();
+ }
+
+ @Override
+ protected SystemSpaceEntityImpl getSystemSpaceEntityImpl() {
+ return (SystemSpaceEntityImpl) activitySpace;
+ }
+
+ @Override
+ protected FakeScheduledExecutorService getDefaultFakeExecutor() {
+ return fakeExecutor;
+ }
+
+ @Override
+ protected AndroidXrEntity createChildAndroidXrEntity() {
+ return (AndroidXrEntity) testRuntime.createEntity(new Pose(), "child", activitySpace);
+ }
+
+ @Override
+ protected ActivitySpaceImpl getActivitySpaceEntity() {
+ return (ActivitySpaceImpl) activitySpace;
+ }
+
+ @Test
+ public void getBounds_returnsBounds() {
+ assertThat(activitySpace.getBounds().width).isPositiveInfinity();
+ assertThat(activitySpace.getBounds().height).isPositiveInfinity();
+ assertThat(activitySpace.getBounds().depth).isPositiveInfinity();
+
+ FakeSpatialState spatialState = new FakeSpatialState();
+ spatialState.setBounds(new Bounds(100.0f, 200.0f, 300.0f));
+ fakeExtensions.sendSpatialState(spatialState);
+
+ assertThat(activitySpace.getBounds().width).isEqualTo(100f);
+ assertThat(activitySpace.getBounds().height).isEqualTo(200f);
+ assertThat(activitySpace.getBounds().depth).isEqualTo(300f);
+ }
+
+ @Test
+ public void addBoundsChangedListener_happyPath() {
+ JxrPlatformAdapter.ActivitySpace.OnBoundsChangedListener listener =
+ Mockito.mock(JxrPlatformAdapter.ActivitySpace.OnBoundsChangedListener.class);
+
+ FakeSpatialState spatialState = new FakeSpatialState();
+ spatialState.setBounds(new Bounds(100.0f, 200.0f, 300.0f));
+ activitySpace.addOnBoundsChangedListener(listener);
+ fakeExtensions.sendSpatialState(spatialState);
+
+ verify(listener).onBoundsChanged(Mockito.refEq(new Dimensions(100.0f, 200.0f, 300.0f)));
+ }
+
+ @Test
+ public void removeBoundsChangedListener_happyPath() {
+ JxrPlatformAdapter.ActivitySpace.OnBoundsChangedListener listener =
+ Mockito.mock(JxrPlatformAdapter.ActivitySpace.OnBoundsChangedListener.class);
+
+ activitySpace.addOnBoundsChangedListener(listener);
+ activitySpace.removeOnBoundsChangedListener(listener);
+ FakeSpatialState spatialState = new FakeSpatialState();
+ spatialState.setBounds(new Bounds(100.0f, 200.0f, 300.0f));
+ fakeExtensions.sendSpatialState(spatialState);
+
+ verify(listener, Mockito.never()).onBoundsChanged(Mockito.any());
+ }
+
+ @Test
+ public void getPoseInActivitySpace_returnsIdentity() {
+ ActivitySpaceImpl activitySpaceImpl = (ActivitySpaceImpl) activitySpace;
+
+ assertPose(activitySpaceImpl.getPoseInActivitySpace(), new Pose());
+ }
+
+ @Test
+ public void getActivitySpaceScale_returnsUnitScale() {
+ ActivitySpaceImpl activitySpaceImpl = (ActivitySpaceImpl) activitySpace;
+ activitySpaceImpl.setOpenXrReferenceSpacePose(Matrix4.fromScale(5f));
+ assertVector3(activitySpaceImpl.getActivitySpaceScale(), new Vector3(1f, 1f, 1f));
+ }
+
+ @Test
+ public void setScale_doesNothing() throws Exception {
+ Vector3 scale = new Vector3(1, 1, 9999);
+ activitySpace.setScale(scale);
+
+ // The returned scale(s) here should be the identity scale despite the setScale call.
+ assertThat(activitySpace.getScale().getX()).isWithin(1e-5f).of(1.0f);
+ assertThat(activitySpace.getScale().getY()).isWithin(1e-5f).of(1.0f);
+ assertThat(activitySpace.getScale().getZ()).isWithin(1e-5f).of(1.0f);
+
+ // Note that there's no exception thrown.
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/AnchorEntityImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/AnchorEntityImplTest.java
new file mode 100644
index 0000000..84b93bb
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/AnchorEntityImplTest.java
@@ -0,0 +1,1247 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static androidx.xr.runtime.testing.math.MathAssertions.assertPose;
+import static androidx.xr.runtime.testing.math.MathAssertions.assertVector3;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.os.IBinder;
+import android.os.SystemClock;
+
+import androidx.xr.extensions.node.Node;
+import androidx.xr.runtime.internal.Anchor.PersistenceState;
+import androidx.xr.runtime.internal.TrackingState;
+import androidx.xr.runtime.math.Matrix4;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Quaternion;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.runtime.openxr.ExportableAnchor;
+import androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.OnStateChangedListener;
+import androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.PersistState;
+import androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.PersistStateChangeListener;
+import androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.State;
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.PlaneSemantic;
+import androidx.xr.scenecore.JxrPlatformAdapter.PlaneType;
+import androidx.xr.scenecore.impl.perception.Anchor;
+import androidx.xr.scenecore.impl.perception.PerceptionLibrary;
+import androidx.xr.scenecore.impl.perception.Plane;
+import androidx.xr.scenecore.impl.perception.Session;
+import androidx.xr.scenecore.testing.FakeScheduledExecutorService;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeGltfModelToken;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeNode;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.RobolectricTestRunner;
+
+import java.time.Duration;
+import java.util.UUID;
+
+@RunWith(RobolectricTestRunner.class)
+public final class AnchorEntityImplTest extends SystemSpaceEntityImplTest {
+ private static class FakeExportableAnchor implements ExportableAnchor {
+ private final long nativePointer;
+ private final IBinder anchorToken;
+ private final Pose pose;
+ private final TrackingState trackingState;
+ private final PersistenceState persistenceState;
+ private final UUID uuid;
+
+ public FakeExportableAnchor(
+ long nativePointer,
+ IBinder anchorToken,
+ Pose pose,
+ TrackingState trackingState,
+ PersistenceState persistenceState,
+ UUID uuid) {
+ this.nativePointer = nativePointer;
+ this.anchorToken = anchorToken;
+ this.pose = pose;
+ this.trackingState = trackingState;
+ this.persistenceState = persistenceState;
+ this.uuid = uuid;
+ }
+
+ @Override
+ public long getNativePointer() {
+ return nativePointer;
+ }
+
+ @Override
+ public IBinder getAnchorToken() {
+ return anchorToken;
+ }
+
+ @Override
+ public Pose getPose() {
+ return pose;
+ }
+
+ @Override
+ public TrackingState getTrackingState() {
+ return trackingState;
+ }
+
+ @Override
+ public PersistenceState getPersistenceState() {
+ return persistenceState;
+ }
+
+ @Override
+ public UUID getUuid() {
+ return uuid;
+ }
+
+ @Override
+ public void detach() {}
+
+ @Override
+ public void persist() {}
+ }
+
+ private static final Dimensions ANCHOR_DIMENSIONS = new Dimensions(2f, 5f, 0f);
+ private static final Plane.Type PLANE_TYPE = Plane.Type.VERTICAL;
+ private static final Plane.Label PLANE_LABEL = Plane.Label.WALL;
+ private static final long NATIVE_POINTER = 1234567890L;
+ private final AndroidXrEntity activitySpaceRoot = Mockito.mock(AndroidXrEntity.class);
+ private final FakeXrExtensions fakeExtensions = new FakeXrExtensions();
+ private final PerceptionLibrary perceptionLibrary = Mockito.mock(PerceptionLibrary.class);
+ private final Session session = Mockito.mock(Session.class);
+ private final Plane plane = mock(Plane.class);
+ private final Anchor anchor = Mockito.mock(Anchor.class);
+ private final OnStateChangedListener anchorStateListener =
+ Mockito.mock(OnStateChangedListener.class);
+ private final IBinder sharedAnchorToken = Mockito.mock(IBinder.class);
+ private final FakeScheduledExecutorService executor = new FakeScheduledExecutorService();
+ private final EntityManager entityManager = new EntityManager();
+ private final PersistStateChangeListener persistStateChangeListener =
+ Mockito.mock(PersistStateChangeListener.class);
+ private final androidx.xr.scenecore.impl.perception.Pose perceptionIdentityPose =
+ androidx.xr.scenecore.impl.perception.Pose.identity();
+ private long currentTimeMillis = 1000000000L;
+ private ActivitySpaceImpl activitySpace;
+
+ @Before
+ public void doBeforeEachTest() {
+ Node taskNode = fakeExtensions.createNode();
+ this.activitySpace =
+ new ActivitySpaceImpl(
+ taskNode,
+ fakeExtensions,
+ entityManager,
+ () -> fakeExtensions.fakeSpatialState,
+ executor);
+ SystemClock.setCurrentTimeMillis(currentTimeMillis);
+
+ // By default, set the activity space to the root of the underlying OpenXR reference space.
+ this.activitySpace.setOpenXrReferenceSpacePose(Matrix4.Identity);
+ }
+
+ /**
+ * Returns the anchor entity impl. Used in the base SystemSpaceEntityImplTest to ensure that the
+ * anchor entity complies with all the expected behaviors of a system space entity.
+ */
+ @Override
+ protected SystemSpaceEntityImpl getSystemSpaceEntityImpl() {
+ return createSemanticAnchorEntity();
+ }
+
+ @Override
+ protected FakeScheduledExecutorService getDefaultFakeExecutor() {
+ return executor;
+ }
+
+ @Override
+ protected AndroidXrEntity createChildAndroidXrEntity() {
+ return createGltfEntity();
+ }
+
+ @Override
+ protected ActivitySpaceImpl getActivitySpaceEntity() {
+ return this.activitySpace;
+ }
+
+ // Advances the clock and the executor. The fake executor is not based on the clock because we
+ // are
+ // using the SystemClock but they can be advanced together.
+ void advanceClock(Duration duration) {
+ currentTimeMillis += duration.toMillis();
+ SystemClock.setCurrentTimeMillis(currentTimeMillis);
+ executor.simulateSleepExecutingAllTasks(duration);
+ }
+
+ /** Creates an AnchorEntityImpl instance. */
+ private AnchorEntityImpl createSemanticAnchorEntity() {
+ return createAnchorEntityWithTimeout(null);
+ }
+
+ /** Creates an AnchorEntityImpl instance with a timeout. */
+ private AnchorEntityImpl createAnchorEntityWithTimeout(Duration anchorSearchTimeout) {
+ Node node = fakeExtensions.createNode();
+ return AnchorEntityImpl.createSemanticAnchor(
+ node,
+ ANCHOR_DIMENSIONS,
+ PlaneType.VERTICAL,
+ PlaneSemantic.WALL,
+ anchorSearchTimeout,
+ activitySpace,
+ activitySpaceRoot,
+ fakeExtensions,
+ entityManager,
+ executor,
+ perceptionLibrary);
+ }
+
+ /**
+ * Creates an AnchorEntityImpl instance that will search for a persisted anchor within
+ * `anchorSearchTimeout`.
+ */
+ private AnchorEntityImpl createPersistedAnchorEntityWithTimeout(
+ UUID uuid, Duration anchorSearchTimeout) {
+ Node node = fakeExtensions.createNode();
+ return AnchorEntityImpl.createPersistedAnchor(
+ node,
+ uuid,
+ anchorSearchTimeout,
+ activitySpace,
+ activitySpaceRoot,
+ fakeExtensions,
+ entityManager,
+ executor,
+ perceptionLibrary);
+ }
+
+ /** Creates an AnchorEntityImpl instance and initializes it with a persisted anchor. */
+ private AnchorEntityImpl createInitializedPersistedAnchorEntity(Anchor anchor, UUID uuid) {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.createAnchorFromUuid(uuid)).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ Node node = fakeExtensions.createNode();
+ return AnchorEntityImpl.createPersistedAnchor(
+ node,
+ uuid,
+ /* anchorSearchTimeout= */ null,
+ activitySpace,
+ activitySpaceRoot,
+ fakeExtensions,
+ entityManager,
+ executor,
+ perceptionLibrary);
+ }
+
+ /**
+ * Creates an AnchorEntityImpl and initializes it with an anchor from the perception library.
+ */
+ private AnchorEntityImpl createAndInitAnchorEntity() {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+ when(plane.getData(any()))
+ .thenReturn(
+ new Plane.PlaneData(
+ perceptionIdentityPose,
+ ANCHOR_DIMENSIONS.width,
+ ANCHOR_DIMENSIONS.height,
+ PLANE_TYPE.intValue,
+ PLANE_LABEL.intValue));
+ when(plane.createAnchor(eq(perceptionIdentityPose), any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ return createSemanticAnchorEntity();
+ }
+
+ private AnchorEntityImpl createInitAndPersistAnchorEntity() {
+ when(anchor.persist()).thenReturn(UUID.randomUUID());
+ AnchorEntityImpl anchorEntity = createAndInitAnchorEntity();
+ anchorEntity.registerPersistStateChangeListener(persistStateChangeListener);
+ UUID unused = anchorEntity.persist();
+ return anchorEntity;
+ }
+
+ private AnchorEntityImpl createAnchorEntityFromPlane() {
+ when(anchor.persist()).thenReturn(UUID.randomUUID());
+
+ Node node = fakeExtensions.createNode();
+ return AnchorEntityImpl.createAnchorFromPlane(
+ node,
+ plane,
+ new Pose(),
+ MILLISECONDS.toNanos(currentTimeMillis),
+ activitySpace,
+ activitySpaceRoot,
+ fakeExtensions,
+ entityManager,
+ executor,
+ perceptionLibrary);
+ }
+
+ private AnchorEntityImpl createAnchorEntityFromPerceptionAnchor(
+ androidx.xr.arcore.Anchor perceptionAnchor) {
+ Node node = fakeExtensions.createNode();
+
+ return AnchorEntityImpl.createAnchorFromPerceptionAnchor(
+ node,
+ perceptionAnchor,
+ activitySpace,
+ activitySpaceRoot,
+ fakeExtensions,
+ entityManager,
+ executor,
+ perceptionLibrary);
+ }
+
+ /** Creates a generic glTF entity. */
+ private GltfEntityImpl createGltfEntity() {
+ FakeGltfModelToken modelToken = new FakeGltfModelToken("model");
+ GltfModelResourceImpl model = new GltfModelResourceImpl(modelToken);
+ return new GltfEntityImpl(model, activitySpace, fakeExtensions, entityManager, executor);
+ }
+
+ @Test
+ public void createAnchorEntityWithPersistedAnchor_returnsAnchored() throws Exception {
+ AnchorEntityImpl anchorEntity =
+ createInitializedPersistedAnchorEntity(anchor, UUID.randomUUID());
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.ANCHORED);
+ assertThat(((FakeNode) anchorEntity.getNode()).getName())
+ .isEqualTo(AnchorEntityImpl.ANCHOR_NODE_NAME);
+ assertThat(((FakeNode) anchorEntity.getNode()).getAnchorId()).isEqualTo(sharedAnchorToken);
+ assertThat(anchorEntity.getPersistState()).isEqualTo(PersistState.PERSISTED);
+ }
+
+ @Test
+ public void createAnchorEntityWithPersistedAnchor_persistAnchor_returnsUuid() throws Exception {
+ UUID uuid = UUID.randomUUID();
+ AnchorEntityImpl anchorEntity = createInitializedPersistedAnchorEntity(anchor, uuid);
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.ANCHORED);
+ assertThat(((FakeNode) anchorEntity.getNode()).getName())
+ .isEqualTo(AnchorEntityImpl.ANCHOR_NODE_NAME);
+ assertThat(((FakeNode) anchorEntity.getNode()).getAnchorId()).isEqualTo(sharedAnchorToken);
+ assertThat(anchorEntity.getPersistState()).isEqualTo(PersistState.PERSISTED);
+
+ UUID returnedUuid = anchorEntity.persist();
+ assertThat(returnedUuid).isEqualTo(uuid);
+ }
+
+ @Test
+ public void createPersistedAnchorEntity_sessionNotReady_keepUnanchored() throws Exception {
+ when(perceptionLibrary.getSession()).thenReturn(null);
+ AnchorEntityImpl anchorEntity =
+ createPersistedAnchorEntityWithTimeout(
+ UUID.randomUUID(), /* anchorSearchTimeout= */ null);
+
+ // if the session isn't ready, we should retry later and search for the anchor.
+ assertThat(anchorEntity.getState()).isEqualTo(State.UNANCHORED);
+ }
+
+ @Test
+ public void createPersistedAnchorEntity_persistedAnchorNotFound_keepUnanchored()
+ throws Exception {
+ UUID uuid = UUID.randomUUID();
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.createAnchorFromUuid(uuid)).thenReturn(null);
+ AnchorEntityImpl anchorEntity =
+ createPersistedAnchorEntityWithTimeout(uuid, /* anchorSearchTimeout= */ null);
+
+ // If the session is ready and we can't find it, the perception stack might be warming up,
+ // so
+ // we'll retry later.
+ assertThat(anchorEntity.getState()).isEqualTo(State.UNANCHORED);
+ }
+
+ @Test
+ public void createPersistedAnchorEntity_persistedAnchorHasNoToken_keepUnanchored()
+ throws Exception {
+ UUID uuid = UUID.randomUUID();
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.createAnchorFromUuid(uuid)).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(null);
+ AnchorEntityImpl anchorEntity =
+ createPersistedAnchorEntityWithTimeout(uuid, /* anchorSearchTimeout= */ null);
+
+ // If the anchor is ready but its token isn't available, we'll retry later.
+ assertThat(anchorEntity.getState()).isEqualTo(State.UNANCHORED);
+ }
+
+ @Test
+ public void createPersistedAnchorEntity_delayedSession_callsCallback() throws Exception {
+ // This will return an error on the first attempt so will need to be called twice.
+ when(perceptionLibrary.getSession()).thenReturn(null).thenReturn(session);
+ UUID uuid = UUID.randomUUID();
+ when(session.createAnchorFromUuid(uuid)).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ AnchorEntityImpl anchorEntity =
+ createPersistedAnchorEntityWithTimeout(uuid, /* anchorSearchTimeout= */ null);
+ anchorEntity.setOnStateChangedListener(anchorStateListener);
+
+ verify(anchorStateListener, never()).onStateChanged(State.ANCHORED);
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.UNANCHORED);
+
+ // The anchor starts as unanchored. Advance the executor to try again successfully and get a
+ // callback for the anchor to be anchored.
+ advanceClock(AnchorEntityImpl.ANCHOR_SEARCH_DELAY);
+ verify(anchorStateListener).onStateChanged(State.ANCHORED);
+ assertThat(anchorEntity.getState()).isEqualTo(State.ANCHORED);
+ assertThat(anchorEntity.getPersistState()).isEqualTo(PersistState.PERSISTED);
+ }
+
+ @Test
+ public void createPersistedAnchorEntity_delayedAnchor_callsCallback() throws Exception {
+ // This will return an error on the first attempt so will need to be called twice.
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ UUID uuid = UUID.randomUUID();
+ when(session.createAnchorFromUuid(uuid)).thenReturn(anchor).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(null).thenReturn(sharedAnchorToken);
+
+ AnchorEntityImpl anchorEntity =
+ createPersistedAnchorEntityWithTimeout(uuid, /* anchorSearchTimeout= */ null);
+ anchorEntity.setOnStateChangedListener(anchorStateListener);
+
+ verify(anchorStateListener, never()).onStateChanged(State.ANCHORED);
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.UNANCHORED);
+
+ // The anchor starts as unanchored. Advance the executor to try again successfully and get a
+ // callback for the anchor to be anchored.
+ advanceClock(AnchorEntityImpl.ANCHOR_SEARCH_DELAY);
+ verify(anchorStateListener).onStateChanged(State.ANCHORED);
+ assertThat(anchorEntity.getState()).isEqualTo(State.ANCHORED);
+ assertThat(anchorEntity.getPersistState()).isEqualTo(PersistState.PERSISTED);
+ }
+
+ @Test
+ public void createPersistedAnchorEntity_delayedSession_timeout_noCallback() throws Exception {
+ // This will return an error on the first attempt so will need to be called twice.
+ when(perceptionLibrary.getSession()).thenReturn(null).thenReturn(session);
+ UUID uuid = UUID.randomUUID();
+ when(session.createAnchorFromUuid(uuid)).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ AnchorEntityImpl anchorEntity =
+ createPersistedAnchorEntityWithTimeout(
+ uuid, AnchorEntityImpl.ANCHOR_SEARCH_DELAY.dividedBy(2));
+ anchorEntity.setOnStateChangedListener(anchorStateListener);
+
+ advanceClock(AnchorEntityImpl.ANCHOR_SEARCH_DELAY);
+
+ verify(anchorStateListener, never()).onStateChanged(State.ANCHORED);
+ verify(anchorStateListener).onStateChanged(State.TIMED_OUT);
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.TIMED_OUT);
+ }
+
+ @Test
+ public void createAnchorEntity_defaultUnanchored() throws Exception {
+ AnchorEntityImpl anchorEntity = createSemanticAnchorEntity();
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.UNANCHORED);
+ assertThat(((FakeNode) anchorEntity.getNode()).getName())
+ .isEqualTo(AnchorEntityImpl.ANCHOR_NODE_NAME);
+ }
+
+ @Test
+ public void createAndInitAnchor_returnsAnchored() throws Exception {
+ AnchorEntityImpl anchorEntity = createAndInitAnchorEntity();
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.ANCHORED);
+ assertThat(((FakeNode) anchorEntity.getNode()).getName())
+ .isEqualTo(AnchorEntityImpl.ANCHOR_NODE_NAME);
+ assertThat(((FakeNode) anchorEntity.getNode()).getAnchorId()).isEqualTo(sharedAnchorToken);
+ }
+
+ @Test
+ public void createAndInitAnchor_noPlanes_remainsUnanchored() throws Exception {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of());
+
+ AnchorEntityImpl anchorEntity = createSemanticAnchorEntity();
+
+ assertThat(anchorEntity.getState()).isEqualTo(State.UNANCHORED);
+ assertThat(((FakeNode) anchorEntity.getNode()).getName())
+ .isEqualTo(AnchorEntityImpl.ANCHOR_NODE_NAME);
+ }
+
+ @Test
+ public void createAndInitAnchor_noViablePlane_remainsUnanchored() throws Exception {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+ when(plane.getData(any()))
+ .thenReturn(
+ new Plane.PlaneData(
+ perceptionIdentityPose,
+ ANCHOR_DIMENSIONS.width - 1,
+ ANCHOR_DIMENSIONS.height,
+ PLANE_TYPE.intValue,
+ PLANE_LABEL.intValue));
+
+ AnchorEntityImpl anchorEntity = createSemanticAnchorEntity();
+
+ assertThat(anchorEntity.getState()).isEqualTo(State.UNANCHORED);
+ assertThat(((FakeNode) anchorEntity.getNode()).getName())
+ .isEqualTo(AnchorEntityImpl.ANCHOR_NODE_NAME);
+ }
+
+ @Test
+ public void createAndInitAnchor_delayedSession_callsCallback() throws Exception {
+ // This will return an error on the first attempt so will need to be called twice.
+ when(perceptionLibrary.getSession()).thenReturn(null).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+ when(plane.getData(any()))
+ .thenReturn(
+ new Plane.PlaneData(
+ perceptionIdentityPose,
+ ANCHOR_DIMENSIONS.width,
+ ANCHOR_DIMENSIONS.height,
+ PLANE_TYPE.intValue,
+ PLANE_LABEL.intValue));
+ when(plane.createAnchor(eq(perceptionIdentityPose), any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ AnchorEntityImpl anchorEntity = createSemanticAnchorEntity();
+ anchorEntity.setOnStateChangedListener(anchorStateListener);
+
+ verify(anchorStateListener, never()).onStateChanged(State.ANCHORED);
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.UNANCHORED);
+
+ // The anchor starts as unanchored. Advance the executor to try again successfully and get a
+ // callback for the anchor to be anchored.
+ advanceClock(AnchorEntityImpl.ANCHOR_SEARCH_DELAY);
+ verify(anchorStateListener).onStateChanged(State.ANCHORED);
+ assertThat(anchorEntity.getState()).isEqualTo(State.ANCHORED);
+ }
+
+ @Test
+ public void createAndInitAnchor_delayedAnchor_callsCallback() throws Exception {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ // This will return no planes on the first attempt so will need to be called twice.
+ when(session.getAllPlanes())
+ .thenReturn(ImmutableList.of())
+ .thenReturn(ImmutableList.of(plane));
+ when(plane.getData(any()))
+ .thenReturn(
+ new Plane.PlaneData(
+ perceptionIdentityPose,
+ ANCHOR_DIMENSIONS.width,
+ ANCHOR_DIMENSIONS.height,
+ PLANE_TYPE.intValue,
+ PLANE_LABEL.intValue));
+ when(plane.createAnchor(eq(perceptionIdentityPose), any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ AnchorEntityImpl anchorEntity = createSemanticAnchorEntity();
+ anchorEntity.setOnStateChangedListener(anchorStateListener);
+
+ verify(anchorStateListener, never()).onStateChanged(State.ANCHORED);
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.UNANCHORED);
+
+ // The anchor starts as unanchored. Advance the executor to try again successfully and get a
+ // callback for the anchor to be anchored.
+ advanceClock(AnchorEntityImpl.ANCHOR_SEARCH_DELAY);
+ verify(anchorStateListener).onStateChanged(State.ANCHORED);
+ assertThat(anchorEntity.getState()).isEqualTo(State.ANCHORED);
+ }
+
+ @Test
+ public void createAndInitAnchor_delayedSessionAndAnchor_callsCallback() throws Exception {
+ // This will return an error on the first attempt then it will find no planes on the
+ // second attempt. So it will need to be called three times.
+ when(perceptionLibrary.getSession())
+ .thenReturn(null)
+ .thenReturn(session)
+ .thenReturn(session);
+ when(session.getAllPlanes())
+ .thenReturn(ImmutableList.of())
+ .thenReturn(ImmutableList.of(plane));
+ when(plane.getData(any()))
+ .thenReturn(
+ new Plane.PlaneData(
+ perceptionIdentityPose,
+ ANCHOR_DIMENSIONS.width,
+ ANCHOR_DIMENSIONS.height,
+ PLANE_TYPE.intValue,
+ PLANE_LABEL.intValue));
+ when(plane.createAnchor(eq(perceptionIdentityPose), any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ AnchorEntityImpl anchorEntity = createSemanticAnchorEntity();
+ anchorEntity.setOnStateChangedListener(anchorStateListener);
+
+ verify(anchorStateListener, never()).onStateChanged(State.ANCHORED);
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.UNANCHORED);
+
+ // Advance the executor to try again with a working session but an error on the anchor.
+ advanceClock(AnchorEntityImpl.ANCHOR_SEARCH_DELAY);
+ assertThat(anchorEntity.getState()).isEqualTo(State.UNANCHORED);
+
+ // Advance the executor attempt the anchor again successfully and get a callback for the
+ // anchor
+ // to be anchored.
+ advanceClock(AnchorEntityImpl.ANCHOR_SEARCH_DELAY);
+ verify(anchorStateListener).onStateChanged(State.ANCHORED);
+ assertThat(anchorEntity.getState()).isEqualTo(State.ANCHORED);
+ }
+
+ @Test
+ public void createAndInitAnchor_noToken_returnsUnanchored() throws Exception {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+ when(plane.getData(any()))
+ .thenReturn(
+ new Plane.PlaneData(
+ perceptionIdentityPose,
+ ANCHOR_DIMENSIONS.width,
+ ANCHOR_DIMENSIONS.height,
+ PLANE_TYPE.intValue,
+ PLANE_LABEL.intValue));
+ when(plane.createAnchor(eq(perceptionIdentityPose), any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(null);
+
+ AnchorEntityImpl anchorEntity = createSemanticAnchorEntity();
+
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.UNANCHORED);
+ }
+
+ @Test
+ public void createAndInitAnchor_withinTimeout_success() throws Exception {
+ // The anchor creation will return an error so that it is called again on the executor. The
+ // timeout will happen after the second attempt so it will still succeed.
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes())
+ .thenReturn(ImmutableList.of())
+ .thenReturn(ImmutableList.of(plane));
+ when(plane.getData(any()))
+ .thenReturn(
+ new Plane.PlaneData(
+ perceptionIdentityPose,
+ ANCHOR_DIMENSIONS.width,
+ ANCHOR_DIMENSIONS.height,
+ PLANE_TYPE.intValue,
+ PLANE_LABEL.intValue));
+ when(plane.createAnchor(eq(perceptionIdentityPose), any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ // Create an anchor entity with a timeout of 1ms above the normal search delay.
+ Duration timeout = AnchorEntityImpl.ANCHOR_SEARCH_DELAY.plusMillis(1);
+ AnchorEntityImpl anchorEntity = createAnchorEntityWithTimeout(timeout);
+ anchorEntity.setOnStateChangedListener(anchorStateListener);
+
+ verify(anchorStateListener, never()).onStateChanged(State.ANCHORED);
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.UNANCHORED);
+
+ // Advance the executor to try again it should now be anchored.
+ advanceClock(AnchorEntityImpl.ANCHOR_SEARCH_DELAY);
+ verify(anchorStateListener).onStateChanged(State.ANCHORED);
+ assertThat(anchorEntity.getState()).isEqualTo(State.ANCHORED);
+
+ // Advance the clock past the timeout and verify that nothing changed.
+ advanceClock(timeout.minus(AnchorEntityImpl.ANCHOR_SEARCH_DELAY));
+ verify(anchorStateListener, never()).onStateChanged(State.TIMED_OUT);
+ assertThat(anchorEntity.getState()).isEqualTo(State.ANCHORED);
+ }
+
+ @Test
+ public void createAndInitAnchor_passedTimeout_error() throws Exception {
+ // The anchor creation will return an error so that it is called again on the executor. The
+ // timeout will happen before the next call.
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of());
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ // Create an anchor entity with a timeout of 1ms below the normal search delay.
+ Duration timeout = AnchorEntityImpl.ANCHOR_SEARCH_DELAY.minusMillis(1);
+ AnchorEntityImpl anchorEntity = createAnchorEntityWithTimeout(timeout);
+ anchorEntity.setOnStateChangedListener(anchorStateListener);
+
+ verify(anchorStateListener, never()).onStateChanged(State.ERROR);
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.UNANCHORED);
+
+ // Advance the executor to try again it should now be timed out.
+ advanceClock(AnchorEntityImpl.ANCHOR_SEARCH_DELAY);
+ assertThat(anchorEntity.getState()).isEqualTo(State.TIMED_OUT);
+
+ // Advance the clock again and verify that nothing changed.
+ advanceClock(AnchorEntityImpl.ANCHOR_SEARCH_DELAY);
+ assertThat(anchorEntity.getState()).isEqualTo(State.TIMED_OUT);
+ }
+
+ @Test
+ public void createAndInitAnchor_zeroTimeout_keepsSearching() throws Exception {
+ // The anchor creation will return an error so that it is called again on the executor. The
+ // timeout will happen after the second attempt so it will still succeed.
+ int anchorAttempts = 100;
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of());
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ // Create an anchor entity with a zero duration it should be search for indefinitely..
+ AnchorEntityImpl anchorEntity = createAnchorEntityWithTimeout(Duration.ZERO);
+ anchorEntity.setOnStateChangedListener(anchorStateListener);
+
+ for (int i = 0; i < anchorAttempts - 1; i++) {
+ advanceClock(AnchorEntityImpl.ANCHOR_SEARCH_DELAY);
+ verify(anchorStateListener, never()).onStateChanged(any());
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.UNANCHORED);
+ }
+ }
+
+ @Test
+ public void anchorEntityAddChildren_addsChildren() throws Exception {
+ GltfEntityImpl childEntity1 = createGltfEntity();
+ GltfEntityImpl childEntity2 = createGltfEntity();
+ AnchorEntityImpl parentEntity = createSemanticAnchorEntity();
+
+ parentEntity.addChild(childEntity1);
+
+ assertThat(parentEntity.getChildren()).containsExactly(childEntity1);
+
+ parentEntity.addChildren(ImmutableList.of(childEntity2));
+
+ assertThat(childEntity1.getParent()).isEqualTo(parentEntity);
+ assertThat(childEntity2.getParent()).isEqualTo(parentEntity);
+ assertThat(parentEntity.getChildren()).containsExactly(childEntity1, childEntity2);
+
+ FakeNode parentNode = (FakeNode) parentEntity.getNode();
+ FakeNode childNode1 = (FakeNode) childEntity1.getNode();
+ FakeNode childNode2 = (FakeNode) childEntity2.getNode();
+
+ assertThat(childNode1.getParent()).isEqualTo(parentNode);
+ assertThat(childNode2.getParent()).isEqualTo(parentNode);
+ }
+
+ @Test
+ public void anchorEntitySetPose_throwsException() throws Exception {
+ AnchorEntityImpl anchorEntity = createSemanticAnchorEntity();
+ Pose pose = new Pose();
+ assertThrows(UnsupportedOperationException.class, () -> anchorEntity.setPose(pose));
+ }
+
+ @Test
+ public void anchorEntitySetScale_throwsException() throws Exception {
+ AnchorEntityImpl anchorEntity = createSemanticAnchorEntity();
+ Vector3 scale = new Vector3(1, 1, 1);
+ assertThrows(UnsupportedOperationException.class, () -> anchorEntity.setScale(scale));
+ }
+
+ @Test
+ public void anchorEntityGetScale_returnsIdentityScale() throws Exception {
+ AnchorEntityImpl anchorEntity = createSemanticAnchorEntity();
+ assertVector3(anchorEntity.getScale(), new Vector3(1f, 1f, 1f));
+ }
+
+ @Test
+ public void anchorEntityGetWorldSpaceScale_returnsIdentityScale() throws Exception {
+ AnchorEntityImpl anchorEntity = createSemanticAnchorEntity();
+ assertVector3(anchorEntity.getWorldSpaceScale(), new Vector3(1f, 1f, 1f));
+ }
+
+ @Test
+ public void anchorEntityGetActivitySpaceScale_returnsInverseOfActivitySpace() throws Exception {
+ float activitySpaceScale = 5f;
+ this.activitySpace.setOpenXrReferenceSpacePose(Matrix4.fromScale(activitySpaceScale));
+ AnchorEntityImpl anchorEntity = createSemanticAnchorEntity();
+ assertVector3(
+ anchorEntity.getActivitySpaceScale(),
+ new Vector3(1f, 1f, 1f).div(activitySpaceScale));
+ }
+
+ @Test
+ public void getPoseInActivitySpace_unanchored_returnsIdentityPose() {
+ AnchorEntityImpl anchorEntity = createSemanticAnchorEntity();
+ Pose pose = new Pose(new Vector3(1, 1, 1), new Quaternion(0, 1, 0, 1));
+ activitySpace.setOpenXrReferenceSpacePose(Matrix4.Identity);
+ anchorEntity.setOpenXrReferenceSpacePose(Matrix4.fromPose(pose));
+
+ assertPose(anchorEntity.getPoseInActivitySpace(), new Pose());
+ }
+
+ @Test
+ public void
+ getPoseInActivitySpace_noActivitySpaceOpenXrReferenceSpacePose_returnsIdentityPose() {
+ AnchorEntityImpl anchorEntity = createAndInitAnchorEntity();
+ Pose pose = new Pose(new Vector3(1, 1, 1), new Quaternion(0, 1, 0, 1));
+ activitySpace.openXrReferenceSpacePose = null;
+ anchorEntity.setOpenXrReferenceSpacePose(Matrix4.fromPose(pose));
+ assertPose(anchorEntity.getPoseInActivitySpace(), new Pose());
+ }
+
+ @Test
+ public void getPoseInActivitySpace_noAnchorOpenXrReferenceSpacePose_returnsIdentityPose() {
+ AnchorEntityImpl anchorEntity = createAndInitAnchorEntity();
+ Pose pose = new Pose(new Vector3(1, 1, 1), new Quaternion(0, 1, 0, 1));
+ activitySpace.setOpenXrReferenceSpacePose(Matrix4.fromPose(pose));
+ // anchorEntity.setOpenXrReferenceSpacePose(..) is not called to set the underlying pose.
+
+ assertPose(anchorEntity.getPoseInActivitySpace(), new Pose());
+ }
+
+ @Test
+ public void getPoseInActivitySpace_whenAtSamePose_returnsIdentityPose() {
+ AnchorEntityImpl anchorEntity = createAndInitAnchorEntity();
+ Pose pose = new Pose(new Vector3(1, 1, 1), new Quaternion(0, 1, 0, 1).toNormalized());
+ activitySpace.setOpenXrReferenceSpacePose(Matrix4.fromPose(pose));
+ anchorEntity.setOpenXrReferenceSpacePose(Matrix4.fromPose(pose));
+
+ assertPose(anchorEntity.getPoseInActivitySpace(), new Pose());
+ }
+
+ @Test
+ public void getPoseInActivitySpace_returnsDifferencePose() {
+ AnchorEntityImpl anchorEntity = createAndInitAnchorEntity();
+ Pose pose = new Pose(new Vector3(1, 1, 1), new Quaternion(0, 1, 0, 1).toNormalized());
+ activitySpace.setOpenXrReferenceSpacePose(Matrix4.Identity);
+ anchorEntity.setOpenXrReferenceSpacePose(Matrix4.fromPose(pose));
+
+ assertPose(anchorEntity.getPoseInActivitySpace(), pose);
+ }
+
+ @Test
+ public void getPoseInActivitySpace_withNoActivitySpace_throwsException() throws Exception {
+ Node node = fakeExtensions.createNode();
+ AnchorEntityImpl anchorEntity =
+ AnchorEntityImpl.createSemanticAnchor(
+ node,
+ /* dimensions= */ null,
+ /* planeType= */ null,
+ /* planeSemantic= */ null,
+ /* anchorSearchTimeout= */ null,
+ /* activitySpace= */ null,
+ activitySpaceRoot,
+ fakeExtensions,
+ entityManager,
+ executor,
+ perceptionLibrary);
+
+ assertThat(anchorEntity.getState()).isEqualTo(State.ERROR);
+ assertThrows(IllegalStateException.class, anchorEntity::getPoseInActivitySpace);
+ }
+
+ @Test
+ public void getActivitySpacePose_whenAtSamePose_returnsIdentityPose() {
+ AnchorEntityImpl anchorEntity = createAndInitAnchorEntity();
+ Pose pose = new Pose(new Vector3(1, 1, 1), new Quaternion(0, 1, 0, 1).toNormalized());
+ when(activitySpaceRoot.getPoseInActivitySpace()).thenReturn(pose);
+
+ anchorEntity.setOpenXrReferenceSpacePose(Matrix4.fromPose(pose));
+
+ assertPose(anchorEntity.getActivitySpacePose(), new Pose());
+ }
+
+ @Test
+ public void getActivitySpacePose_returnsDifferencePose() {
+ AnchorEntityImpl anchorEntity = createAndInitAnchorEntity();
+ Pose pose = new Pose(new Vector3(1, 1, 1), new Quaternion(0, 1, 0, 1).toNormalized());
+ when(activitySpaceRoot.getPoseInActivitySpace()).thenReturn(new Pose());
+
+ anchorEntity.setOpenXrReferenceSpacePose(Matrix4.fromPose(pose));
+
+ assertPose(anchorEntity.getActivitySpacePose(), pose);
+ }
+
+ @Test
+ public void getActivitySpacePose_withNonAndroidXrActivitySpaceRoot_throwsException()
+ throws Exception {
+ Node node = fakeExtensions.createNode();
+ AnchorEntityImpl anchorEntity =
+ AnchorEntityImpl.createSemanticAnchor(
+ node,
+ /* dimensions= */ null,
+ /* planeType= */ null,
+ /* planeSemantic= */ null,
+ /* anchorSearchTimeout= */ null,
+ activitySpace,
+ /* activitySpaceRoot= */ null,
+ fakeExtensions,
+ entityManager,
+ executor,
+ perceptionLibrary);
+
+ assertThat(anchorEntity.getState()).isEqualTo(State.ERROR);
+ assertThrows(IllegalStateException.class, anchorEntity::getActivitySpacePose);
+ }
+
+ @Test
+ public void transformPoseTo_withActivitySpace_returnsTransformedPose() {
+ AnchorEntityImpl anchorEntity = createAndInitAnchorEntity();
+ Pose pose = new Pose(new Vector3(1f, 2f, 3f), Quaternion.Identity);
+ activitySpace.setOpenXrReferenceSpacePose(Matrix4.Identity);
+ anchorEntity.setOpenXrReferenceSpacePose(Matrix4.fromPose(pose));
+
+ Pose anchorOffset =
+ new Pose(
+ new Vector3(10f, 0f, 0f),
+ Quaternion.fromEulerAngles(new Vector3(0f, 0f, 90f)));
+ Pose transformedPose = anchorEntity.transformPoseTo(anchorOffset, activitySpace);
+
+ assertPose(
+ transformedPose,
+ new Pose(
+ new Vector3(11f, 2f, 3f),
+ Quaternion.fromEulerAngles(new Vector3(0f, 0f, 90f))));
+ }
+
+ @Test
+ public void transformPoseTo_fromActivitySpaceChild_returnsAnchorSpacePose() {
+ AnchorEntityImpl anchorEntity = createAndInitAnchorEntity();
+ GltfEntityImpl childEntity1 = createGltfEntity();
+ Pose pose = new Pose(new Vector3(1f, 2f, 3f), Quaternion.Identity);
+ Pose childPose = new Pose(new Vector3(-1f, -2f, -3f), Quaternion.Identity);
+
+ activitySpace.setOpenXrReferenceSpacePose(Matrix4.Identity);
+ activitySpace.addChild(childEntity1);
+ childEntity1.setPose(childPose);
+ anchorEntity.setOpenXrReferenceSpacePose(Matrix4.fromPose(pose));
+
+ assertPose(
+ activitySpace.transformPoseTo(new Pose(), anchorEntity),
+ new Pose(new Vector3(-1f, -2f, -3f), Quaternion.Identity));
+
+ Pose transformedPose = childEntity1.transformPoseTo(new Pose(), anchorEntity);
+ assertPose(transformedPose, new Pose(new Vector3(-2f, -4f, -6f), Quaternion.Identity));
+ }
+
+ @Test
+ public void anchorEntity_setsParentAfterAnchoring() throws Exception {
+ // This will return an error on the first attempt so will need to be called twice.
+ when(perceptionLibrary.getSession()).thenReturn(null).thenReturn(session);
+
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+ when(plane.getData(any()))
+ .thenReturn(
+ new Plane.PlaneData(
+ perceptionIdentityPose,
+ ANCHOR_DIMENSIONS.width,
+ ANCHOR_DIMENSIONS.height,
+ PLANE_TYPE.intValue,
+ PLANE_LABEL.intValue));
+ when(plane.createAnchor(eq(perceptionIdentityPose), any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ AnchorEntityImpl anchorEntity = createSemanticAnchorEntity();
+ assertThat(anchorEntity.getState()).isEqualTo(State.UNANCHORED);
+
+ FakeNode anchorNode = (FakeNode) anchorEntity.getNode();
+ FakeNode rootNode = (FakeNode) activitySpace.getNode();
+ assertThat(anchorNode.getParent()).isNull();
+
+ // The anchor starts as unanchored. Advance the executor to wait for it to become anchored
+ // and
+ // verify that the parent is the root node.
+ advanceClock(AnchorEntityImpl.ANCHOR_SEARCH_DELAY);
+ assertThat(anchorEntity.getState()).isEqualTo(State.ANCHORED);
+ assertThat(anchorNode.getParent()).isEqualTo(rootNode);
+ }
+
+ @Test
+ public void disposeAnchor_detachesAnchor() throws Exception {
+ when(anchor.detach()).thenReturn(true);
+ AnchorEntityImpl anchorEntity = createAndInitAnchorEntity();
+ anchorEntity.setOnStateChangedListener(anchorStateListener);
+ verify(anchorStateListener, never()).onStateChanged(State.ERROR);
+ assertThat(anchorEntity.getState()).isEqualTo(State.ANCHORED);
+
+ // Verify the parent of the anchor is the root node (ActivitySpace) before disposing it.
+ FakeNode anchorNode = (FakeNode) anchorEntity.getNode();
+ FakeNode rootNode = (FakeNode) activitySpace.getNode();
+ assertThat(anchorNode.getParent()).isEqualTo(rootNode);
+ assertThat(anchorNode.getAnchorId()).isEqualTo(sharedAnchorToken);
+
+ // Dispose the entity and verify that the state was updated.
+ anchorEntity.dispose();
+
+ verify(anchorStateListener).onStateChanged(State.ERROR);
+ assertThat(anchorEntity.getState()).isEqualTo(State.ERROR);
+ assertThat(anchorNode.getParent()).isNull();
+ assertThat(anchorNode.getAnchorId()).isNull();
+ }
+
+ @Test
+ public void disposeAnchorUnanchered_stopsSearching() throws Exception {
+ when(perceptionLibrary.getSession()).thenReturn(null);
+
+ AnchorEntityImpl anchorEntity = createSemanticAnchorEntity();
+ anchorEntity.setOnStateChangedListener(anchorStateListener);
+
+ verify(perceptionLibrary).getSession();
+ verify(anchorStateListener, never()).onStateChanged(State.ERROR);
+ assertThat(anchorEntity.getState()).isEqualTo(State.UNANCHORED);
+
+ // Dispose the anchor entity before it was anchored.
+ anchorEntity.dispose();
+
+ // verify(anchorStateListener).onStateChanged(State.ERROR);
+ assertThat(anchorEntity.getState()).isEqualTo(State.ERROR);
+
+ // Advance the executor attempt to show that it will not call into the perception library
+ // again
+ // once disposed.
+ advanceClock(AnchorEntityImpl.ANCHOR_SEARCH_DELAY);
+ verify(perceptionLibrary).getSession();
+ }
+
+ @Test
+ public void disposeAnchorTwice_callsCalbackOnce() throws Exception {
+ when(anchor.detach()).thenReturn(true);
+ AnchorEntityImpl anchorEntity = createAndInitAnchorEntity();
+ anchorEntity.setOnStateChangedListener(anchorStateListener);
+ verify(anchorStateListener, never()).onStateChanged(State.ERROR);
+
+ // Dispose the entity and verify that the state was updated.
+ anchorEntity.dispose();
+
+ verify(anchorStateListener).onStateChanged(State.ERROR);
+
+ // Dispose anchor again and verify onStateChanged was called only once.
+ anchorEntity.dispose();
+
+ verify(anchorStateListener).onStateChanged(State.ERROR);
+ }
+
+ @Test
+ public void createAnchorEntity_defaultPersistState_returnsPersistNotRequested()
+ throws Exception {
+ AnchorEntityImpl anchorEntity = createSemanticAnchorEntity();
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getPersistState()).isEqualTo(PersistState.PERSIST_NOT_REQUESTED);
+ }
+
+ @Test
+ public void persistAnchor_notAnchored_returnsNull() throws Exception {
+ AnchorEntityImpl anchorEntity = createSemanticAnchorEntity();
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.persist()).isNull();
+ }
+
+ @Test
+ public void persistAnchor_returnsUuid() throws Exception {
+ AnchorEntityImpl anchorEntity = createAndInitAnchorEntity();
+ when(anchor.persist()).thenReturn(UUID.randomUUID());
+ anchorEntity.registerPersistStateChangeListener(persistStateChangeListener);
+ assertThat(anchorEntity.persist()).isNotNull();
+ assertThat(anchorEntity.getPersistState()).isEqualTo(PersistState.PERSIST_PENDING);
+ verify(persistStateChangeListener).onPersistStateChanged(PersistState.PERSIST_PENDING);
+ }
+
+ @Test
+ public void persistAnchor_returnsNull() throws Exception {
+ AnchorEntityImpl anchorEntity = createAndInitAnchorEntity();
+ when(anchor.persist()).thenReturn(null);
+ anchorEntity.registerPersistStateChangeListener(persistStateChangeListener);
+ assertThat(anchorEntity.persist()).isNull();
+ assertThat(anchorEntity.getPersistState()).isEqualTo(PersistState.PERSIST_NOT_REQUESTED);
+ verify(persistStateChangeListener, never()).onPersistStateChanged(any());
+ }
+
+ @Test
+ public void persistAnchor_secondPersist_returnsSameUuid_updatesPersistStateOnce()
+ throws Exception {
+ AnchorEntityImpl anchorEntity = createAndInitAnchorEntity();
+ when(anchor.persist()).thenReturn(UUID.randomUUID());
+ anchorEntity.registerPersistStateChangeListener(persistStateChangeListener);
+ UUID firstUuid = anchorEntity.persist();
+ assertThat(firstUuid).isNotNull();
+ assertThat(anchorEntity.getPersistState()).isEqualTo(PersistState.PERSIST_PENDING);
+
+ UUID secondUuid = anchorEntity.persist();
+ assertThat(firstUuid).isEquivalentAccordingToCompareTo(secondUuid);
+ assertThat(anchorEntity.getPersistState()).isEqualTo(PersistState.PERSIST_PENDING);
+ verify(persistStateChangeListener).onPersistStateChanged(PersistState.PERSIST_PENDING);
+ }
+
+ @Test
+ public void persistAnchor_updatesPersistStateToPersisted() throws Exception {
+ AnchorEntityImpl anchorEntity = createInitAndPersistAnchorEntity();
+ when(anchor.getPersistState()).thenReturn(Anchor.PersistState.PERSISTED);
+
+ executor.simulateSleepExecutingAllTasks(AnchorEntityImpl.PERSIST_STATE_CHECK_DELAY);
+ assertThat(anchorEntity.getPersistState()).isEqualTo(PersistState.PERSISTED);
+ verify(persistStateChangeListener).onPersistStateChanged(PersistState.PERSISTED);
+ }
+
+ @Test
+ public void updatePersistState_delayedPersistedState_callsCallback() throws Exception {
+ AnchorEntityImpl anchorEntity = createInitAndPersistAnchorEntity();
+
+ when(anchor.getPersistState())
+ .thenReturn(Anchor.PersistState.PERSIST_PENDING)
+ .thenReturn(Anchor.PersistState.PERSISTED);
+ verify(persistStateChangeListener, never()).onPersistStateChanged(PersistState.PERSISTED);
+
+ executor.simulateSleepExecutingAllTasks(AnchorEntityImpl.PERSIST_STATE_CHECK_DELAY);
+ verify(persistStateChangeListener, never()).onPersistStateChanged(PersistState.PERSISTED);
+ assertThat(anchorEntity.getPersistState()).isEqualTo(PersistState.PERSIST_PENDING);
+
+ executor.simulateSleepExecutingAllTasks(AnchorEntityImpl.PERSIST_STATE_CHECK_DELAY);
+ verify(persistStateChangeListener).onPersistStateChanged(PersistState.PERSISTED);
+ assertThat(anchorEntity.getPersistState()).isEqualTo(PersistState.PERSISTED);
+ }
+
+ @Test
+ public void updatePersistState_noCallbackAfterStateBecomesPersisted() throws Exception {
+ AnchorEntityImpl anchorEntity = createInitAndPersistAnchorEntity();
+
+ when(anchor.getPersistState()).thenReturn(Anchor.PersistState.PERSISTED);
+ executor.simulateSleepExecutingAllTasks(AnchorEntityImpl.PERSIST_STATE_CHECK_DELAY);
+
+ assertThat(anchorEntity.getPersistState()).isEqualTo(PersistState.PERSISTED);
+ verify(persistStateChangeListener).onPersistStateChanged(PersistState.PERSISTED);
+ Mockito.clearInvocations(anchor);
+ Mockito.clearInvocations(persistStateChangeListener);
+
+ executor.simulateSleepExecutingAllTasks(AnchorEntityImpl.PERSIST_STATE_CHECK_DELAY);
+ verify(anchor, never()).getPersistState();
+ verify(persistStateChangeListener, never()).onPersistStateChanged(any());
+ }
+
+ @Test
+ public void updatePersistState_noQueryForPersistStateAfterDispose() throws Exception {
+ AnchorEntityImpl anchorEntity = createInitAndPersistAnchorEntity();
+ when(anchor.getPersistState()).thenReturn(Anchor.PersistState.PERSIST_PENDING);
+ executor.simulateSleepExecutingAllTasks(AnchorEntityImpl.PERSIST_STATE_CHECK_DELAY);
+ verify(anchor).getPersistState();
+
+ executor.simulateSleepExecutingAllTasks(AnchorEntityImpl.PERSIST_STATE_CHECK_DELAY);
+ verify(anchor, times(2)).getPersistState();
+
+ Mockito.clearInvocations(anchor);
+ anchorEntity.dispose();
+ executor.simulateSleepExecutingAllTasks(AnchorEntityImpl.PERSIST_STATE_CHECK_DELAY);
+ verify(anchor, never()).getPersistState();
+ }
+
+ @Test
+ public void createAnchorEntityFromPlane_returnsAnchorEntity() throws Exception {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(plane.createAnchor(any(), any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ AnchorEntityImpl anchorEntity = createAnchorEntityFromPlane();
+
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.ANCHORED);
+ assertThat(((FakeNode) anchorEntity.getNode()).getName())
+ .isEqualTo(AnchorEntityImpl.ANCHOR_NODE_NAME);
+ assertThat(((FakeNode) anchorEntity.getNode()).getAnchorId()).isEqualTo(sharedAnchorToken);
+ assertThat(anchorEntity.getPersistState()).isEqualTo(PersistState.PERSIST_NOT_REQUESTED);
+ assertThat(((FakeNode) anchorEntity.getNode()).getParent())
+ .isEqualTo(activitySpace.getNode());
+ }
+
+ @Test
+ public void createAnchorEntityFromPlane_failureToAnchor_hasErrorState() throws Exception {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(plane.createAnchor(any(), any())).thenReturn(null);
+
+ AnchorEntityImpl anchorEntity = createAnchorEntityFromPlane();
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.ERROR);
+ assertThat(((FakeNode) anchorEntity.getNode()).getParent()).isEqualTo(null);
+ }
+
+ @Test
+ public void createAnchorEntityFromPerceptionAnchor_nativePointerMatches() throws Exception {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ FakeExportableAnchor fakeAnchor =
+ new FakeExportableAnchor(
+ NATIVE_POINTER,
+ sharedAnchorToken,
+ new Pose(),
+ TrackingState.Tracking,
+ PersistenceState.NotPersisted,
+ null);
+ androidx.xr.arcore.Anchor perceptionAnchor = new androidx.xr.arcore.Anchor(fakeAnchor);
+
+ AnchorEntityImpl anchorEntity = createAnchorEntityFromPerceptionAnchor(perceptionAnchor);
+
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.nativePointer()).isEqualTo(NATIVE_POINTER);
+ }
+
+ @Test
+ public void createAnchorEntityFromPerceptionAnchor_returnsAnchorEntity() throws Exception {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+
+ FakeExportableAnchor fakeAnchor =
+ new FakeExportableAnchor(
+ NATIVE_POINTER,
+ sharedAnchorToken,
+ new Pose(),
+ TrackingState.Tracking,
+ PersistenceState.NotPersisted,
+ null);
+ androidx.xr.arcore.Anchor perceptionAnchor = new androidx.xr.arcore.Anchor(fakeAnchor);
+
+ AnchorEntityImpl anchorEntity = createAnchorEntityFromPerceptionAnchor(perceptionAnchor);
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.ANCHORED);
+ assertThat(((FakeNode) anchorEntity.getNode()).getName())
+ .isEqualTo(AnchorEntityImpl.ANCHOR_NODE_NAME);
+ assertThat(((FakeNode) anchorEntity.getNode()).getAnchorId()).isEqualTo(sharedAnchorToken);
+ assertThat(anchorEntity.getPersistState()).isEqualTo(PersistState.PERSIST_NOT_REQUESTED);
+ assertThat(((FakeNode) anchorEntity.getNode()).getParent())
+ .isEqualTo(activitySpace.getNode());
+ }
+
+ @Test
+ public void createAnchorEntityFromPerceptionAnchor_failureToAnchor_hasErrorState()
+ throws Exception {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+
+ FakeExportableAnchor fakeAnchor =
+ new FakeExportableAnchor(
+ NATIVE_POINTER,
+ null,
+ new Pose(),
+ TrackingState.Tracking,
+ PersistenceState.NotPersisted,
+ null);
+ androidx.xr.arcore.Anchor perceptionAnchor = new androidx.xr.arcore.Anchor(fakeAnchor);
+
+ AnchorEntityImpl anchorEntity = createAnchorEntityFromPerceptionAnchor(perceptionAnchor);
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.ERROR);
+ assertThat(((FakeNode) anchorEntity.getNode()).getParent()).isEqualTo(null);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/AudioTrackExtensionsWrapperImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/AudioTrackExtensionsWrapperImplTest.java
new file mode 100644
index 0000000..a782e82
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/AudioTrackExtensionsWrapperImplTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.media.AudioTrack;
+
+import androidx.xr.extensions.media.PointSourceAttributes;
+import androidx.xr.extensions.media.SoundFieldAttributes;
+import androidx.xr.extensions.media.SpatializerExtensions;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.scenecore.JxrPlatformAdapter;
+import androidx.xr.scenecore.JxrPlatformAdapter.AudioTrackExtensionsWrapper;
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatializerConstants;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeAudioTrackExtensions;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeSpatialAudioExtensions;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class AudioTrackExtensionsWrapperImplTest {
+
+ FakeXrExtensions fakeXrExtensions;
+ FakeSpatialAudioExtensions fakeSpatialAudioExtensions;
+ FakeAudioTrackExtensions fakeAudioTrackExtensions;
+
+ private EntityManager entityManager;
+
+ @Mock private AudioTrack.Builder builder;
+
+ @Before
+ public void setUp() {
+ fakeXrExtensions = new FakeXrExtensions();
+ fakeSpatialAudioExtensions = fakeXrExtensions.fakeSpatialAudioExtensions;
+ fakeAudioTrackExtensions = new FakeAudioTrackExtensions();
+
+ fakeSpatialAudioExtensions.setFakeAudioTrackExtensions(fakeAudioTrackExtensions);
+
+ entityManager = new EntityManager();
+ }
+
+ @Test
+ public void setPointSourceAttr_callsExtensionsSetPointSourceAttr() {
+ Node fakeNode = new FakeXrExtensions().createNode();
+ AndroidXrEntity entity = mock(AndroidXrEntity.class);
+ when(entity.getNode()).thenReturn(fakeNode);
+
+ JxrPlatformAdapter.PointSourceAttributes expectedRtAttr =
+ new JxrPlatformAdapter.PointSourceAttributes(entity);
+
+ AudioTrackExtensionsWrapper wrapper =
+ new AudioTrackExtensionsWrapperImpl(fakeAudioTrackExtensions, entityManager);
+ AudioTrack.Builder actual = wrapper.setPointSourceAttributes(builder, expectedRtAttr);
+
+ assertThat(actual).isEqualTo(builder);
+ assertThat(fakeAudioTrackExtensions.getPointSourceAttributes().getNode())
+ .isEqualTo(fakeNode);
+ }
+
+ @Test
+ public void setSoundFieldAttr_callsExtensionsSetSoundFieldAttr() {
+ int expectedAmbisonicOrder = SpatializerExtensions.AMBISONICS_ORDER_THIRD_ORDER;
+ JxrPlatformAdapter.SoundFieldAttributes expectedRtAttr =
+ new JxrPlatformAdapter.SoundFieldAttributes(
+ JxrPlatformAdapter.SpatializerConstants.AMBISONICS_ORDER_THIRD_ORDER);
+
+ AudioTrackExtensionsWrapper wrapper =
+ new AudioTrackExtensionsWrapperImpl(fakeAudioTrackExtensions, entityManager);
+
+ AudioTrack.Builder actual = wrapper.setSoundFieldAttributes(builder, expectedRtAttr);
+
+ assertThat(actual).isEqualTo(builder);
+ assertThat(fakeAudioTrackExtensions.getSoundFieldAttributes().getAmbisonicsOrder())
+ .isEqualTo(expectedAmbisonicOrder);
+ }
+
+ @Test
+ public void getPointSourceAttributes_callsExtensionsGetPointSourceAttributes() {
+ AudioTrack track = mock(AudioTrack.class);
+
+ Node fakeNode = new FakeXrExtensions().createNode();
+ AndroidXrEntity entity = mock(AndroidXrEntity.class);
+ when(entity.getNode()).thenReturn(fakeNode);
+ entityManager.setEntityForNode(fakeNode, entity);
+
+ fakeAudioTrackExtensions.setPointSourceAttributes(
+ new PointSourceAttributes.Builder().setNode(fakeNode).build());
+
+ JxrPlatformAdapter.PointSourceAttributes expectedRtAttr =
+ new JxrPlatformAdapter.PointSourceAttributes(entity);
+ AudioTrackExtensionsWrapper wrapper =
+ new AudioTrackExtensionsWrapperImpl(fakeAudioTrackExtensions, entityManager);
+
+ JxrPlatformAdapter.PointSourceAttributes actual = wrapper.getPointSourceAttributes(track);
+
+ assertThat(actual.getEntity()).isEqualTo(expectedRtAttr.getEntity());
+ }
+
+ @Test
+ public void getPointSourceAttributes_returnsNullIfNotInExtensions() {
+ AudioTrack track = mock(AudioTrack.class);
+
+ AudioTrackExtensionsWrapper wrapper =
+ new AudioTrackExtensionsWrapperImpl(fakeAudioTrackExtensions, entityManager);
+
+ JxrPlatformAdapter.PointSourceAttributes actual = wrapper.getPointSourceAttributes(track);
+
+ assertThat(actual).isNull();
+ }
+
+ @Test
+ public void getSoundFieldAttributes_callsExtensionsGetSoundFieldAttributes() {
+ AudioTrack track = mock(AudioTrack.class);
+
+ fakeAudioTrackExtensions.setSoundFieldAttributes(
+ new SoundFieldAttributes.Builder()
+ .setAmbisonicsOrder(SpatializerExtensions.AMBISONICS_ORDER_THIRD_ORDER)
+ .build());
+
+ JxrPlatformAdapter.SoundFieldAttributes expectedRtAttr =
+ new JxrPlatformAdapter.SoundFieldAttributes(
+ SpatializerConstants.AMBISONICS_ORDER_THIRD_ORDER);
+ AudioTrackExtensionsWrapper wrapper =
+ new AudioTrackExtensionsWrapperImpl(fakeAudioTrackExtensions, entityManager);
+
+ JxrPlatformAdapter.SoundFieldAttributes actual = wrapper.getSoundFieldAttributes(track);
+
+ assertThat(actual.getAmbisonicsOrder()).isEqualTo(expectedRtAttr.getAmbisonicsOrder());
+ }
+
+ @Test
+ public void getSoundFieldAttributes_returnsNullIfNotInExtensions() {
+ AudioTrack track = mock(AudioTrack.class);
+
+ AudioTrackExtensionsWrapper wrapper =
+ new AudioTrackExtensionsWrapperImpl(fakeAudioTrackExtensions, entityManager);
+
+ JxrPlatformAdapter.SoundFieldAttributes actual = wrapper.getSoundFieldAttributes(track);
+
+ assertThat(actual).isNull();
+ }
+
+ @Test
+ public void getSourceType_returnsFromExtensions() {
+ AudioTrack track = mock(AudioTrack.class);
+
+ int expected = SpatializerConstants.SOURCE_TYPE_SOUND_FIELD;
+
+ fakeAudioTrackExtensions.setSourceType(expected);
+ AudioTrackExtensionsWrapper wrapper =
+ new AudioTrackExtensionsWrapperImpl(fakeAudioTrackExtensions, entityManager);
+
+ int actualSourceType = wrapper.getSpatialSourceType(track);
+ assertThat(actualSourceType).isEqualTo(expected);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/CameraViewActivityPoseImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/CameraViewActivityPoseImplTest.java
new file mode 100644
index 0000000..b837469
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/CameraViewActivityPoseImplTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static androidx.xr.runtime.testing.math.MathAssertions.assertPose;
+import static androidx.xr.runtime.testing.math.MathAssertions.assertVector3;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import androidx.xr.runtime.math.Matrix4;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Quaternion;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter.CameraViewActivityPose;
+import androidx.xr.scenecore.JxrPlatformAdapter.CameraViewActivityPose.CameraType;
+import androidx.xr.scenecore.JxrPlatformAdapter.CameraViewActivityPose.Fov;
+import androidx.xr.scenecore.impl.perception.PerceptionLibrary;
+import androidx.xr.scenecore.impl.perception.Session;
+import androidx.xr.scenecore.impl.perception.ViewProjection;
+import androidx.xr.scenecore.impl.perception.ViewProjections;
+import androidx.xr.scenecore.testing.FakeScheduledExecutorService;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public final class CameraViewActivityPoseImplTest {
+
+ private final AndroidXrEntity activitySpaceRoot = Mockito.mock(AndroidXrEntity.class);
+ private final FakeXrExtensions fakeExtensions = new FakeXrExtensions();
+ private final PerceptionLibrary perceptionLibrary = Mockito.mock(PerceptionLibrary.class);
+ private final Session session = Mockito.mock(Session.class);
+ private final FakeScheduledExecutorService executor = new FakeScheduledExecutorService();
+ private final ActivitySpaceImpl activitySpace =
+ new ActivitySpaceImpl(
+ fakeExtensions.createNode(),
+ fakeExtensions,
+ new EntityManager(),
+ () -> fakeExtensions.fakeSpatialState,
+ executor);
+
+ /** Creates a CameraViewActivityPoseImpl. */
+ private CameraViewActivityPoseImpl createCameraViewActivityPose(@CameraType int cameraType) {
+ return new CameraViewActivityPoseImpl(
+ cameraType, activitySpace, activitySpaceRoot, perceptionLibrary);
+ }
+
+ @Test
+ public void getCameraType_returnsCameraType() {
+ CameraViewActivityPoseImpl cameraActivityPoseLeft =
+ createCameraViewActivityPose(CameraViewActivityPose.CAMERA_TYPE_LEFT_EYE);
+ assertThat(cameraActivityPoseLeft.getCameraType())
+ .isEqualTo(CameraViewActivityPose.CAMERA_TYPE_LEFT_EYE);
+
+ CameraViewActivityPoseImpl cameraActivityPoseRight =
+ createCameraViewActivityPose(CameraViewActivityPose.CAMERA_TYPE_RIGHT_EYE);
+ assertThat(cameraActivityPoseRight.getCameraType())
+ .isEqualTo(CameraViewActivityPose.CAMERA_TYPE_RIGHT_EYE);
+ }
+
+ @Test
+ public void getFov_returnsCorrectFov() {
+ Fov fovLeft = new Fov(1, 2, 3, 4);
+ Fov fovRight = new Fov(5, 6, 7, 8);
+
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ ViewProjection viewProjectionLeft =
+ new ViewProjection(
+ RuntimeUtils.poseToPerceptionPose(new Pose()),
+ RuntimeUtils.perceptionFovFromFov(fovLeft));
+ ViewProjection viewProjectionRight =
+ new ViewProjection(
+ RuntimeUtils.poseToPerceptionPose(new Pose()),
+ RuntimeUtils.perceptionFovFromFov(fovRight));
+ when(session.getStereoViews())
+ .thenReturn(new ViewProjections(viewProjectionLeft, viewProjectionRight));
+
+ CameraViewActivityPoseImpl cameraActivityPoseLeft =
+ createCameraViewActivityPose(CameraViewActivityPose.CAMERA_TYPE_LEFT_EYE);
+ assertThat(cameraActivityPoseLeft.getFov().angleLeft).isEqualTo(fovLeft.angleLeft);
+ assertThat(cameraActivityPoseLeft.getFov().angleRight).isEqualTo(fovLeft.angleRight);
+ assertThat(cameraActivityPoseLeft.getFov().angleUp).isEqualTo(fovLeft.angleUp);
+ assertThat(cameraActivityPoseLeft.getFov().angleDown).isEqualTo(fovLeft.angleDown);
+
+ CameraViewActivityPoseImpl cameraActivityPoseRight =
+ createCameraViewActivityPose(CameraViewActivityPose.CAMERA_TYPE_RIGHT_EYE);
+ assertThat(cameraActivityPoseRight.getFov().angleLeft).isEqualTo(fovRight.angleLeft);
+ assertThat(cameraActivityPoseRight.getFov().angleRight).isEqualTo(fovRight.angleRight);
+ assertThat(cameraActivityPoseRight.getFov().angleUp).isEqualTo(fovRight.angleUp);
+ assertThat(cameraActivityPoseRight.getFov().angleDown).isEqualTo(fovRight.angleDown);
+ }
+
+ @Test
+ public void transformPoseTo_returnsCorrectPose() {
+ // Set the activity space to the root of the underlying OpenXR reference space.
+ this.activitySpace.setOpenXrReferenceSpacePose(Matrix4.Identity);
+ Pose poseLeft = new Pose(new Vector3(1, 2, 3), new Quaternion(1, 0, 0, 0));
+ Pose poseRight = new Pose(new Vector3(4, 5, 6), new Quaternion(0, 1, 0, 0));
+ Fov fov = new Fov(0, 0, 0, 0);
+
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ ViewProjection viewProjectionLeft =
+ new ViewProjection(
+ RuntimeUtils.poseToPerceptionPose(poseLeft),
+ RuntimeUtils.perceptionFovFromFov(fov));
+ ViewProjection viewProjectionRight =
+ new ViewProjection(
+ RuntimeUtils.poseToPerceptionPose(poseRight),
+ RuntimeUtils.perceptionFovFromFov(fov));
+ when(session.getStereoViews())
+ .thenReturn(new ViewProjections(viewProjectionLeft, viewProjectionRight));
+
+ CameraViewActivityPoseImpl cameraActivityPoseLeft =
+ createCameraViewActivityPose(CameraViewActivityPose.CAMERA_TYPE_LEFT_EYE);
+
+ assertPose(cameraActivityPoseLeft.transformPoseTo(new Pose(), activitySpace), poseLeft);
+
+ CameraViewActivityPoseImpl cameraActivityPoseRight =
+ createCameraViewActivityPose(CameraViewActivityPose.CAMERA_TYPE_RIGHT_EYE);
+
+ assertPose(cameraActivityPoseRight.transformPoseTo(new Pose(), activitySpace), poseRight);
+ }
+
+ @Test
+ public void getActivitySpaceScale_returnsInverseOfActivitySpaceWorldScale() throws Exception {
+ float activitySpaceScale = 5f;
+ this.activitySpace.setOpenXrReferenceSpacePose(Matrix4.fromScale(activitySpaceScale));
+ CameraViewActivityPoseImpl cameraActivityPoseLeft =
+ createCameraViewActivityPose(CameraViewActivityPose.CAMERA_TYPE_LEFT_EYE);
+ assertVector3(
+ cameraActivityPoseLeft.getActivitySpaceScale(),
+ new Vector3(1f, 1f, 1f).div(activitySpaceScale));
+
+ CameraViewActivityPoseImpl cameraActivityPoseRight =
+ createCameraViewActivityPose(CameraViewActivityPose.CAMERA_TYPE_RIGHT_EYE);
+ assertVector3(
+ cameraActivityPoseRight.getActivitySpaceScale(),
+ new Vector3(1f, 1f, 1f).div(activitySpaceScale));
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/EntityManagerTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/EntityManagerTest.java
new file mode 100644
index 0000000..4674fe0
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/EntityManagerTest.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.Binder;
+import android.os.SystemClock;
+import android.view.Display;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+
+import androidx.xr.extensions.node.Node;
+import androidx.xr.runtime.math.Matrix4;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.scenecore.JxrPlatformAdapter.ActivityPanelEntity;
+import androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity;
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.GltfEntity;
+import androidx.xr.scenecore.JxrPlatformAdapter.GltfModelResource;
+import androidx.xr.scenecore.JxrPlatformAdapter.PanelEntity;
+import androidx.xr.scenecore.JxrPlatformAdapter.PixelDimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.PlaneSemantic;
+import androidx.xr.scenecore.JxrPlatformAdapter.PlaneType;
+import androidx.xr.scenecore.impl.perception.PerceptionLibrary;
+import androidx.xr.scenecore.impl.perception.Session;
+import androidx.xr.scenecore.testing.FakeImpressApi;
+import androidx.xr.scenecore.testing.FakeScheduledExecutorService;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+
+import com.google.androidxr.splitengine.SplitEngineSubspaceManager;
+import com.google.ar.imp.view.splitengine.ImpSplitEngineRenderer;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.android.controller.ActivityController;
+
+import java.util.Objects;
+
+@RunWith(RobolectricTestRunner.class)
+public class EntityManagerTest {
+
+ private static final int VGA_WIDTH = 640;
+ private static final int VGA_HEIGHT = 480;
+ private final FakeXrExtensions fakeExtensions = new FakeXrExtensions();
+ private final FakeImpressApi fakeImpressApi = new FakeImpressApi();
+ private final FakeScheduledExecutorService fakeExecutor = new FakeScheduledExecutorService();
+ private final PerceptionLibrary perceptionLibrary = mock(PerceptionLibrary.class);
+ private final Session session = mock(Session.class);
+ private final AndroidXrEntity activitySpaceRoot = mock(AndroidXrEntity.class);
+ private final FakeScheduledExecutorService executor = new FakeScheduledExecutorService();
+ private final Node panelEntityNode = fakeExtensions.createNode();
+ private final Node anchorEntityNode = fakeExtensions.createNode();
+ private final EntityManager entityManager = new EntityManager();
+ private final SplitEngineSubspaceManager splitEngineSubspaceManager =
+ Mockito.mock(SplitEngineSubspaceManager.class);
+ private final ImpSplitEngineRenderer splitEngineRenderer =
+ Mockito.mock(ImpSplitEngineRenderer.class);
+ private Node contentLessEntityNode;
+ private Node gltfEntityNode;
+ private Activity activity;
+ private JxrPlatformAdapterAxr runtime;
+ private ActivitySpaceImpl activitySpace;
+
+ @Before
+ public void setUp() {
+ try (ActivityController<Activity> activityController =
+ Robolectric.buildActivity(Activity.class)) {
+ activity = activityController.create().start().get();
+ }
+ when(perceptionLibrary.initSession(eq(activity), anyInt(), eq(fakeExecutor)))
+ .thenReturn(immediateFuture(session));
+ runtime =
+ JxrPlatformAdapterAxr.create(
+ activity,
+ fakeExecutor,
+ fakeExtensions,
+ fakeImpressApi,
+ entityManager,
+ perceptionLibrary,
+ splitEngineSubspaceManager,
+ splitEngineRenderer,
+ /* useSplitEngine= */ false);
+ Node taskNode = fakeExtensions.createNode();
+ this.activitySpace =
+ new ActivitySpaceImpl(
+ taskNode,
+ fakeExtensions,
+ entityManager,
+ () -> fakeExtensions.fakeSpatialState,
+ executor);
+ long currentTimeMillis = 1000000000L;
+ SystemClock.setCurrentTimeMillis(currentTimeMillis);
+
+ // By default, set the activity space to the root of the underlying OpenXR reference space.
+ this.activitySpace.setOpenXrReferenceSpacePose(Matrix4.Identity);
+ }
+
+ @Test
+ public void creatingEntity_addsEntityToEntityManager() throws Exception {
+ GltfEntity gltfEntity = createGltfEntity();
+ PanelEntity panelEntity = createPanelEntity();
+ Entity contentlessEntity = createContentlessEntity();
+ AnchorEntity anchorEntity = createAnchorEntity();
+ ActivityPanelEntity activityPanelEntity = createActivityPanelEntity();
+
+ // Entity manager also contains the main panel entity and activity space, which are created
+ // when
+ // the runtime is created.
+ assertThat(entityManager.getAllEntities().size()).isAtLeast(5);
+ assertThat(entityManager.getAllEntities())
+ .containsAtLeast(
+ gltfEntity,
+ panelEntity,
+ contentlessEntity,
+ anchorEntity,
+ activityPanelEntity);
+ }
+
+ @Test
+ public void getEntityForNode_returnsEntity() throws Exception {
+ GltfEntity gltfEntity = createGltfEntity();
+ PanelEntity panelEntity = createPanelEntity();
+ Entity contentlessEntity = createContentlessEntity();
+ AnchorEntity anchorEntity = createAnchorEntity();
+ Node testNode = fakeExtensions.createNode();
+
+ assertThat(entityManager.getEntityForNode(gltfEntityNode)).isEqualTo(gltfEntity);
+ assertThat(entityManager.getEntityForNode(panelEntityNode)).isEqualTo(panelEntity);
+ assertThat(entityManager.getEntityForNode(contentLessEntityNode))
+ .isEqualTo(contentlessEntity);
+ assertThat(entityManager.getEntityForNode(anchorEntityNode)).isEqualTo(anchorEntity);
+ assertThat(entityManager.getEntityForNode(testNode)).isNull();
+ }
+
+ @Test
+ public void getEntityByType_returnsEntityOfType() throws Exception {
+ GltfEntity gltfEntity = createGltfEntity();
+ PanelEntity panelEntity = createPanelEntity();
+ Entity contentlessEntity = createContentlessEntity();
+ AnchorEntity anchorEntity = createAnchorEntity();
+ ActivityPanelEntity activityPanelEntity = createActivityPanelEntity();
+
+ assertThat(entityManager.getEntitiesOfType(GltfEntity.class)).containsExactly(gltfEntity);
+ // MainPanel is also a PanelEntity.
+ assertThat(entityManager.getEntitiesOfType(PanelEntity.class)).contains(panelEntity);
+ // Base class of all entities.
+ assertThat(entityManager.getEntitiesOfType(Entity.class)).contains(contentlessEntity);
+ assertThat(entityManager.getEntitiesOfType(AnchorEntity.class))
+ .containsExactly(anchorEntity);
+ assertThat(entityManager.getEntitiesOfType(ActivityPanelEntity.class))
+ .containsExactly(activityPanelEntity);
+ }
+
+ @Test
+ public void removeEntity_removesFromEntityManager() throws Exception {
+ GltfEntity gltfEntity = createGltfEntity();
+ PanelEntity panelEntity = createPanelEntity();
+ Entity contentlessEntity = createContentlessEntity();
+ AnchorEntity anchorEntity = createAnchorEntity();
+ ActivityPanelEntity activityPanelEntity = createActivityPanelEntity();
+
+ assertThat(entityManager.getAllEntities().size()).isAtLeast(5);
+ assertThat(entityManager.getAllEntities())
+ .containsAtLeast(
+ gltfEntity,
+ panelEntity,
+ contentlessEntity,
+ anchorEntity,
+ activityPanelEntity);
+
+ entityManager.removeEntityForNode(contentLessEntityNode);
+
+ assertThat(entityManager.getAllEntities().size()).isAtLeast(4);
+ assertThat(entityManager.getAllEntities()).doesNotContain(contentlessEntity);
+ }
+
+ @Test
+ public void disposeEntity_removesFromEntityManager() throws Exception {
+ GltfEntity gltfEntity = createGltfEntity();
+ PanelEntity panelEntity = createPanelEntity();
+ Entity contentlessEntity = createContentlessEntity();
+ AnchorEntity anchorEntity = createAnchorEntity();
+ ActivityPanelEntity activityPanelEntity = createActivityPanelEntity();
+
+ assertThat(entityManager.getAllEntities().size()).isAtLeast(5);
+ assertThat(entityManager.getAllEntities())
+ .containsAtLeast(
+ gltfEntity,
+ panelEntity,
+ contentlessEntity,
+ anchorEntity,
+ activityPanelEntity);
+
+ contentlessEntity.dispose();
+
+ assertThat(entityManager.getAllEntities().size()).isAtLeast(4);
+ assertThat(entityManager.getAllEntities()).doesNotContain(contentlessEntity);
+ }
+
+ @Test
+ public void clearEntityManager_removesAllEntityFromEntityManager() throws Exception {
+ GltfEntity gltfEntity = createGltfEntity();
+ PanelEntity panelEntity = createPanelEntity();
+ Entity contentlessEntity = createContentlessEntity();
+ AnchorEntity anchorEntity = createAnchorEntity();
+ ActivityPanelEntity activityPanelEntity = createActivityPanelEntity();
+
+ assertThat(entityManager.getAllEntities().size()).isAtLeast(5);
+ assertThat(entityManager.getAllEntities())
+ .containsAtLeast(
+ gltfEntity,
+ panelEntity,
+ contentlessEntity,
+ anchorEntity,
+ activityPanelEntity);
+
+ entityManager.clear();
+
+ assertThat(entityManager.getAllEntities()).isEmpty();
+ }
+
+ private GltfEntity createGltfEntity() throws Exception {
+ ListenableFuture<GltfModelResource> modelFuture =
+ runtime.loadGltfByAssetName("FakeAsset.glb");
+ assertThat(modelFuture).isNotNull();
+ GltfModelResource model = modelFuture.get();
+ GltfEntityImpl gltfEntity =
+ new GltfEntityImpl(
+ (GltfModelResourceImpl) model,
+ activitySpaceRoot,
+ fakeExtensions,
+ entityManager,
+ executor);
+ gltfEntityNode = gltfEntity.getNode();
+ entityManager.setEntityForNode(gltfEntityNode, gltfEntity);
+ return gltfEntity;
+ }
+
+ private PanelEntity createPanelEntity() {
+ Display display = activity.getSystemService(DisplayManager.class).getDisplays()[0];
+ Context displayContext = activity.createDisplayContext(display);
+ View view = new View(displayContext);
+ view.setLayoutParams(new LayoutParams(VGA_WIDTH, VGA_HEIGHT));
+ SurfaceControlViewHost surfaceControlViewHost =
+ new SurfaceControlViewHost(
+ displayContext,
+ Objects.requireNonNull(displayContext.getDisplay()),
+ new Binder());
+ surfaceControlViewHost.setView(view, VGA_WIDTH, VGA_HEIGHT);
+ PanelEntityImpl panelEntity =
+ new PanelEntityImpl(
+ panelEntityNode,
+ fakeExtensions,
+ entityManager,
+ surfaceControlViewHost,
+ new PixelDimensions(VGA_WIDTH, VGA_HEIGHT),
+ executor);
+ entityManager.setEntityForNode(panelEntityNode, panelEntity);
+ return panelEntity;
+ }
+
+ private Entity createContentlessEntity() {
+ Entity contentlessEntity =
+ runtime.createEntity(new Pose(), "testContentLess", runtime.getActivitySpace());
+ contentLessEntityNode = ((AndroidXrEntity) contentlessEntity).getNode();
+ entityManager.setEntityForNode(contentLessEntityNode, contentlessEntity);
+ return contentlessEntity;
+ }
+
+ private AnchorEntity createAnchorEntity() {
+ AnchorEntityImpl anchorEntity =
+ AnchorEntityImpl.createSemanticAnchor(
+ anchorEntityNode,
+ new Dimensions(1f, 1f, 1f),
+ PlaneType.VERTICAL,
+ PlaneSemantic.WALL,
+ null,
+ activitySpace,
+ activitySpaceRoot,
+ fakeExtensions,
+ entityManager,
+ executor,
+ perceptionLibrary);
+ entityManager.setEntityForNode(anchorEntityNode, anchorEntity);
+ return anchorEntity;
+ }
+
+ private ActivityPanelEntity createActivityPanelEntity() {
+ return runtime.createActivityPanelEntity(
+ new Pose(),
+ new PixelDimensions(VGA_WIDTH, VGA_HEIGHT),
+ "test",
+ activity,
+ activitySpace);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/InteractableComponentImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/InteractableComponentImplTest.java
new file mode 100644
index 0000000..2995e1a
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/InteractableComponentImplTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+
+import androidx.xr.extensions.node.Vec3;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.InputEventListener;
+import androidx.xr.scenecore.JxrPlatformAdapter.InteractableComponent;
+import androidx.xr.scenecore.impl.perception.PerceptionLibrary;
+import androidx.xr.scenecore.impl.perception.Session;
+import androidx.xr.scenecore.testing.FakeImpressApi;
+import androidx.xr.scenecore.testing.FakeScheduledExecutorService;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeInputEvent;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeNode;
+
+import com.google.androidxr.splitengine.SplitEngineSubspaceManager;
+import com.google.ar.imp.view.splitengine.ImpSplitEngineRenderer;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.android.controller.ActivityController;
+
+import java.util.concurrent.Executor;
+
+@RunWith(RobolectricTestRunner.class)
+public class InteractableComponentImplTest {
+ private final ActivityController<Activity> activityController =
+ Robolectric.buildActivity(Activity.class);
+ private final Activity activity = activityController.create().start().get();
+ private final FakeScheduledExecutorService fakeExecutor = new FakeScheduledExecutorService();
+ private final PerceptionLibrary perceptionLibrary = mock(PerceptionLibrary.class);
+ private final FakeXrExtensions fakeExtensions = new FakeXrExtensions();
+ private final FakeImpressApi fakeImpressApi = new FakeImpressApi();
+ private JxrPlatformAdapterAxr fakeRuntime;
+ SplitEngineSubspaceManager splitEngineSubspaceManager =
+ Mockito.mock(SplitEngineSubspaceManager.class);
+ ImpSplitEngineRenderer splitEngineRenderer = Mockito.mock(ImpSplitEngineRenderer.class);
+
+ private Entity createTestEntity() {
+ when(perceptionLibrary.initSession(eq(activity), anyInt(), eq(fakeExecutor)))
+ .thenReturn(immediateFuture(mock(Session.class)));
+ fakeRuntime =
+ JxrPlatformAdapterAxr.create(
+ activity,
+ fakeExecutor,
+ fakeExtensions,
+ fakeImpressApi,
+ new EntityManager(),
+ perceptionLibrary,
+ splitEngineSubspaceManager,
+ splitEngineRenderer,
+ /* useSplitEngine= */ false);
+ return fakeRuntime.createEntity(new Pose(), "test", fakeRuntime.getActivitySpace());
+ }
+
+ @Test
+ public void addInteractableComponent_addsListenerToNode() {
+ Entity entity = createTestEntity();
+ Executor executor = directExecutor();
+ InputEventListener inputEventListener = mock(InputEventListener.class);
+ InteractableComponent interactableComponent =
+ new InteractableComponentImpl(executor, inputEventListener);
+ assertThat(entity.addComponent(interactableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+
+ assertThat(node.getListener()).isNotNull();
+ assertThat(node.getExecutor()).isEqualTo(fakeExecutor);
+
+ FakeInputEvent inputEvent = new FakeInputEvent();
+ inputEvent.setOrigin(new Vec3(0, 0, 0));
+ inputEvent.setDirection(new Vec3(1, 1, 1));
+ node.sendInputEvent(inputEvent);
+ fakeExecutor.runAll();
+
+ assertThat(((AndroidXrEntity) entity).inputEventListenerMap).isNotEmpty();
+ verify(inputEventListener).onInputEvent(any());
+ }
+
+ @Test
+ public void removeInteractableComponent_removesListenerFromNode() {
+ Entity entity = createTestEntity();
+ Executor executor = directExecutor();
+ InputEventListener inputEventListener = mock(InputEventListener.class);
+ InteractableComponent interactableComponent =
+ new InteractableComponentImpl(executor, inputEventListener);
+ assertThat(entity.addComponent(interactableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+
+ assertThat(node.getListener()).isNotNull();
+ assertThat(node.getExecutor()).isEqualTo(fakeExecutor);
+
+ FakeInputEvent inputEvent = new FakeInputEvent();
+ inputEvent.setOrigin(new Vec3(0, 0, 0));
+ inputEvent.setDirection(new Vec3(1, 1, 1));
+ node.sendInputEvent(inputEvent);
+ fakeExecutor.runAll();
+
+ assertThat(((AndroidXrEntity) entity).inputEventListenerMap).isNotEmpty();
+ verify(inputEventListener).onInputEvent(any());
+
+ entity.removeComponent(interactableComponent);
+ assertThat(node.getListener()).isNull();
+ assertThat(node.getExecutor()).isNull();
+ }
+
+ @Test
+ public void interactableComponent_canAttachOnlyOnce() {
+ Entity entity = createTestEntity();
+ Entity entity2 = createTestEntity();
+ Executor executor = directExecutor();
+ InputEventListener inputEventListener = mock(InputEventListener.class);
+ InteractableComponent interactableComponent =
+ new InteractableComponentImpl(executor, inputEventListener);
+ assertThat(entity.addComponent(interactableComponent)).isTrue();
+ assertThat(entity2.addComponent(interactableComponent)).isFalse();
+ }
+
+ @Test
+ public void interactableComponent_canAttachAgainAfterDetach() {
+ Entity entity = createTestEntity();
+ Executor executor = directExecutor();
+ InputEventListener inputEventListener = mock(InputEventListener.class);
+ InteractableComponent interactableComponent =
+ new InteractableComponentImpl(executor, inputEventListener);
+ assertThat(entity.addComponent(interactableComponent)).isTrue();
+ entity.removeComponent(interactableComponent);
+ assertThat(entity.addComponent(interactableComponent)).isTrue();
+ }
+
+ @Test
+ public void interactableComponent_enablesColliderForGltfEntity() {
+ GltfEntityImplSplitEngine gltfEntity = mock(GltfEntityImplSplitEngine.class);
+ Executor executor = directExecutor();
+ InputEventListener inputEventListener = mock(InputEventListener.class);
+ InteractableComponent interactableComponent =
+ new InteractableComponentImpl(executor, inputEventListener);
+ assertThat(interactableComponent.onAttach(gltfEntity)).isTrue();
+ verify(gltfEntity).setColliderEnabled(true);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java
new file mode 100644
index 0000000..3d30602
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java
@@ -0,0 +1,2269 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static androidx.xr.runtime.testing.math.MathAssertions.assertPose;
+import static androidx.xr.runtime.testing.math.MathAssertions.assertRotation;
+import static androidx.xr.runtime.testing.math.MathAssertions.assertVector3;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.view.Display;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+
+import androidx.xr.extensions.environment.EnvironmentVisibilityState;
+import androidx.xr.extensions.environment.PassthroughVisibilityState;
+import androidx.xr.extensions.node.Mat4f;
+import androidx.xr.extensions.node.ReformOptions;
+import androidx.xr.extensions.node.Vec3;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Quaternion;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter;
+import androidx.xr.scenecore.JxrPlatformAdapter.ActivitySpace;
+import androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity;
+import androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity.State;
+import androidx.xr.scenecore.JxrPlatformAdapter.AnchorPlacement;
+import androidx.xr.scenecore.JxrPlatformAdapter.CameraViewActivityPose;
+import androidx.xr.scenecore.JxrPlatformAdapter.Component;
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.ExrImageResource;
+import androidx.xr.scenecore.JxrPlatformAdapter.GltfEntity;
+import androidx.xr.scenecore.JxrPlatformAdapter.GltfModelResource;
+import androidx.xr.scenecore.JxrPlatformAdapter.HeadActivityPose;
+import androidx.xr.scenecore.JxrPlatformAdapter.InputEvent;
+import androidx.xr.scenecore.JxrPlatformAdapter.InputEventListener;
+import androidx.xr.scenecore.JxrPlatformAdapter.InteractableComponent;
+import androidx.xr.scenecore.JxrPlatformAdapter.LoggingEntity;
+import androidx.xr.scenecore.JxrPlatformAdapter.MovableComponent;
+import androidx.xr.scenecore.JxrPlatformAdapter.PanelEntity;
+import androidx.xr.scenecore.JxrPlatformAdapter.PixelDimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.PlaneSemantic;
+import androidx.xr.scenecore.JxrPlatformAdapter.PlaneType;
+import androidx.xr.scenecore.JxrPlatformAdapter.PointerCaptureComponent;
+import androidx.xr.scenecore.JxrPlatformAdapter.ResizableComponent;
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatialCapabilities;
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatialEnvironment;
+import androidx.xr.scenecore.JxrPlatformAdapter.StereoSurfaceEntity;
+import androidx.xr.scenecore.impl.perception.Anchor;
+import androidx.xr.scenecore.impl.perception.Fov;
+import androidx.xr.scenecore.impl.perception.PerceptionLibrary;
+import androidx.xr.scenecore.impl.perception.Plane;
+import androidx.xr.scenecore.impl.perception.Session;
+import androidx.xr.scenecore.impl.perception.ViewProjection;
+import androidx.xr.scenecore.impl.perception.ViewProjections;
+import androidx.xr.scenecore.impl.perception.exceptions.FailedToInitializeException;
+import androidx.xr.scenecore.testing.FakeImpressApi;
+import androidx.xr.scenecore.testing.FakeScheduledExecutorService;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeEnvironmentToken;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeEnvironmentVisibilityState;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeGltfModelToken;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeInputEvent;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeInputEvent.FakeHitInfo;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeNode;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakePassthroughVisibilityState;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeSpatialState;
+import androidx.xr.scenecore.testing.FakeXrExtensions.SpaceMode;
+
+import com.google.androidxr.splitengine.SplitEngineSubspaceManager;
+import com.google.androidxr.splitengine.SubspaceNode;
+import com.google.ar.imp.view.splitengine.ImpSplitEngineRenderer;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.android.controller.ActivityController;
+
+import java.time.Duration;
+import java.util.HashSet;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+@RunWith(RobolectricTestRunner.class)
+public final class JxrPlatformAdapterAxrTest {
+ private static final int OPEN_XR_REFERENCE_SPACE_TYPE = 1;
+
+ private static final int SUBSPACE_ID = 5;
+ private final FakeXrExtensions fakeExtensions = new FakeXrExtensions();
+ private final FakeImpressApi fakeImpressApi = new FakeImpressApi();
+ private final FakeNode subspaceNode = (FakeNode) fakeExtensions.createNode();
+ private final SubspaceNode expectedSubspace = new SubspaceNode(SUBSPACE_ID, subspaceNode);
+ private final FakeScheduledExecutorService fakeExecutor = new FakeScheduledExecutorService();
+ private final PerceptionLibrary perceptionLibrary = mock(PerceptionLibrary.class);
+ private final Session session = mock(Session.class);
+ private final Plane plane = mock(Plane.class);
+ private final Anchor anchor = mock(Anchor.class);
+ private final IBinder sharedAnchorToken = mock(IBinder.class);
+ SplitEngineSubspaceManager splitEngineSubspaceManager =
+ Mockito.mock(SplitEngineSubspaceManager.class);
+ ImpSplitEngineRenderer splitEngineRenderer = Mockito.mock(ImpSplitEngineRenderer.class);
+ private ActivityController<Activity> activityController;
+ private Activity activity;
+ private JxrPlatformAdapter realityCoreRuntime;
+
+ @Before
+ public void setUp() {
+ activityController = Robolectric.buildActivity(Activity.class);
+ activity = activityController.create().start().get();
+ fakeExtensions.setOpenXrWorldSpaceType(OPEN_XR_REFERENCE_SPACE_TYPE);
+ when(perceptionLibrary.initSession(activity, OPEN_XR_REFERENCE_SPACE_TYPE, fakeExecutor))
+ .thenReturn(immediateFuture(session));
+
+ realityCoreRuntime =
+ JxrPlatformAdapterAxr.create(
+ activity,
+ fakeExecutor,
+ fakeExtensions,
+ fakeImpressApi,
+ new EntityManager(),
+ perceptionLibrary,
+ splitEngineSubspaceManager,
+ splitEngineRenderer,
+ /* useSplitEngine= */ false);
+ }
+
+ GltfEntity createGltfEntity() throws Exception {
+ return createGltfEntity(new Pose());
+ }
+
+ GltfEntity createGltfEntity(Pose pose) throws Exception {
+ ListenableFuture<GltfModelResource> modelFuture =
+ realityCoreRuntime.loadGltfByAssetName("FakeAsset.glb");
+ assertThat(modelFuture).isNotNull();
+ GltfModelResource model = modelFuture.get();
+ return realityCoreRuntime.createGltfEntity(
+ pose, model, realityCoreRuntime.getActivitySpaceRootImpl());
+ }
+
+ GltfEntity createGltfEntitySplitEngine() throws Exception {
+ return createGltfEntitySplitEngine(new Pose());
+ }
+
+ GltfEntity createGltfEntitySplitEngine(Pose pose) throws Exception {
+ FakeNode rootNode = (FakeNode) fakeExtensions.createNode();
+ FakeNode taskWindowLeashNode = (FakeNode) fakeExtensions.createNode();
+
+ when(splitEngineSubspaceManager.createSubspace(anyString(), anyInt()))
+ .thenReturn(expectedSubspace);
+
+ JxrPlatformAdapterAxr realityCoreRuntimeWithSplitEngine =
+ JxrPlatformAdapterAxr.create(
+ activity,
+ fakeExecutor,
+ fakeExtensions,
+ fakeImpressApi,
+ new EntityManager(),
+ perceptionLibrary,
+ splitEngineSubspaceManager,
+ splitEngineRenderer,
+ rootNode,
+ taskWindowLeashNode,
+ /* useSplitEngine= */ true);
+
+ realityCoreRuntimeWithSplitEngine.setSplitEngineSubspaceManager(splitEngineSubspaceManager);
+
+ ListenableFuture<GltfModelResource> modelFuture =
+ realityCoreRuntimeWithSplitEngine.loadGltfByAssetNameSplitEngine("FakeAsset.glb");
+ assertThat(modelFuture).isNotNull();
+ // This resolves the transformation of the Future from a SplitEngine token to the JXR
+ // GltfModelResource. This is a hidden detail from the API surface's perspective.
+ fakeExecutor.runAll();
+ GltfModelResource model = modelFuture.get();
+ return realityCoreRuntimeWithSplitEngine.createGltfEntity(
+ pose, model, realityCoreRuntime.getActivitySpaceRootImpl());
+ }
+
+ private PanelEntity createPanelEntity() {
+ return createPanelEntity(new Pose());
+ }
+
+ /**
+ * Creates a generic panel entity instance for testing by creating a dummy view to insert into
+ * the panel, and setting the activity space as parent.
+ */
+ private PanelEntity createPanelEntity(Pose pose) {
+ Display display = activity.getSystemService(DisplayManager.class).getDisplays()[0];
+ Context displayContext = activity.createDisplayContext(display);
+ View view = new View(displayContext);
+ view.setLayoutParams(new LayoutParams(640, 480));
+ return realityCoreRuntime.createPanelEntity(
+ pose,
+ view,
+ new PixelDimensions(640, 480),
+ new Dimensions(0.5f, 0.5f, 0.5f),
+ "testPanel",
+ displayContext,
+ realityCoreRuntime.getActivitySpaceRootImpl());
+ }
+
+ private Entity createContentlessEntity() {
+ return createContentlessEntity(new Pose());
+ }
+
+ private Entity createContentlessEntity(Pose pose) {
+ return realityCoreRuntime.createEntity(
+ pose, "test", realityCoreRuntime.getActivitySpaceRootImpl());
+ }
+
+ @Test
+ public void initRuntimePerceptionFailure() {
+ ListenableFuture<Session> sessionFuture =
+ immediateFailedFuture(
+ new FailedToInitializeException("Failed to initialize a session."));
+ when(perceptionLibrary.initSession(activity, OPEN_XR_REFERENCE_SPACE_TYPE, fakeExecutor))
+ .thenReturn(sessionFuture);
+
+ realityCoreRuntime =
+ JxrPlatformAdapterAxr.create(
+ activity,
+ fakeExecutor,
+ fakeExtensions,
+ fakeImpressApi,
+ new EntityManager(),
+ perceptionLibrary,
+ splitEngineSubspaceManager,
+ splitEngineRenderer,
+ /* useSplitEngine= */ false);
+
+ // The perception library failed to initialize a session, but the runtime should still be
+ // created.
+ assertThat(realityCoreRuntime).isNotNull();
+ }
+
+ @Test
+ public void requestHomeSpaceMode_callsExtensions() {
+ realityCoreRuntime.requestHomeSpaceMode();
+ assertThat(fakeExtensions.getSpaceMode()).isEqualTo(SpaceMode.HOME_SPACE);
+ }
+
+ @Test
+ public void requestFullSpaceMode_callsExtensions() {
+ realityCoreRuntime.requestFullSpaceMode();
+ assertThat(fakeExtensions.getSpaceMode()).isEqualTo(SpaceMode.FULL_SPACE);
+ }
+
+ @Test
+ public void createLoggingEntity_returnsEntity() {
+ Pose pose = new Pose();
+ LoggingEntity loggingeEntity = realityCoreRuntime.createLoggingEntity(pose);
+ Pose updatedPose =
+ new Pose(
+ new Vector3(1f, pose.getTranslation().getY(), pose.getTranslation().getZ()),
+ pose.getRotation());
+ loggingeEntity.setPose(updatedPose);
+ }
+
+ @Test
+ public void loggingEntitySetParent() {
+ Pose pose = new Pose();
+ LoggingEntity childEntity = realityCoreRuntime.createLoggingEntity(pose);
+ LoggingEntity parentEntity = realityCoreRuntime.createLoggingEntity(pose);
+
+ childEntity.setParent(parentEntity);
+ parentEntity.addChild(childEntity);
+
+ assertThat(childEntity.getParent()).isEqualTo(parentEntity);
+ assertThat(parentEntity.getParent()).isEqualTo(null);
+ assertThat(childEntity.getChildren()).isEmpty();
+ assertThat(parentEntity.getChildren()).containsExactly(childEntity);
+ }
+
+ @Test
+ public void loggingEntityUpdateParent() {
+ Pose pose = new Pose();
+ LoggingEntity childEntity = realityCoreRuntime.createLoggingEntity(pose);
+ LoggingEntity parentEntity1 = realityCoreRuntime.createLoggingEntity(pose);
+ LoggingEntity parentEntity2 = realityCoreRuntime.createLoggingEntity(pose);
+
+ childEntity.setParent(parentEntity1);
+ assertThat(childEntity.getParent()).isEqualTo(parentEntity1);
+ assertThat(parentEntity1.getChildren()).containsExactly(childEntity);
+ assertThat(parentEntity2.getChildren()).isEmpty();
+
+ childEntity.setParent(parentEntity2);
+ assertThat(childEntity.getParent()).isEqualTo(parentEntity2);
+ assertThat(parentEntity2.getChildren()).containsExactly(childEntity);
+ assertThat(parentEntity1.getChildren()).isEmpty();
+ }
+
+ @Test
+ public void onSpatialStateChanged_setsSpatialCapabilities() {
+ realityCoreRuntime =
+ JxrPlatformAdapterAxr.create(
+ activity,
+ fakeExecutor,
+ fakeExtensions,
+ fakeImpressApi,
+ new EntityManager(),
+ perceptionLibrary,
+ splitEngineSubspaceManager,
+ splitEngineRenderer,
+ /* useSplitEngine= */ false);
+
+ FakeSpatialState spatialState = new FakeSpatialState();
+ spatialState.setSpatialCapabilities(
+ new androidx.xr.extensions.space.SpatialCapabilities() {
+ @Override
+ public boolean get(int capability) {
+ return capability
+ == androidx.xr.extensions.space.SpatialCapabilities
+ .SPATIAL_UI_CAPABLE;
+ }
+ });
+ ((JxrPlatformAdapterAxr) realityCoreRuntime).onSpatialStateChanged(spatialState);
+
+ SpatialCapabilities caps = realityCoreRuntime.getSpatialCapabilities();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_UI)).isTrue();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT)).isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL))
+ .isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT))
+ .isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO))
+ .isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY))
+ .isFalse();
+ }
+
+ @Test
+ public void onSpatialStateChanged_setsEnvironmentVisibility() {
+ SpatialEnvironment environment = realityCoreRuntime.getSpatialEnvironment();
+ assertThat(environment.isSpatialEnvironmentPreferenceActive()).isFalse();
+
+ FakeSpatialState state = new FakeSpatialState();
+ state.setEnvironmentVisibility(
+ new FakeEnvironmentVisibilityState(EnvironmentVisibilityState.APP_VISIBLE));
+ fakeExtensions.sendSpatialState(state);
+ assertThat(environment.isSpatialEnvironmentPreferenceActive()).isTrue();
+
+ state = new FakeSpatialState();
+ state.setEnvironmentVisibility(
+ new FakeEnvironmentVisibilityState(EnvironmentVisibilityState.INVISIBLE));
+ fakeExtensions.sendSpatialState(state);
+ assertThat(environment.isSpatialEnvironmentPreferenceActive()).isFalse();
+
+ state = new FakeSpatialState();
+ state.setEnvironmentVisibility(
+ new FakeEnvironmentVisibilityState(EnvironmentVisibilityState.HOME_VISIBLE));
+ fakeExtensions.sendSpatialState(state);
+ assertThat(environment.isSpatialEnvironmentPreferenceActive()).isFalse();
+ }
+
+ @Test
+ public void onSpatialStateChanged_callsEnvironmentListenerOnlyForChanges() {
+ SpatialEnvironment environment = realityCoreRuntime.getSpatialEnvironment();
+ @SuppressWarnings(value = "unchecked")
+ Consumer<Boolean> listener = (Consumer<Boolean>) mock(Consumer.class);
+
+ environment.addOnSpatialEnvironmentChangedListener(listener);
+
+ assertThat(environment.isSpatialEnvironmentPreferenceActive()).isFalse();
+
+ // The first spatial state should always fire the listener
+ FakeSpatialState state = new FakeSpatialState();
+ state.setEnvironmentVisibility(
+ new FakeEnvironmentVisibilityState(EnvironmentVisibilityState.APP_VISIBLE));
+ fakeExtensions.sendSpatialState(state);
+ verify(listener).accept(true);
+
+ // The second spatial state should also fire the listener since it's a different state
+ state = new FakeSpatialState();
+ state.setEnvironmentVisibility(
+ new FakeEnvironmentVisibilityState(EnvironmentVisibilityState.INVISIBLE));
+ fakeExtensions.sendSpatialState(state);
+ assertThat(environment.isSpatialEnvironmentPreferenceActive()).isFalse();
+ verify(listener).accept(false);
+
+ // The third spatial state should not fire the listener since it is the same as the last
+ // state.
+ state = new FakeSpatialState();
+ state.setEnvironmentVisibility(
+ new FakeEnvironmentVisibilityState(EnvironmentVisibilityState.INVISIBLE));
+ fakeExtensions.sendSpatialState(state);
+ assertThat(environment.isSpatialEnvironmentPreferenceActive()).isFalse();
+ verify(listener, times(2))
+ .accept(any()); // Verify the listener was not called a third time.
+ }
+
+ @Test
+ public void onSpatialStateChanged_setsPassthroughOpacity() {
+ SpatialEnvironment environment = realityCoreRuntime.getSpatialEnvironment();
+ assertThat(environment.getCurrentPassthroughOpacity()).isZero();
+
+ FakeSpatialState state = new FakeSpatialState();
+ state.setPassthroughVisibility(
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.APP, 0.4f));
+ fakeExtensions.sendSpatialState(state);
+ assertThat(environment.getCurrentPassthroughOpacity()).isEqualTo(0.4f);
+
+ state = new FakeSpatialState();
+ state.setPassthroughVisibility(
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.HOME, 0.5f));
+ fakeExtensions.sendSpatialState(state);
+ assertThat(environment.getCurrentPassthroughOpacity()).isEqualTo(0.5f);
+
+ state = new FakeSpatialState();
+ state.setPassthroughVisibility(
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.SYSTEM, 0.9f));
+ fakeExtensions.sendSpatialState(state);
+ assertThat(environment.getCurrentPassthroughOpacity()).isEqualTo(0.9f);
+
+ state = new FakeSpatialState();
+ state.setPassthroughVisibility(
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.DISABLED, 0.0f));
+ fakeExtensions.sendSpatialState(state);
+ assertThat(environment.getCurrentPassthroughOpacity()).isZero();
+ }
+
+ @Test
+ public void onSpatialStateChanged_callsPassthroughListenerOnlyForChanges() {
+ SpatialEnvironment environment = realityCoreRuntime.getSpatialEnvironment();
+ @SuppressWarnings(value = "unchecked")
+ Consumer<Float> listener = (Consumer<Float>) mock(Consumer.class);
+
+ environment.addOnPassthroughOpacityChangedListener(listener);
+
+ assertThat(environment.getCurrentPassthroughOpacity()).isZero();
+
+ // The first spatial state should always fire the listener
+ FakeSpatialState state = new FakeSpatialState();
+ state.setPassthroughVisibility(
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.APP, 1.0f));
+ fakeExtensions.sendSpatialState(state);
+ verify(listener).accept(1.0f);
+
+ // The second spatial state should also fire the listener even if only the opacity changes
+ state = new FakeSpatialState();
+ state.setPassthroughVisibility(
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.APP, 0.5f));
+ fakeExtensions.sendSpatialState(state);
+ assertThat(environment.getCurrentPassthroughOpacity()).isEqualTo(0.5f);
+
+ // The third spatial state should also fire the listener even if only the visibility state
+ // changes, but getCurrentPassthroughOpacity() returns the same value as the last state.
+ state = new FakeSpatialState();
+ state.setPassthroughVisibility(
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.HOME, 0.5f));
+ fakeExtensions.sendSpatialState(state);
+ assertThat(environment.getCurrentPassthroughOpacity()).isEqualTo(0.5f);
+ verify(listener, times(2))
+ .accept(0.5f); // Verify it was called a second time with this value.
+
+ // The fourth spatial state should not fire the listener since it is the same as the last
+ // state.
+ state = new FakeSpatialState();
+ state.setPassthroughVisibility(
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.HOME, 0.5f));
+ fakeExtensions.sendSpatialState(state);
+ assertThat(environment.getCurrentPassthroughOpacity()).isEqualTo(0.5f);
+ verify(listener, times(3))
+ .accept(any()); // Verify the listener was not called a fourth time.
+ }
+
+ @Test
+ public void currentPassthroughOpacity_isSetDuringRuntimeCreation() {
+ fakeExtensions.fakeSpatialState.setPassthroughVisibility(
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.APP, 0.5f));
+
+ JxrPlatformAdapter newRealityCoreRuntime =
+ JxrPlatformAdapterAxr.create(
+ activity,
+ fakeExecutor,
+ fakeExtensions,
+ fakeImpressApi,
+ new EntityManager(),
+ perceptionLibrary,
+ splitEngineSubspaceManager,
+ splitEngineRenderer,
+ /* useSplitEngine= */ false);
+
+ SpatialEnvironment newEnvironment = newRealityCoreRuntime.getSpatialEnvironment();
+ assertThat(newEnvironment.getCurrentPassthroughOpacity()).isEqualTo(0.5f);
+ }
+
+ @Test
+ public void onSpatialStateChanged_firesSpatialCapabilitiesChangedListener() {
+ realityCoreRuntime =
+ JxrPlatformAdapterAxr.create(
+ activity,
+ fakeExecutor,
+ fakeExtensions,
+ fakeImpressApi,
+ new EntityManager(),
+ perceptionLibrary,
+ splitEngineSubspaceManager,
+ splitEngineRenderer,
+ /* useSplitEngine= */ false);
+
+ @SuppressWarnings(value = "unchecked")
+ Consumer<SpatialCapabilities> listener1 =
+ (Consumer<SpatialCapabilities>) mock(Consumer.class);
+ @SuppressWarnings(value = "unchecked")
+ Consumer<SpatialCapabilities> listener2 =
+ (Consumer<SpatialCapabilities>) mock(Consumer.class);
+
+ realityCoreRuntime.addSpatialCapabilitiesChangedListener(directExecutor(), listener1);
+ realityCoreRuntime.addSpatialCapabilitiesChangedListener(directExecutor(), listener2);
+
+ FakeSpatialState state = new FakeSpatialState();
+ state.setSpatialCapabilities(
+ new androidx.xr.extensions.space.SpatialCapabilities() {
+ @Override
+ public boolean get(int capability) {
+ return true;
+ }
+ });
+ fakeExtensions.sendSpatialState(state);
+ verify(listener1).accept(any());
+ verify(listener2).accept(any());
+
+ state = new FakeSpatialState();
+ state.setSpatialCapabilities(
+ new androidx.xr.extensions.space.SpatialCapabilities() {
+ @Override
+ public boolean get(int capability) {
+ return false;
+ }
+ });
+ realityCoreRuntime.removeSpatialCapabilitiesChangedListener(listener1);
+ fakeExtensions.sendSpatialState(state);
+ verify(listener1).accept(any()); // Verify the removed listener was called exactly once
+ verify(listener2, times(2)).accept(any()); // Verify the active listener was called twice
+ }
+
+ @Test
+ public void getHeadPoseInOpenXrUnboundedSpace_returnsNullWhenPerceptionSessionUninitialized() {
+ when(perceptionLibrary.getSession()).thenReturn(null);
+ assertThat(((JxrPlatformAdapterAxr) realityCoreRuntime).getHeadPoseInOpenXrUnboundedSpace())
+ .isNull();
+ }
+
+ @Test
+ public void getHeadPoseInOpenXrUnboundedSpace_returnsPose() {
+ when(session.getHeadPose())
+ .thenReturn(
+ new androidx.xr.scenecore.impl.perception.Pose(1f, 1f, 1f, 0f, 0f, 0f, 1f));
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ assertPose(
+ ((JxrPlatformAdapterAxr) realityCoreRuntime).getHeadPoseInOpenXrUnboundedSpace(),
+ new Pose(new Vector3(1f, 1f, 1f), new Quaternion(0f, 0f, 0f, 1f)));
+ }
+
+ @Test
+ public void
+ getStereoViewsInOpenXrUnboundedSpace_returnsNullWhenPerceptionSessionUninitialized() {
+ when(perceptionLibrary.getSession()).thenReturn(null);
+ assertThat(
+ ((JxrPlatformAdapterAxr) realityCoreRuntime)
+ .getStereoViewsInOpenXrUnboundedSpace())
+ .isNull();
+ }
+
+ @Test
+ public void getStereoViewsInOpenXrUnboundedSpace_returnsViewProjections() {
+ ViewProjection leftViewProjection =
+ new ViewProjection(
+ new androidx.xr.scenecore.impl.perception.Pose(-1f, 1f, 1f, 0f, 0f, 0f, 1f),
+ new Fov(-1f, -1f, -1f, -1f));
+
+ ViewProjection rightViewProjection =
+ new ViewProjection(
+ new androidx.xr.scenecore.impl.perception.Pose(1f, 1f, 1f, 0f, 0f, 0f, 1f),
+ new Fov(1f, 1f, 1f, 1f));
+
+ when(session.getStereoViews())
+ .thenReturn(new ViewProjections(leftViewProjection, rightViewProjection));
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ assertThat(
+ ((JxrPlatformAdapterAxr) realityCoreRuntime)
+ .getStereoViewsInOpenXrUnboundedSpace())
+ .isEqualTo(new ViewProjections(leftViewProjection, rightViewProjection));
+ }
+
+ @Test
+ public void loggingEntity_getActivitySpacePose_returnsIdentityPose() {
+ Pose identityPose = new Pose();
+ LoggingEntity loggingEntity = realityCoreRuntime.createLoggingEntity(identityPose);
+ assertPose(loggingEntity.getActivitySpacePose(), identityPose);
+ }
+
+ @Test
+ public void loggingEntity_transformPoseTo_returnsIdentityPose() {
+ Pose identityPose = new Pose();
+ LoggingEntity loggingEntity = realityCoreRuntime.createLoggingEntity(identityPose);
+ assertPose(loggingEntity.transformPoseTo(identityPose, loggingEntity), identityPose);
+ }
+
+ @Test
+ public void getPose_returnsSetPose() throws Exception {
+ Pose pose = new Pose(new Vector3(1f, 2f, 3f), new Quaternion(1f, 2f, 3f, 4f));
+ Pose identityPose = new Pose();
+ PanelEntity panelEntity = createPanelEntity();
+ GltfEntity gltfEntity = createGltfEntity();
+ LoggingEntity loggingEntity = realityCoreRuntime.createLoggingEntity(identityPose);
+ Entity contentlessEntity = createContentlessEntity();
+
+ assertPose(panelEntity.getPose(), identityPose);
+ assertPose(gltfEntity.getPose(), identityPose);
+ assertPose(loggingEntity.getPose(), identityPose);
+ assertPose(contentlessEntity.getPose(), identityPose);
+
+ panelEntity.setPose(pose);
+ gltfEntity.setPose(pose);
+ loggingEntity.setPose(pose);
+ contentlessEntity.setPose(pose);
+
+ assertPose(panelEntity.getPose(), pose);
+ assertPose(gltfEntity.getPose(), pose);
+ assertPose(loggingEntity.getPose(), pose);
+ assertPose(contentlessEntity.getPose(), pose);
+ }
+
+ @Test
+ public void getPose_returnsFactoryMethodPose() throws Exception {
+ Pose pose = new Pose(new Vector3(1f, 2f, 3f), new Quaternion(1f, 2f, 3f, 4f));
+ PanelEntity panelEntity = createPanelEntity(pose);
+ GltfEntity gltfEntity = createGltfEntity(pose);
+ LoggingEntity loggingEntity = realityCoreRuntime.createLoggingEntity(pose);
+ Entity contentlessEntity = createContentlessEntity(pose);
+
+ assertPose(panelEntity.getPose(), pose);
+ assertPose(gltfEntity.getPose(), pose);
+ assertPose(loggingEntity.getPose(), pose);
+ assertPose(contentlessEntity.getPose(), pose);
+ }
+
+ @Test
+ public void getPoseInActivitySpace_withParentChainTranslation_returnsOffsetPositionFromRoot()
+ throws Exception {
+ // Create a simple pose with only a small translation on all axes
+ Pose pose = new Pose(new Vector3(1f, 2f, 3f), Quaternion.Identity);
+
+ // Set the activity space as the root of this entity hierarchy..
+ AndroidXrEntity parentEntity =
+ (AndroidXrEntity)
+ realityCoreRuntime.createEntity(
+ pose, "parent", realityCoreRuntime.getActivitySpace());
+ AndroidXrEntity childEntity1 =
+ (AndroidXrEntity) realityCoreRuntime.createEntity(pose, "child1", parentEntity);
+ AndroidXrEntity childEntity2 =
+ (AndroidXrEntity) realityCoreRuntime.createEntity(pose, "child2", childEntity1);
+
+ assertVector3(
+ parentEntity.getPoseInActivitySpace().getTranslation(), new Vector3(1f, 2f, 3f));
+ assertVector3(
+ childEntity1.getPoseInActivitySpace().getTranslation(), new Vector3(2f, 4f, 6f));
+ assertVector3(
+ childEntity2.getPoseInActivitySpace().getTranslation(), new Vector3(3f, 6f, 9f));
+ }
+
+ @Test
+ public void getPoseInActivitySpace_withParentChainRotation_returnsOffsetRotationFromRoot()
+ throws Exception {
+ // Create a pose with a translation and one with 90 degree rotation around the y axis.
+ Vector3 parentTranslation = new Vector3(1f, 2f, 3f);
+ Pose translatedPose = new Pose(parentTranslation, Quaternion.Identity);
+ Quaternion quaternion = Quaternion.fromAxisAngle(new Vector3(0f, 1f, 0f), 90f);
+ Pose rotatedPose = new Pose(new Vector3(0f, 0f, 0f), quaternion);
+
+ // The parent has a translation and no rotation.
+ AndroidXrEntity parentEntity =
+ (AndroidXrEntity)
+ realityCoreRuntime.createEntity(
+ translatedPose, "parent", realityCoreRuntime.getActivitySpace());
+
+ // Each child adds a rotation, but no translation.
+ AndroidXrEntity childEntity1 =
+ (AndroidXrEntity)
+ realityCoreRuntime.createEntity(rotatedPose, "child1", parentEntity);
+ AndroidXrEntity childEntity2 =
+ (AndroidXrEntity)
+ realityCoreRuntime.createEntity(rotatedPose, "child2", childEntity1);
+
+ // There should be no translation offset from the root, only changes in rotation.
+ assertPose(parentEntity.getPoseInActivitySpace(), translatedPose);
+ assertPose(childEntity1.getPoseInActivitySpace(), new Pose(parentTranslation, quaternion));
+ assertPose(
+ childEntity2.getPoseInActivitySpace(),
+ new Pose(
+ parentTranslation,
+ Quaternion.fromAxisAngle(new Vector3(0f, 1f, 0f), 180f)));
+ }
+
+ @Test
+ public void getPoseInActivitySpace_withParentChainPoseOffsets_returnsOffsetPoseFromRoot()
+ throws Exception {
+ // Create a pose with a 1D translation and a 90 degree rotation around the z axis.
+ Vector3 parentTranslation = new Vector3(1f, 0f, 0f);
+ Quaternion quaternion = Quaternion.fromAxisAngle(new Vector3(0f, 0f, 1f), 90f);
+ Pose pose = new Pose(parentTranslation, quaternion);
+
+ // Each entity adds a translation and a rotation.
+ AndroidXrEntity parentEntity =
+ (AndroidXrEntity)
+ realityCoreRuntime.createEntity(
+ pose, "parent", realityCoreRuntime.getActivitySpace());
+ AndroidXrEntity childEntity1 =
+ (AndroidXrEntity) realityCoreRuntime.createEntity(pose, "child1", parentEntity);
+ AndroidXrEntity childEntity2 =
+ (AndroidXrEntity) realityCoreRuntime.createEntity(pose, "child2", childEntity1);
+
+ // Local pose of ActivitySpace's direct child must be the same as child's ActivitySpace
+ // pose.
+ assertPose(parentEntity.getPoseInActivitySpace(), parentEntity.getPose());
+
+ // Each child should be positioned one unit away at 90 degrees from its parent's position.
+ // Since our coordinate system is right-handed, a +ve rotation around the z axis is a
+ // counter-clockwise rotation of the XY plane.
+ // First child should be 1 unit in the ActivitySpace's positive y direction from its parent
+ assertPose(
+ childEntity1.getPoseInActivitySpace(),
+ new Pose(
+ new Vector3(1f, 1f, 0f),
+ Quaternion.fromAxisAngle(new Vector3(0f, 0f, 1f), 180f)));
+ // Second child should be 1 unit in the ActivitySpace's negative x direction from its parent
+ assertPose(
+ childEntity2.getPoseInActivitySpace(),
+ new Pose(
+ new Vector3(0f, 1f, 0f),
+ Quaternion.fromAxisAngle(new Vector3(0f, 0f, 1f), 270f)));
+ }
+
+ @Test
+ public void getPoseInActivitySpace_withActivitySpaceParent_returnsScaledPose()
+ throws Exception {
+ Pose pose = new Pose(new Vector3(1f, 2f, 3f), new Quaternion(1f, 2f, 3f, 4f));
+
+ // Set the parent as the activity space so these entities' activitySpacePose should match
+ // their
+ // local pose relative to their parent.
+ PanelEntityImpl panelEntity = (PanelEntityImpl) createPanelEntity(pose);
+ GltfEntityImpl gltfEntity = (GltfEntityImpl) createGltfEntity(pose);
+ AndroidXrEntity contentlessEntity = (AndroidXrEntity) createContentlessEntity(pose);
+ ActivitySpace activitySpace = realityCoreRuntime.getActivitySpace();
+ panelEntity.setParent(activitySpace);
+ gltfEntity.setParent(activitySpace);
+ contentlessEntity.setParent(activitySpace);
+
+ assertPose(panelEntity.getPoseInActivitySpace(), pose);
+ assertPose(gltfEntity.getPoseInActivitySpace(), pose);
+ assertPose(contentlessEntity.getPoseInActivitySpace(), pose);
+ }
+
+ @Test
+ public void getPoseInActivitySpace_withScale_returnsPose() throws Exception {
+ Pose localPose = new Pose(new Vector3(1f, 2f, 1f), Quaternion.Identity);
+
+ // Create a hierarchy of entities each translated from their parent by (1,2,1) in parent
+ // space.
+ GltfEntityImpl child1 = (GltfEntityImpl) createGltfEntity(localPose);
+ GltfEntityImpl child2 = (GltfEntityImpl) createGltfEntity(localPose);
+ GltfEntityImpl child3 = (GltfEntityImpl) createGltfEntity(localPose);
+ ActivitySpace activitySpace = realityCoreRuntime.getActivitySpace();
+ assertVector3(activitySpace.getScale(), new Vector3(1f, 1f, 1f));
+
+ // Set a non-unit local scale to each child.
+ child1.setParent(activitySpace);
+ child1.setScale(new Vector3(2f, 2f, 2f));
+
+ child2.setParent(child1);
+ child2.setScale(new Vector3(3f, 2f, 3f));
+
+ child3.setParent(child2);
+ child3.setScale(new Vector3(1f, 1f, 2f));
+
+ // The position (in ActivitySpace) should be:
+ // child's local position * parent's scale in AS + parent's position since there's no
+ // rotation.
+
+ // Assuming c1 = child1, c2 = child2, c3 = child3, AS = activitySpace.
+ // c1.posInAS = c1.localPos * AS.scaleInAS + AS.posInAS = (1,2,1) * (1,1,1) + (0,0,0) =
+ // (1,2,1)
+ assertPose(
+ child1.getPoseInActivitySpace(),
+ new Pose(new Vector3(1f, 2f, 1f), Quaternion.Identity));
+
+ // c2.posInAS = c2.localPos * c1.scaleInAS + c1.posInAS = (1,2,1) * (2,2,2) + (1,2,1) =
+ // (3,6,3)
+ assertPose(
+ child2.getPoseInActivitySpace(),
+ new Pose(new Vector3(3f, 6f, 3f), Quaternion.Identity));
+
+ // c2.scaleInA = c2.localScale * c1.scaleInAS * AS.scale = (3,2,3) * (2,2,2) * (1,1,1) =
+ // (6,4,6)
+ // c3.posInAS = c3.localPos * c2.scaleInAS + c2.posInAS = (1,2,1) * (6,4,6) + (3,6,3) =
+ // (9,14,9)
+ assertPose(
+ child3.getPoseInActivitySpace(),
+ new Pose(new Vector3(9f, 14f, 9f), Quaternion.Identity));
+ }
+
+ @Test
+ public void getActivitySpacePose_withParentChainTranslation_returnsOffsetPositionFromRoot()
+ throws Exception {
+ // Create a simple pose with only a small translation on all axes
+ Pose pose = new Pose(new Vector3(1f, 2f, 3f), Quaternion.Identity);
+
+ // Set the ActivitySpace as the root of this entity hierarchy.
+ Entity parentEntity =
+ realityCoreRuntime.createEntity(
+ pose, "parent", realityCoreRuntime.getActivitySpaceRootImpl());
+ Entity childEntity1 = realityCoreRuntime.createEntity(pose, "child1", parentEntity);
+ Entity childEntity2 = realityCoreRuntime.createEntity(pose, "child2", childEntity1);
+
+ // The translations should accumulate with each child, but there should be no rotation.
+ assertVector3(
+ parentEntity.getActivitySpacePose().getTranslation(), new Vector3(1f, 2f, 3f));
+ assertVector3(
+ childEntity1.getActivitySpacePose().getTranslation(), new Vector3(2f, 4f, 6f));
+ assertVector3(
+ childEntity2.getActivitySpacePose().getTranslation(), new Vector3(3f, 6f, 9f));
+ assertRotation(childEntity2.getActivitySpacePose().getRotation(), Quaternion.Identity);
+ }
+
+ @Test
+ public void getActivitySpacePose_withParentChainRotation_returnsOffsetRotationFromRoot()
+ throws Exception {
+ // Create a pose with a translation and one with 90 degree rotation around the y axis.
+ Vector3 parentTranslation = new Vector3(1f, 0f, 0f);
+ Pose translatedPose = new Pose(parentTranslation, Quaternion.Identity);
+ Quaternion quaternion = Quaternion.fromAxisAngle(new Vector3(0f, 1f, 0f), 90f);
+ Pose rotatedPose = new Pose(new Vector3(0f, 0f, 0f), quaternion);
+
+ // The parent has a translation and no rotation and each child adds a rotation.
+ Entity parentEntity =
+ realityCoreRuntime.createEntity(
+ translatedPose, "parent", realityCoreRuntime.getActivitySpaceRootImpl());
+ Entity childEntity1 = realityCoreRuntime.createEntity(rotatedPose, "child1", parentEntity);
+ Entity childEntity2 = realityCoreRuntime.createEntity(rotatedPose, "child2", childEntity1);
+
+ // There should be no translation offset from the parent, but rotations should accumulate.
+ assertPose(parentEntity.getActivitySpacePose(), translatedPose);
+ assertPose(childEntity1.getActivitySpacePose(), new Pose(parentTranslation, quaternion));
+ assertPose(
+ childEntity2.getActivitySpacePose(),
+ new Pose(
+ parentTranslation,
+ Quaternion.fromAxisAngle(new Vector3(0f, 1f, 0f), 180f)));
+ }
+
+ @Test
+ public void getActivitySpacePose_withParentChainPoseOffsets_returnsOffsetPoseFromRoot()
+ throws Exception {
+ // Create a pose with a 1D translation and a 90 degree rotation around the z axis.
+ Vector3 parentTranslation = new Vector3(1f, 0f, 0f);
+ Quaternion quaternion = Quaternion.fromAxisAngle(new Vector3(0f, 0f, 1f), 90f);
+ Pose pose = new Pose(parentTranslation, quaternion);
+
+ // Each entity adds a translation and a rotation.
+ Entity parentEntity =
+ realityCoreRuntime.createEntity(
+ pose, "parent", realityCoreRuntime.getActivitySpaceRootImpl());
+ Entity childEntity1 = realityCoreRuntime.createEntity(pose, "child1", parentEntity);
+ Entity childEntity2 = realityCoreRuntime.createEntity(pose, "child2", childEntity1);
+
+ // Local pose of ActivitySpace's direct child must be the same as child's ActivitySpace
+ // pose.
+ assertPose(parentEntity.getActivitySpacePose(), parentEntity.getPose());
+
+ // Each child should be positioned one unit away at 90 degrees from its parent's position.
+ // Since our coordinate system is right-handed, a +ve rotation around the z axis is a
+ // counter-clockwise rotation of the XY plane.
+ // First child should be 1 unit in the ActivitySpace's positive y direction from its parent
+ assertPose(
+ childEntity1.getActivitySpacePose(),
+ new Pose(
+ new Vector3(1f, 1f, 0f),
+ Quaternion.fromAxisAngle(new Vector3(0f, 0f, 1f), 180f)));
+ // Second child should be 1 unit in the ActivitySpace's negative x direction from its parent
+ assertPose(
+ childEntity2.getActivitySpacePose(),
+ new Pose(
+ new Vector3(0f, 1f, 0f),
+ Quaternion.fromAxisAngle(new Vector3(0f, 0f, 1f), 270f)));
+ }
+
+ @Test
+ public void getActivitySpacePose_withDefaultParent_returnsPose() throws Exception {
+ Pose pose = new Pose(new Vector3(1f, 2f, 3f), new Quaternion(1f, 2f, 3f, 4f));
+
+ // All these entities should have the ActivitySpaceRootImpl as their parent by default.
+ PanelEntity panelEntity = createPanelEntity(pose);
+ GltfEntity gltfEntity = createGltfEntity(pose);
+ Entity contentlessEntity = createContentlessEntity(pose);
+
+ assertPose(panelEntity.getActivitySpacePose(), pose);
+ assertPose(gltfEntity.getActivitySpacePose(), pose);
+ assertPose(contentlessEntity.getActivitySpacePose(), pose);
+ }
+
+ @Test
+ public void getPoseInActivitySpace_withScale_returnsScaledPose() throws Exception {
+ Pose localPose = new Pose(new Vector3(1f, 2f, 1f), Quaternion.Identity);
+
+ // Create a hierarchy of entities each translated from their parent by (1,1,1) in parent
+ // space.
+ GltfEntityImpl child1 = (GltfEntityImpl) createGltfEntity(localPose);
+ GltfEntityImpl child2 = (GltfEntityImpl) createGltfEntity(localPose);
+ GltfEntityImpl child3 = (GltfEntityImpl) createGltfEntity(localPose);
+ assertVector3(
+ realityCoreRuntime.getActivitySpaceRootImpl().getScale(), new Vector3(1f, 1f, 1f));
+
+ // Set a non-unit local scale to each child.
+ child1.setParent(realityCoreRuntime.getActivitySpaceRootImpl());
+ child1.setScale(new Vector3(2f, 2f, 2f));
+
+ child2.setParent(child1);
+ child2.setScale(new Vector3(3f, 2f, 3f));
+
+ child3.setParent(child2);
+ child3.setScale(new Vector3(1f, 1f, 2f));
+
+ // See getPoseInActivitySpace_withScale_returnsScaledPose for more detailed comments.
+ // The position should be (parent's scale * child's position) + parent's position
+ // since there's no rotation.
+ assertPose(
+ child1.getActivitySpacePose(),
+ new Pose(new Vector3(1f, 2f, 1f), Quaternion.Identity));
+ assertPose(
+ child2.getActivitySpacePose(),
+ new Pose(new Vector3(3f, 6f, 3f), Quaternion.Identity));
+ assertPose(
+ child3.getActivitySpacePose(),
+ new Pose(new Vector3(9f, 14f, 9f), Quaternion.Identity));
+ }
+
+ @Test
+ public void transformPoseTo_sameDestAndSourceEntity_returnsUnchangedPose() throws Exception {
+ Pose pose =
+ new Pose(new Vector3(1f, 2f, 3f), new Quaternion(1f, 2f, 3f, 4f).toNormalized());
+ Pose identity = new Pose();
+
+ PanelEntity panelEntity = createPanelEntity(pose);
+ GltfEntity gltfEntity = createGltfEntity(pose);
+ Entity contentlessEntity = createContentlessEntity(pose);
+ assertPose(panelEntity.transformPoseTo(pose, panelEntity), pose);
+ assertPose(gltfEntity.transformPoseTo(pose, gltfEntity), pose);
+ assertPose(contentlessEntity.transformPoseTo(pose, contentlessEntity), pose);
+
+ assertPose(panelEntity.transformPoseTo(identity, panelEntity), identity);
+ assertPose(gltfEntity.transformPoseTo(identity, gltfEntity), identity);
+ assertPose(contentlessEntity.transformPoseTo(identity, contentlessEntity), identity);
+ }
+
+ @Test
+ public void transformPoseTo_withOnlyTranslationOffset_returnsTranslationDifference()
+ throws Exception {
+ PanelEntityImpl sourceEntity = (PanelEntityImpl) createPanelEntity();
+ GltfEntityImpl destinationEntity = (GltfEntityImpl) createGltfEntity();
+ sourceEntity.setPose(
+ new Pose(new Vector3(1f, 2f, 3f), sourceEntity.getPose().getRotation()));
+ destinationEntity.setPose(
+ new Pose(new Vector3(4f, 5f, 6f), destinationEntity.getPose().getRotation()));
+ Pose offsetFromSource = new Pose(new Vector3(7f, 8f, 9f), Quaternion.Identity);
+
+ // The expected translation is destOffset = (sourceOrigin + sourceOffset) - destOrigin
+ // since there's no rotation and the entities are in the same coordinate space.
+ // So ((1,2,3) + (7,8,9)) - (4,5,6) = (4, 5, 6)
+ Pose offsetInDestinationSpace =
+ sourceEntity.transformPoseTo(offsetFromSource, destinationEntity);
+ Pose expectedPose = new Pose(new Vector3(4f, 5f, 6f), Quaternion.Identity);
+ assertPose(offsetInDestinationSpace, expectedPose);
+ }
+
+ @Test
+ public void transformPoseTo_withOnlyRotationOffset_returnsRotationDifference()
+ throws Exception {
+ PanelEntityImpl sourceEntity = (PanelEntityImpl) createPanelEntity();
+ GltfEntityImpl destinationEntity = (GltfEntityImpl) createGltfEntity();
+
+ sourceEntity.setPose(
+ new Pose(
+ sourceEntity.getPose().getTranslation(),
+ Quaternion.fromEulerAngles(new Vector3(1f, 2f, 3f))));
+ destinationEntity.setPose(
+ new Pose(
+ destinationEntity.getPose().getTranslation(),
+ Quaternion.fromEulerAngles(new Vector3(4f, 5f, 6f))));
+
+ Pose offsetFromSource =
+ new Pose(new Vector3(), Quaternion.fromEulerAngles(new Vector3(7f, 8f, 9f)));
+
+ // The expected rotation is (source + sourceOffset) - destination since the source and
+ // destination are in the same coordinate space: ((1,2,3) + (7,8,9)) - (4,5,6) = (4, 5, 6)
+ Pose offsetInDestinationSpace =
+ sourceEntity.transformPoseTo(offsetFromSource, destinationEntity);
+ Pose expectedPose =
+ new Pose(new Vector3(), Quaternion.fromEulerAngles(new Vector3(4f, 5f, 6f)));
+ assertPose(offsetInDestinationSpace, expectedPose);
+ }
+
+ @Test
+ public void transformPoseTo_withDifferentTranslationAndRotation_returnsTransformedPose() {
+ // Assume the source and destination entities are in the same coordinate space.
+ Vector3 sourceVector = new Vector3(1f, 2f, 3f);
+ Quaternion sourceQuaternion = Quaternion.fromAxisAngle(new Vector3(0f, 0f, 1f), -90f);
+ Vector3 destinationVector = new Vector3(10f, 20f, 30f);
+ Quaternion destinationQuaternion = Quaternion.fromAxisAngle(new Vector3(0f, 0f, 1f), 90f);
+ Pose identity = new Pose();
+
+ AndroidXrEntity sourceEntity =
+ (AndroidXrEntity) createContentlessEntity(new Pose(sourceVector, sourceQuaternion));
+ AndroidXrEntity destinationEntity =
+ (AndroidXrEntity)
+ createContentlessEntity(new Pose(destinationVector, destinationQuaternion));
+
+ //// Transform an identity pose from the source to the destination space. ////
+ Pose sourceToDestinationPose = sourceEntity.transformPoseTo(identity, destinationEntity);
+
+ // The expected rotation is the difference between the quaternions -90 - 90 = -180.
+ Quaternion expectedQuaternion = Quaternion.fromAxisAngle(new Vector3(0f, 0f, 1f), -180f);
+ assertRotation(sourceToDestinationPose.getRotation(), expectedQuaternion);
+
+ // The expected translation is the difference between the source and destination vectors
+ // rotated
+ // by the inverse of the destination quaternion.
+ Vector3 expectedVector =
+ destinationQuaternion.getInverse().times(sourceVector.minus(destinationVector));
+
+ // So difference is (1,2,3) - (10,20,30) = (-9,-18,-27) then rotate CCW by -90 degrees
+ // around
+ // the z axis (ie. swap x and y, set x positive and y negative since we're rotating from 3rd
+ // quadrant to the 2nd quadrant of XY plane) => (-18, 9, -27)
+ assertVector3(expectedVector, new Vector3(-18f, 9f, -27f));
+ assertVector3(sourceToDestinationPose.getTranslation(), expectedVector);
+
+ //// Transform an offset pose from the source to the destination. ////
+ Pose offsetPose =
+ new Pose(
+ new Vector3(1f, 0f, 0f),
+ Quaternion.fromAxisAngle(new Vector3(0f, 0f, 1f), 20f));
+ Pose newSourceToDestinationPose =
+ sourceEntity.transformPoseTo(offsetPose, destinationEntity);
+
+ // The expected rotation is the difference between the quaternions (20-90) - 90 = -160.
+ Quaternion newExpectedQuaternion = Quaternion.fromAxisAngle(new Vector3(0f, 0f, 1f), -160f);
+ assertRotation(newSourceToDestinationPose.getRotation(), newExpectedQuaternion);
+
+ // The expected translation is expected to be the same as the previous one but with the
+ // offset
+ // vector added to it in the destination space.
+ Vector3 offsetInActivitySpace = sourceQuaternion.times(offsetPose.getTranslation());
+ Vector3 offsetInDestinationSpace =
+ destinationQuaternion.getInverse().times(offsetInActivitySpace);
+ Vector3 newExpectedVector = expectedVector.plus(offsetInDestinationSpace);
+
+ // So (1, 0, 0) rotated by -90 degrees around the z axis is (0, 1, 0) in activity space then
+ // add to the difference from source to destination vector (-9,-18,-27) to get (-9, -19,
+ // -27)
+ // and finally rotate by the inverse of the destination quaternion to get (-19, 9, -27).
+ assertVector3(newExpectedVector, new Vector3(-19f, 9f, -27f));
+ assertVector3(newSourceToDestinationPose.getTranslation(), newExpectedVector);
+ }
+
+ @Test
+ public void getAlpha_returnsSetAlpha() throws Exception {
+ PanelEntity panelEntity = createPanelEntity();
+ GltfEntity gltfEntity = createGltfEntity();
+ Entity contentlessEntity = createContentlessEntity();
+
+ assertThat(panelEntity.getAlpha()).isEqualTo(1.0f);
+ assertThat(gltfEntity.getAlpha()).isEqualTo(1.0f);
+ assertThat(contentlessEntity.getAlpha()).isEqualTo(1.0f);
+
+ panelEntity.setAlpha(0.5f);
+ gltfEntity.setAlpha(0.5f);
+ contentlessEntity.setAlpha(0.5f);
+
+ assertThat(panelEntity.getAlpha()).isEqualTo(0.5f);
+ assertThat(gltfEntity.getAlpha()).isEqualTo(0.5f);
+ assertThat(contentlessEntity.getAlpha()).isEqualTo(0.5f);
+ assertThat(
+ fakeExtensions.createdNodes.stream()
+ .map(FakeNode::getAlpha)
+ .collect(Collectors.toList()))
+ .containsAtLeast(0.5f, 0.5f, 0.5f);
+ }
+
+ @Test
+ public void getActivitySpaceAlpha_returnsTotalAncestorAlpha() throws Exception {
+ PanelEntity grandparent = createPanelEntity();
+ GltfEntity parent = createGltfEntity();
+ Entity entity = createContentlessEntity();
+
+ assertThat(grandparent.getActivitySpaceAlpha()).isEqualTo(1.0f);
+ assertThat(parent.getActivitySpaceAlpha()).isEqualTo(1.0f);
+ assertThat(entity.getActivitySpaceAlpha()).isEqualTo(1.0f);
+
+ grandparent.setAlpha(0.5f);
+ parent.setParent(grandparent);
+ parent.setAlpha(0.5f);
+ entity.setParent(parent);
+ entity.setAlpha(0.5f);
+
+ assertThat(grandparent.getActivitySpaceAlpha()).isEqualTo(0.5f);
+ assertThat(parent.getActivitySpaceAlpha()).isEqualTo(0.25f);
+ assertThat(entity.getActivitySpaceAlpha()).isEqualTo(0.125f);
+ assertThat(
+ fakeExtensions.createdNodes.stream()
+ .map(FakeNode::getAlpha)
+ .collect(Collectors.toList()))
+ .containsAtLeast(0.5f, 0.5f, 0.5f);
+ }
+
+ @Test
+ public void transformPoseTo_withScale_returnsPose() throws Exception {
+ PanelEntityImpl sourceEntity = (PanelEntityImpl) createPanelEntity();
+ GltfEntityImpl destinationEntity = (GltfEntityImpl) createGltfEntity();
+ sourceEntity.setPose(new Pose(new Vector3(0f, 0f, 1f), Quaternion.Identity));
+ sourceEntity.setScale(new Vector3(2f, 2f, 2f));
+ destinationEntity.setPose(new Pose(new Vector3(1f, 0f, 0f), Quaternion.Identity));
+ destinationEntity.setScale(new Vector3(3f, 3f, 3f));
+
+ Pose offsetFromSource = new Pose(new Vector3(0f, 0f, 1f), Quaternion.Identity);
+ assertPose(
+ sourceEntity.transformPoseTo(offsetFromSource, destinationEntity),
+ new Pose(new Vector3(-1 / 3f, 0f, 1f), Quaternion.Identity));
+ }
+
+ @Test
+ @Ignore("Flaky test, see b/380269912")
+ public void transformPoseTo_withNonUniformScalesAndTranslations_returnsPose() throws Exception {
+ PanelEntityImpl sourceEntity = (PanelEntityImpl) createPanelEntity();
+ GltfEntityImpl destinationEntity = (GltfEntityImpl) createGltfEntity();
+ sourceEntity.setPose(new Pose(new Vector3(0f, 0f, 1f), Quaternion.Identity));
+ sourceEntity.setScale(new Vector3(0.5f, 2f, -3f));
+ destinationEntity.setPose(new Pose(new Vector3(1f, 1f, 0f), Quaternion.Identity));
+ destinationEntity.setScale(new Vector3(4f, 5f, 6f));
+
+ Pose offsetFromSource = new Pose(new Vector3(1f, 3f, 1f), Quaternion.Identity);
+ // translation is:
+ // ((localOffsetFromSource * scale of source) + sourceTranslation - destinationTranslation)
+ // * (1/scale of destination)
+ //
+ // ((1, 3, 1) * (1/2, 2, -3) + (0, 0, 1) - (1, 1, 0)) * (1/4, 1/5, 1/6) =
+ // ((1/2, 6, -3) + (0, 0, 1) - (1, 1, 0)) * (1/4, 1/5, 1/6) =
+ // (-1/2, 5, -2) * (1/4, 1/5, 1/6) =
+ // (-1/8, 1, -2/6) = (-1/8, 1, -1/3)
+ assertPose(
+ sourceEntity.transformPoseTo(offsetFromSource, destinationEntity),
+ new Pose(new Vector3(-1 / 8f, 1f, -1 / 3f), Quaternion.Identity));
+ }
+
+ @Test
+ public void isHidden_returnsSetHidden() throws Exception {
+ PanelEntity parentEntity = createPanelEntity();
+ assertThat(parentEntity.isHidden(true)).isFalse();
+ assertThat(parentEntity.isHidden(false)).isFalse();
+
+ PanelEntity childEntity1 = createPanelEntity();
+ PanelEntity childEntity2 = createPanelEntity();
+ childEntity1.setParent(parentEntity);
+ childEntity2.setParent(childEntity1);
+
+ assertThat(childEntity1.isHidden(true)).isFalse();
+ assertThat(childEntity1.isHidden(false)).isFalse();
+
+ parentEntity.setHidden(true);
+ assertThat(parentEntity.isHidden(true)).isTrue();
+ assertThat(parentEntity.isHidden(false)).isTrue();
+ assertThat(childEntity1.isHidden(true)).isTrue();
+ assertThat(childEntity1.isHidden(false)).isFalse();
+ assertThat(childEntity2.isHidden(true)).isTrue();
+ assertThat(childEntity2.isHidden(false)).isFalse();
+
+ parentEntity.setHidden(false);
+ assertThat(parentEntity.isHidden(true)).isFalse();
+ assertThat(parentEntity.isHidden(false)).isFalse();
+ assertThat(childEntity1.isHidden(true)).isFalse();
+ assertThat(childEntity1.isHidden(false)).isFalse();
+ assertThat(childEntity2.isHidden(true)).isFalse();
+ assertThat(childEntity2.isHidden(false)).isFalse();
+
+ childEntity1.setHidden(true);
+ assertThat(parentEntity.isHidden(true)).isFalse();
+ assertThat(parentEntity.isHidden(false)).isFalse();
+ assertThat(childEntity1.isHidden(true)).isTrue();
+ assertThat(childEntity1.isHidden(false)).isTrue();
+ assertThat(childEntity2.isHidden(true)).isTrue();
+ assertThat(childEntity2.isHidden(false)).isFalse();
+ }
+
+ @Test
+ public void setHidden_modifiesReforms() throws Exception {
+ PanelEntity testEntity = createPanelEntity();
+ FakeNode testNode = (FakeNode) ((AndroidXrEntity) testEntity).getNode();
+
+ assertThat(
+ testEntity.addComponent(
+ realityCoreRuntime.createMovableComponent(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ /* anchorPlacement= */ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ true)))
+ .isTrue();
+ testEntity.setHidden(true);
+ assertThat(testNode.getReformOptions().getEnabledReform()).isEqualTo(0);
+ testEntity.setHidden(false);
+ assertThat(testNode.getReformOptions().getEnabledReform())
+ .isEqualTo(ReformOptions.ALLOW_MOVE);
+ }
+
+ @Test
+ public void loggingEntityAddChildren() {
+ Pose pose = new Pose();
+ LoggingEntity childEntity1 = realityCoreRuntime.createLoggingEntity(pose);
+ LoggingEntity childEntity2 = realityCoreRuntime.createLoggingEntity(pose);
+ LoggingEntity parentEntity = realityCoreRuntime.createLoggingEntity(pose);
+
+ parentEntity.addChild(childEntity1);
+
+ assertThat(parentEntity.getChildren()).containsExactly(childEntity1);
+
+ parentEntity.addChildren(ImmutableList.of(childEntity2));
+
+ assertThat(childEntity1.getParent()).isEqualTo(parentEntity);
+ assertThat(childEntity2.getParent()).isEqualTo(parentEntity);
+ assertThat(parentEntity.getChildren()).containsExactly(childEntity1, childEntity2);
+ }
+
+ @Test
+ public void getActivitySpace_returnsEntity() {
+ ActivitySpace activitySpace = realityCoreRuntime.getActivitySpace();
+
+ assertThat(activitySpace).isNotNull();
+ // Verify that there is an underlying extension node.
+ ActivitySpaceImpl activitySpaceImpl = (ActivitySpaceImpl) activitySpace;
+ assertThat(activitySpaceImpl.getNode()).isNotNull();
+ }
+
+ @Test
+ public void getActivitySpaceRootImpl_returnsEntity() {
+ Entity activitySpaceRoot = realityCoreRuntime.getActivitySpaceRootImpl();
+ assertThat(activitySpaceRoot).isNotNull();
+
+ // Verify that there is an underlying extension node.
+ AndroidXrEntity activitySpaceRootImpl = (AndroidXrEntity) activitySpaceRoot;
+ assertThat(activitySpaceRootImpl.getNode()).isNotNull();
+ }
+
+ @Test
+ public void getEnvironment_returnsEnvironment() {
+ SpatialEnvironment environment = realityCoreRuntime.getSpatialEnvironment();
+ assertThat(environment).isNotNull();
+ }
+
+ @Test
+ public void getHeadActivityPose_returnsNullIfNotReady() {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getHeadPose()).thenReturn(null);
+ HeadActivityPose headActivityPose = realityCoreRuntime.getHeadActivityPose();
+
+ assertThat(headActivityPose).isNull();
+ }
+
+ @Test
+ public void getHeadActivityPose_returnsActivityPose() {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getHeadPose())
+ .thenReturn(androidx.xr.scenecore.impl.perception.Pose.identity());
+ HeadActivityPose headActivityPose = realityCoreRuntime.getHeadActivityPose();
+
+ assertThat(headActivityPose).isNotNull();
+ }
+
+ @Test
+ public void getCameraViewActivityPose_returnsNullIfNotReady() {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getStereoViews()).thenReturn(new ViewProjections(null, null));
+
+ CameraViewActivityPose leftCameraViewActivityPose =
+ realityCoreRuntime.getCameraViewActivityPose(
+ CameraViewActivityPose.CAMERA_TYPE_LEFT_EYE);
+ CameraViewActivityPose rightCameraViewActivityPose =
+ realityCoreRuntime.getCameraViewActivityPose(
+ CameraViewActivityPose.CAMERA_TYPE_RIGHT_EYE);
+
+ assertThat(leftCameraViewActivityPose).isNull();
+ assertThat(rightCameraViewActivityPose).isNull();
+ }
+
+ @Test
+ public void getLeftCameraViewActivityPose_returnsActivityPose() {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ ViewProjection viewProjection =
+ new ViewProjection(
+ androidx.xr.scenecore.impl.perception.Pose.identity(), new Fov(0, 0, 0, 0));
+ when(session.getStereoViews())
+ .thenReturn(new ViewProjections(viewProjection, viewProjection));
+ CameraViewActivityPose cameraViewActivityPose =
+ realityCoreRuntime.getCameraViewActivityPose(
+ CameraViewActivityPose.CAMERA_TYPE_LEFT_EYE);
+
+ assertThat(cameraViewActivityPose).isNotNull();
+ }
+
+ @Test
+ public void getRightCameraViewActivityPose_returnsActivityPose() {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ ViewProjection viewProjection =
+ new ViewProjection(
+ androidx.xr.scenecore.impl.perception.Pose.identity(), new Fov(0, 0, 0, 0));
+ when(session.getStereoViews())
+ .thenReturn(new ViewProjections(viewProjection, viewProjection));
+ CameraViewActivityPose cameraViewActivityPose =
+ realityCoreRuntime.getCameraViewActivityPose(
+ CameraViewActivityPose.CAMERA_TYPE_RIGHT_EYE);
+
+ assertThat(cameraViewActivityPose).isNotNull();
+ }
+
+ @Test
+ public void getUnknownCameraViewActivityPose_returnsEmptyOptional() {
+ CameraViewActivityPose cameraViewActivityPose =
+ realityCoreRuntime.getCameraViewActivityPose(555);
+
+ assertThat(cameraViewActivityPose).isNull();
+ }
+
+ @Test
+ public void loadExrImageByAssetName_returnsImage() throws Exception {
+ ListenableFuture<ExrImageResource> imageFuture =
+ realityCoreRuntime.loadExrImageByAssetName("FakeAsset.exr");
+
+ assertThat(imageFuture).isNotNull();
+
+ ExrImageResource image = imageFuture.get();
+ assertThat(image).isNotNull();
+ ExrImageResourceImpl imageImpl = (ExrImageResourceImpl) image;
+ assertThat(imageImpl).isNotNull();
+ FakeEnvironmentToken token = (FakeEnvironmentToken) imageImpl.getToken();
+ assertThat(token).isNotNull();
+ assertThat(token.getUrl()).isEqualTo("FakeAsset.exr");
+ }
+
+ @Test
+ public void loadGltfByAssetName_returnsModel() throws Exception {
+ ListenableFuture<GltfModelResource> modelFuture =
+ realityCoreRuntime.loadGltfByAssetName("FakeAsset.glb");
+
+ assertThat(modelFuture).isNotNull();
+
+ GltfModelResource model = modelFuture.get();
+ assertThat(model).isNotNull();
+ GltfModelResourceImpl modelImpl = (GltfModelResourceImpl) model;
+ assertThat(modelImpl).isNotNull();
+ FakeGltfModelToken token = (FakeGltfModelToken) modelImpl.getExtensionModelToken();
+ assertThat(token).isNotNull();
+ assertThat(token.getUrl()).isEqualTo("FakeAsset.glb");
+ }
+
+ @Test
+ public void createGltfEntity_returnsEntity() throws Exception {
+ assertThat(createGltfEntity()).isNotNull();
+ }
+
+ @Test
+ public void createGltfEntitySplitEngine_returnsEntity() throws Exception {
+ assertThat(createGltfEntitySplitEngine()).isNotNull();
+ }
+
+ @Test
+ public void animateGltfEntitySplitEngine_gltfEntityIsAnimating() throws Exception {
+ GltfEntity gltfEntitySplitEngine = createGltfEntitySplitEngine();
+ gltfEntitySplitEngine.startAnimation(false, "animation_name");
+ int animatingNodes = fakeImpressApi.impressNodeAnimatingSize();
+ int loopingAnimatingNodes = fakeImpressApi.impressNodeLoopAnimatingSize();
+
+ // The fakeJniApi returns a future which immediately fires, which makes it seem like the
+ // animation is done immediately. This makes it look like the animation stopped right away.
+ assertThat(gltfEntitySplitEngine.getAnimationState())
+ .isEqualTo(GltfEntity.AnimationState.PLAYING);
+ assertThat(animatingNodes).isEqualTo(1);
+ assertThat(loopingAnimatingNodes).isEqualTo(0);
+ }
+
+ @Test
+ public void animateLoopGltfEntitySplitEngine_gltfEntityIsAnimatingInLoop() throws Exception {
+ GltfEntity gltfEntitySplitEngine = createGltfEntitySplitEngine();
+ gltfEntitySplitEngine.startAnimation(true, "animation_name");
+ int animatingNodes = fakeImpressApi.impressNodeAnimatingSize();
+ int loopingAnimatingNodes = fakeImpressApi.impressNodeLoopAnimatingSize();
+
+ assertThat(gltfEntitySplitEngine.getAnimationState())
+ .isEqualTo(GltfEntity.AnimationState.PLAYING);
+ assertThat(animatingNodes).isEqualTo(0);
+ assertThat(loopingAnimatingNodes).isEqualTo(1);
+ }
+
+ @Test
+ public void stopAnimateGltfEntitySplitEngine_gltfEntityStopsAnimating() throws Exception {
+ GltfEntity gltfEntitySplitEngine = createGltfEntitySplitEngine();
+ gltfEntitySplitEngine.startAnimation(true, "animation_name");
+ gltfEntitySplitEngine.stopAnimation();
+ int animatingNodes = fakeImpressApi.impressNodeAnimatingSize();
+ int loopingAnimatingNodes = fakeImpressApi.impressNodeLoopAnimatingSize();
+
+ assertThat(gltfEntitySplitEngine.getAnimationState())
+ .isEqualTo(GltfEntity.AnimationState.STOPPED);
+ assertThat(animatingNodes).isEqualTo(0);
+ assertThat(loopingAnimatingNodes).isEqualTo(0);
+ }
+
+ @Test
+ public void gltfEntitySetParent() throws Exception {
+ GltfEntity childEntity = createGltfEntity();
+ GltfEntity parentEntity = createGltfEntity();
+
+ childEntity.setParent(parentEntity);
+
+ assertThat(childEntity.getParent()).isEqualTo(parentEntity);
+ assertThat(parentEntity.getParent())
+ .isEqualTo(realityCoreRuntime.getActivitySpaceRootImpl());
+ assertThat(childEntity.getChildren()).isEmpty();
+ assertThat(parentEntity.getChildren()).containsExactly(childEntity);
+
+ // Verify that there is an underlying extension node relationship.
+ FakeNode childNode = (FakeNode) ((GltfEntityImpl) childEntity).getNode();
+ assertThat(childNode.getParent()).isEqualTo(((GltfEntityImpl) parentEntity).getNode());
+ }
+
+ @Test
+ public void gltfEntityUpdateParent() throws Exception {
+ GltfEntity childEntity = createGltfEntity();
+ GltfEntity parentEntity1 = createGltfEntity();
+ GltfEntity parentEntity2 = createGltfEntity();
+
+ childEntity.setParent(parentEntity1);
+ assertThat(childEntity.getParent()).isEqualTo(parentEntity1);
+ assertThat(parentEntity1.getChildren()).containsExactly(childEntity);
+ assertThat(parentEntity2.getChildren()).isEmpty();
+
+ FakeNode childNode = (FakeNode) ((GltfEntityImpl) childEntity).getNode();
+ assertThat(childNode.getParent()).isEqualTo(((GltfEntityImpl) parentEntity1).getNode());
+
+ childEntity.setParent(parentEntity2);
+ assertThat(childEntity.getParent()).isEqualTo(parentEntity2);
+ assertThat(parentEntity2.getChildren()).containsExactly(childEntity);
+ assertThat(parentEntity1.getChildren()).isEmpty();
+ assertThat(childNode.getParent()).isEqualTo(((GltfEntityImpl) parentEntity2).getNode());
+ }
+
+ @Test
+ public void gltfEntityAddChildren() throws Exception {
+ GltfEntity childEntity1 = createGltfEntity();
+ GltfEntity childEntity2 = createGltfEntity();
+ GltfEntity parentEntity = createGltfEntity();
+
+ parentEntity.addChild(childEntity1);
+
+ assertThat(parentEntity.getChildren()).containsExactly(childEntity1);
+
+ parentEntity.addChildren(ImmutableList.of(childEntity2));
+
+ assertThat(childEntity1.getParent()).isEqualTo(parentEntity);
+ assertThat(childEntity2.getParent()).isEqualTo(parentEntity);
+ assertThat(parentEntity.getChildren()).containsExactly(childEntity1, childEntity2);
+
+ FakeNode childNode1 = (FakeNode) ((GltfEntityImpl) childEntity1).getNode();
+ assertThat(childNode1.getParent()).isEqualTo(((GltfEntityImpl) parentEntity).getNode());
+ FakeNode childNode2 = (FakeNode) ((GltfEntityImpl) childEntity2).getNode();
+ assertThat(childNode2.getParent()).isEqualTo(((GltfEntityImpl) parentEntity).getNode());
+ }
+
+ @Test
+ public void createPanelEntity_returnsEntity() throws Exception {
+ assertThat(createPanelEntity()).isNotNull();
+ }
+
+ @Test
+ public void allPanelEnities_haveActivitySpaceRootImplAsParentByDefault() throws Exception {
+ PanelEntity panelEntity = createPanelEntity();
+
+ assertThat(panelEntity.getParent())
+ .isEqualTo(realityCoreRuntime.getActivitySpaceRootImpl());
+ }
+
+ @Test
+ public void panelEntitySetParent_setsParent() throws Exception {
+ PanelEntity childEntity = createPanelEntity();
+ PanelEntity parentEntity = createPanelEntity();
+
+ childEntity.setParent(parentEntity);
+
+ assertThat(childEntity.getParent()).isEqualTo(parentEntity);
+ assertThat(childEntity.getChildren()).isEmpty();
+ assertThat(parentEntity.getChildren()).containsExactly(childEntity);
+
+ // Verify that there is an underlying extension node relationship.
+ FakeNode childNode = (FakeNode) ((PanelEntityImpl) childEntity).getNode();
+ assertThat(childNode.getParent()).isEqualTo(((PanelEntityImpl) parentEntity).getNode());
+ }
+
+ @Test
+ public void panelEntityUpdateParent_updatesParent() throws Exception {
+ PanelEntity childEntity = createPanelEntity();
+ PanelEntity parentEntity1 = createPanelEntity();
+ PanelEntity parentEntity2 = createPanelEntity();
+
+ childEntity.setParent(parentEntity1);
+ assertThat(childEntity.getParent()).isEqualTo(parentEntity1);
+ assertThat(parentEntity1.getChildren()).containsExactly(childEntity);
+ assertThat(parentEntity2.getChildren()).isEmpty();
+
+ FakeNode childNode = (FakeNode) ((PanelEntityImpl) childEntity).getNode();
+ assertThat(childNode.getParent()).isEqualTo(((PanelEntityImpl) parentEntity1).getNode());
+
+ childEntity.setParent(parentEntity2);
+ assertThat(childEntity.getParent()).isEqualTo(parentEntity2);
+ assertThat(parentEntity2.getChildren()).containsExactly(childEntity);
+ assertThat(parentEntity1.getChildren()).isEmpty();
+ assertThat(childNode.getParent()).isEqualTo(((PanelEntityImpl) parentEntity2).getNode());
+ }
+
+ @Test
+ public void panelEntityAddChildren_addsChildren() throws Exception {
+ PanelEntity childEntity1 = createPanelEntity();
+ PanelEntity childEntity2 = createPanelEntity();
+ PanelEntity parentEntity = createPanelEntity();
+
+ parentEntity.addChild(childEntity1);
+
+ assertThat(parentEntity.getChildren()).containsExactly(childEntity1);
+
+ parentEntity.addChildren(ImmutableList.of(childEntity2));
+
+ assertThat(childEntity1.getParent()).isEqualTo(parentEntity);
+ assertThat(childEntity2.getParent()).isEqualTo(parentEntity);
+ assertThat(parentEntity.getChildren()).containsExactly(childEntity1, childEntity2);
+
+ FakeNode childNode1 = (FakeNode) ((PanelEntityImpl) childEntity1).getNode();
+ assertThat(childNode1.getParent()).isEqualTo(((PanelEntityImpl) parentEntity).getNode());
+ FakeNode childNode2 = (FakeNode) ((PanelEntityImpl) childEntity2).getNode();
+ assertThat(childNode2.getParent()).isEqualTo(((PanelEntityImpl) parentEntity).getNode());
+ }
+
+ @Test
+ public void createAnchorEntity_returnsAndInitsAnchor() throws Exception {
+ Dimensions anchorDimensions = new Dimensions(2f, 5f, 0f);
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ androidx.xr.scenecore.impl.perception.Pose.identity();
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+ when(plane.getData(any()))
+ .thenReturn(
+ new Plane.PlaneData(
+ perceptionPose,
+ 3.0f,
+ 5.0f,
+ Plane.Type.VERTICAL.intValue,
+ Plane.Label.WALL.intValue));
+ when(plane.createAnchor(eq(perceptionPose), any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ AnchorEntity anchorEntity =
+ realityCoreRuntime.createAnchorEntity(
+ anchorDimensions, PlaneType.VERTICAL, PlaneSemantic.WALL, Duration.ZERO);
+
+ assertThat(anchorEntity).isNotNull();
+ assertThat(anchorEntity.getState()).isEqualTo(State.ANCHORED);
+ }
+
+ @Test
+ public void getMainPanelEntity_returnsPanelEntity() throws Exception {
+ assertThat(realityCoreRuntime.getMainPanelEntity()).isNotNull();
+ }
+
+ @Test
+ public void getMainPanelEntity_usesWindowLeashNode() throws Exception {
+ PanelEntity mainPanel = realityCoreRuntime.getMainPanelEntity();
+
+ assertThat(((MainPanelEntityImpl) mainPanel).getNode())
+ .isEqualTo(fakeExtensions.getFakeNodeForMainWindow());
+ }
+
+ @Test
+ public void addInputEventConsumerToEntity_setsUpNodeListener() {
+ InputEventListener mockConsumer = mock(InputEventListener.class);
+ PanelEntity panelEntity = createPanelEntity();
+ Executor executor = directExecutor();
+ panelEntity.addInputEventListener(executor, mockConsumer);
+ FakeNode node = (FakeNode) ((PanelEntityImpl) panelEntity).getNode();
+
+ assertThat(node.getListener()).isNotNull();
+ assertThat(node.getExecutor()).isEqualTo(fakeExecutor);
+
+ FakeInputEvent inputEvent = new FakeInputEvent();
+ inputEvent.setOrigin(new Vec3(0, 0, 0));
+ inputEvent.setDirection(new Vec3(1, 1, 1));
+
+ node.sendInputEvent(inputEvent);
+ fakeExecutor.runAll();
+
+ verify(mockConsumer).onInputEvent(any());
+ }
+
+ @Test
+ public void inputEvent_hasHitInfo() {
+ InputEventListener mockConsumer = mock(InputEventListener.class);
+ PanelEntity panelEntity = createPanelEntity();
+ Executor executor = directExecutor();
+ panelEntity.addInputEventListener(executor, mockConsumer);
+ FakeNode node = (FakeNode) ((PanelEntityImpl) panelEntity).getNode();
+ FakeInputEvent xrInputEvent = new FakeInputEvent();
+ xrInputEvent.setOrigin(new Vec3(0, 0, 0));
+ xrInputEvent.setDirection(new Vec3(1, 1, 1));
+ FakeHitInfo hitInfo = new FakeHitInfo();
+ hitInfo.setInputNode(node);
+ hitInfo.setHitPosition(new Vec3(1, 2, 3));
+ hitInfo.setTransform(new Mat4f(new float[16]));
+ xrInputEvent.setFakeHitInfo(hitInfo);
+
+ node.sendInputEvent(xrInputEvent);
+ fakeExecutor.runAll();
+
+ ArgumentCaptor<InputEvent> inputEventCaptor = ArgumentCaptor.forClass(InputEvent.class);
+ verify(mockConsumer).onInputEvent(inputEventCaptor.capture());
+ InputEvent capturedEvent = inputEventCaptor.getValue();
+ assertThat(capturedEvent.hitInfo).isNotNull();
+ assertThat(capturedEvent.hitInfo.inputEntity).isEqualTo(panelEntity);
+ assertThat(capturedEvent.hitInfo.hitPosition).isEqualTo(new Vector3(1, 2, 3));
+ }
+
+ @Test
+ public void passingNullExecutorWhenAddingConsumer_usesInternalExecutor() {
+ InputEventListener mockConsumer = mock(InputEventListener.class);
+ PanelEntity panelEntity = createPanelEntity();
+ panelEntity.addInputEventListener(/* executor= */ null, mockConsumer);
+ FakeNode node = (FakeNode) ((PanelEntityImpl) panelEntity).getNode();
+
+ assertThat(node.getListener()).isNotNull();
+ assertThat(node.getExecutor()).isNotNull();
+ }
+
+ @Test
+ public void addMultipleInputEventConsumerToEntity_setsUpInputCallbacksForAll() {
+ InputEventListener mockConsumer1 = mock(InputEventListener.class);
+ InputEventListener mockConsumer2 = mock(InputEventListener.class);
+ PanelEntity panelEntity = createPanelEntity();
+ Executor executor = directExecutor();
+ panelEntity.addInputEventListener(executor, mockConsumer1);
+ panelEntity.addInputEventListener(executor, mockConsumer2);
+ FakeNode node = (FakeNode) ((PanelEntityImpl) panelEntity).getNode();
+ FakeInputEvent inputEvent = new FakeInputEvent();
+ inputEvent.setOrigin(new Vec3(0, 0, 0));
+ inputEvent.setDirection(new Vec3(1, 1, 1));
+
+ node.sendInputEvent(inputEvent);
+ fakeExecutor.runAll();
+
+ verify(mockConsumer1).onInputEvent(any());
+ verify(mockConsumer2).onInputEvent(any());
+ }
+
+ @Test
+ public void addMultipleInputEventConsumersToEntity_setsUpInputCallbacksOnGivenExecutors() {
+ InputEventListener mockConsumer1 = mock(InputEventListener.class);
+ InputEventListener mockConsumer2 = mock(InputEventListener.class);
+ PanelEntity panelEntity = createPanelEntity();
+ FakeScheduledExecutorService executor1 = new FakeScheduledExecutorService();
+ FakeScheduledExecutorService executor2 = new FakeScheduledExecutorService();
+ panelEntity.addInputEventListener(executor1, mockConsumer1);
+ panelEntity.addInputEventListener(executor2, mockConsumer2);
+ FakeNode node = (FakeNode) ((PanelEntityImpl) panelEntity).getNode();
+ FakeInputEvent inputEvent = new FakeInputEvent();
+ inputEvent.setOrigin(new Vec3(0, 0, 0));
+ inputEvent.setDirection(new Vec3(1, 1, 1));
+
+ node.sendInputEvent(inputEvent);
+ fakeExecutor.runAll();
+
+ assertThat(executor1.hasNext()).isTrue();
+ assertThat(executor2.hasNext()).isTrue();
+
+ executor1.runAll();
+ executor2.runAll();
+
+ verify(mockConsumer1).onInputEvent(any());
+ verify(mockConsumer2).onInputEvent(any());
+ }
+
+ @Test
+ public void removeInputEventConsumerToEntity_removesFromCallbacks() {
+ InputEventListener mockConsumer1 = mock(InputEventListener.class);
+ InputEventListener mockConsumer2 = mock(InputEventListener.class);
+ PanelEntity panelEntity = createPanelEntity();
+ Executor executor = directExecutor();
+ panelEntity.addInputEventListener(executor, mockConsumer1);
+ panelEntity.addInputEventListener(executor, mockConsumer2);
+ FakeNode node = (FakeNode) ((PanelEntityImpl) panelEntity).getNode();
+ FakeInputEvent inputEvent = new FakeInputEvent();
+ inputEvent.setOrigin(new Vec3(0, 0, 0));
+ inputEvent.setDirection(new Vec3(1, 1, 1));
+
+ node.sendInputEvent(inputEvent);
+ fakeExecutor.runAll();
+
+ panelEntity.removeInputEventListener(mockConsumer1);
+
+ node.sendInputEvent(inputEvent);
+ fakeExecutor.runAll();
+
+ verify(mockConsumer2, times(2)).onInputEvent(any());
+ verify(mockConsumer1).onInputEvent(any());
+ }
+
+ @Test
+ public void removeAllInputEventConsumers_stopsInputListening() {
+ InputEventListener mockConsumer1 = mock(InputEventListener.class);
+ InputEventListener mockConsumer2 = mock(InputEventListener.class);
+ PanelEntity panelEntity = createPanelEntity();
+ Executor executor = directExecutor();
+ panelEntity.addInputEventListener(executor, mockConsumer1);
+ panelEntity.addInputEventListener(executor, mockConsumer2);
+ FakeNode node = (FakeNode) ((PanelEntityImpl) panelEntity).getNode();
+ FakeInputEvent inputEvent = new FakeInputEvent();
+ inputEvent.setOrigin(new Vec3(0, 0, 0));
+ inputEvent.setDirection(new Vec3(1, 1, 1));
+
+ node.sendInputEvent(inputEvent);
+ fakeExecutor.runAll();
+
+ verify(mockConsumer1).onInputEvent(any());
+ verify(mockConsumer2).onInputEvent(any());
+
+ panelEntity.removeInputEventListener(mockConsumer1);
+ panelEntity.removeInputEventListener(mockConsumer2);
+
+ assertThat(((PanelEntityImpl) panelEntity).inputEventListenerMap).isEmpty();
+ assertThat(node.getListener()).isNull();
+ assertThat(node.getExecutor()).isNull();
+ }
+
+ @Test
+ public void dispose_stopsInputListening() {
+ InputEventListener mockConsumer1 = mock(InputEventListener.class);
+ InputEventListener mockConsumer2 = mock(InputEventListener.class);
+ PanelEntity panelEntity = createPanelEntity();
+ Executor executor = directExecutor();
+ panelEntity.addInputEventListener(executor, mockConsumer1);
+ panelEntity.addInputEventListener(executor, mockConsumer2);
+ FakeNode node = (FakeNode) ((PanelEntityImpl) panelEntity).getNode();
+ FakeInputEvent inputEvent = new FakeInputEvent();
+ inputEvent.setOrigin(new Vec3(0, 0, 0));
+ inputEvent.setDirection(new Vec3(1, 1, 1));
+
+ node.sendInputEvent(inputEvent);
+ fakeExecutor.runAll();
+
+ verify(mockConsumer1).onInputEvent(any());
+ verify(mockConsumer2).onInputEvent(any());
+
+ panelEntity.dispose();
+
+ assertThat(((PanelEntityImpl) panelEntity).inputEventListenerMap).isEmpty();
+ assertThat(node.getListener()).isNull();
+ assertThat(node.getExecutor()).isNull();
+ }
+
+ @Test
+ public void createContentlessEntity_returnsEntity() throws Exception {
+ assertThat(createContentlessEntity()).isNotNull();
+ }
+
+ @Test
+ public void contentlessEntity_hasActivitySpaceRootImplAsParentByDefault() throws Exception {
+ Entity entity = createContentlessEntity();
+ assertThat(entity.getParent()).isEqualTo(realityCoreRuntime.getActivitySpaceRootImpl());
+ }
+
+ @Test
+ public void contentlessEntityAddChildren_addsChildren() throws Exception {
+ Entity childEntity1 = createContentlessEntity();
+ Entity childEntity2 = createContentlessEntity();
+ Entity parentEntity = createContentlessEntity();
+
+ parentEntity.addChild(childEntity1);
+
+ assertThat(parentEntity.getChildren()).containsExactly(childEntity1);
+
+ parentEntity.addChildren(ImmutableList.of(childEntity2));
+
+ assertThat(childEntity1.getParent()).isEqualTo(parentEntity);
+ assertThat(childEntity2.getParent()).isEqualTo(parentEntity);
+ assertThat(parentEntity.getChildren()).containsExactly(childEntity1, childEntity2);
+
+ FakeNode childNode1 = (FakeNode) ((AndroidXrEntity) childEntity1).getNode();
+ assertThat(childNode1.getParent()).isEqualTo(((AndroidXrEntity) parentEntity).getNode());
+ FakeNode childNode2 = (FakeNode) ((AndroidXrEntity) childEntity2).getNode();
+ assertThat(childNode2.getParent()).isEqualTo(((AndroidXrEntity) parentEntity).getNode());
+ }
+
+ @Test
+ public void addComponent_callsOnAttach() throws Exception {
+ PanelEntity panelEntity = createPanelEntity();
+ GltfEntity gltfEntity = createGltfEntity();
+ LoggingEntity loggingEntity = realityCoreRuntime.createLoggingEntity(new Pose());
+ Component component = mock(Component.class);
+ when(component.onAttach(any())).thenReturn(true);
+
+ assertThat(panelEntity.addComponent(component)).isTrue();
+ verify(component).onAttach(panelEntity);
+
+ assertThat(gltfEntity.addComponent(component)).isTrue();
+ verify(component).onAttach(gltfEntity);
+
+ assertThat(loggingEntity.addComponent(component)).isTrue();
+ verify(component).onAttach(loggingEntity);
+ }
+
+ @Test
+ public void addComponent_failsIfOnAttachFails() throws Exception {
+ PanelEntity panelEntity = createPanelEntity();
+ GltfEntity gltfEntity = createGltfEntity();
+ LoggingEntity loggingEntity = realityCoreRuntime.createLoggingEntity(new Pose());
+ Component component = mock(Component.class);
+ when(component.onAttach(any())).thenReturn(false);
+
+ assertThat(panelEntity.addComponent(component)).isFalse();
+ verify(component).onAttach(panelEntity);
+
+ assertThat(gltfEntity.addComponent(component)).isFalse();
+ verify(component).onAttach(gltfEntity);
+
+ assertThat(loggingEntity.addComponent(component)).isFalse();
+ verify(component).onAttach(loggingEntity);
+ }
+
+ @Test
+ public void removeComponent_callsOnDetach() throws Exception {
+ PanelEntity panelEntity = createPanelEntity();
+ GltfEntity gltfEntity = createGltfEntity();
+ LoggingEntity loggingEntity = realityCoreRuntime.createLoggingEntity(new Pose());
+ Component component = mock(Component.class);
+ when(component.onAttach(any())).thenReturn(true);
+
+ assertThat(panelEntity.addComponent(component)).isTrue();
+ verify(component).onAttach(panelEntity);
+
+ panelEntity.removeComponent(component);
+ verify(component).onDetach(panelEntity);
+
+ assertThat(gltfEntity.addComponent(component)).isTrue();
+ verify(component).onAttach(gltfEntity);
+
+ gltfEntity.removeComponent(component);
+ verify(component).onDetach(gltfEntity);
+
+ assertThat(loggingEntity.addComponent(component)).isTrue();
+ verify(component).onAttach(loggingEntity);
+
+ loggingEntity.removeComponent(component);
+ verify(component).onDetach(loggingEntity);
+ }
+
+ @Test
+ public void addingSameComponentTypeAgain_addsComponent() throws Exception {
+ PanelEntity panelEntity = createPanelEntity();
+ GltfEntity gltfEntity = createGltfEntity();
+ LoggingEntity loggingEntity = realityCoreRuntime.createLoggingEntity(new Pose());
+ Component component1 = mock(Component.class);
+ Component component2 = mock(Component.class);
+ when(component1.onAttach(any())).thenReturn(true);
+ when(component2.onAttach(any())).thenReturn(true);
+
+ assertThat(panelEntity.addComponent(component1)).isTrue();
+ assertThat(panelEntity.addComponent(component2)).isTrue();
+ verify(component1).onAttach(panelEntity);
+ verify(component2).onAttach(panelEntity);
+
+ assertThat(gltfEntity.addComponent(component1)).isTrue();
+ assertThat(gltfEntity.addComponent(component2)).isTrue();
+ verify(component1).onAttach(gltfEntity);
+ verify(component2).onAttach(panelEntity);
+
+ assertThat(loggingEntity.addComponent(component1)).isTrue();
+ assertThat(loggingEntity.addComponent(component2)).isTrue();
+ verify(component1).onAttach(loggingEntity);
+ verify(component2).onAttach(panelEntity);
+ }
+
+ @Test
+ public void addingDifferentComponentType_addComponentSucceeds() throws Exception {
+ PanelEntity panelEntity = createPanelEntity();
+ GltfEntity gltfEntity = createGltfEntity();
+ LoggingEntity loggingEntity = realityCoreRuntime.createLoggingEntity(new Pose());
+ Component component1 = mock(Component.class);
+ Component component2 = mock(FakeComponent.class);
+ when(component1.onAttach(any())).thenReturn(true);
+ when(component2.onAttach(any())).thenReturn(true);
+
+ assertThat(panelEntity.addComponent(component1)).isTrue();
+ assertThat(panelEntity.addComponent(component2)).isTrue();
+ verify(component1).onAttach(panelEntity);
+ verify(component2).onAttach(panelEntity);
+
+ assertThat(gltfEntity.addComponent(component1)).isTrue();
+ assertThat(gltfEntity.addComponent(component2)).isTrue();
+ verify(component1).onAttach(gltfEntity);
+ verify(component2).onAttach(gltfEntity);
+
+ assertThat(loggingEntity.addComponent(component1)).isTrue();
+ assertThat(loggingEntity.addComponent(component2)).isTrue();
+ verify(component1).onAttach(loggingEntity);
+ verify(component2).onAttach(loggingEntity);
+ }
+
+ @Test
+ public void removeAll_callsOnDetachOnAll() throws Exception {
+ PanelEntity panelEntity = createPanelEntity();
+ GltfEntity gltfEntity = createGltfEntity();
+ LoggingEntity loggingEntity = realityCoreRuntime.createLoggingEntity(new Pose());
+ Component component1 = mock(Component.class);
+ Component component2 = mock(FakeComponent.class);
+ when(component1.onAttach(any())).thenReturn(true);
+ when(component2.onAttach(any())).thenReturn(true);
+
+ assertThat(panelEntity.addComponent(component1)).isTrue();
+ assertThat(panelEntity.addComponent(component2)).isTrue();
+ verify(component1).onAttach(panelEntity);
+ verify(component2).onAttach(panelEntity);
+
+ panelEntity.removeAllComponents();
+ verify(component1).onDetach(panelEntity);
+ verify(component2).onDetach(panelEntity);
+
+ assertThat(gltfEntity.addComponent(component1)).isTrue();
+ assertThat(gltfEntity.addComponent(component2)).isTrue();
+ verify(component1).onAttach(gltfEntity);
+ verify(component2).onAttach(gltfEntity);
+
+ gltfEntity.removeAllComponents();
+ verify(component1).onDetach(gltfEntity);
+ verify(component2).onDetach(gltfEntity);
+
+ assertThat(loggingEntity.addComponent(component1)).isTrue();
+ assertThat(loggingEntity.addComponent(component2)).isTrue();
+ verify(component1).onAttach(loggingEntity);
+ verify(component2).onAttach(loggingEntity);
+
+ loggingEntity.removeAllComponents();
+ verify(component1).onDetach(loggingEntity);
+ verify(component2).onDetach(loggingEntity);
+ }
+
+ @Test
+ public void addSameComponentTwice_callsOnAttachTwice() throws Exception {
+ PanelEntity panelEntity = createPanelEntity();
+ GltfEntity gltfEntity = createGltfEntity();
+ LoggingEntity loggingEntity = realityCoreRuntime.createLoggingEntity(new Pose());
+ Component component = mock(Component.class);
+ when(component.onAttach(any())).thenReturn(true);
+
+ assertThat(panelEntity.addComponent(component)).isTrue();
+ assertThat(panelEntity.addComponent(component)).isTrue();
+ verify(component, times(2)).onAttach(panelEntity);
+
+ assertThat(gltfEntity.addComponent(component)).isTrue();
+ assertThat(gltfEntity.addComponent(component)).isTrue();
+ verify(component, times(2)).onAttach(gltfEntity);
+
+ assertThat(loggingEntity.addComponent(component)).isTrue();
+ assertThat(loggingEntity.addComponent(component)).isTrue();
+ verify(component, times(2)).onAttach(loggingEntity);
+ }
+
+ @Test
+ public void removeSameComponentTwice_callsOnDetachOnce() throws Exception {
+ PanelEntity panelEntity = createPanelEntity();
+ GltfEntity gltfEntity = createGltfEntity();
+ LoggingEntity loggingEntity = realityCoreRuntime.createLoggingEntity(new Pose());
+ Component component = mock(Component.class);
+ when(component.onAttach(any())).thenReturn(true);
+
+ assertThat(panelEntity.addComponent(component)).isTrue();
+ verify(component).onAttach(panelEntity);
+
+ panelEntity.removeComponent(component);
+ panelEntity.removeComponent(component);
+ verify(component).onDetach(panelEntity);
+
+ assertThat(gltfEntity.addComponent(component)).isTrue();
+ verify(component).onAttach(gltfEntity);
+
+ gltfEntity.removeComponent(component);
+ gltfEntity.removeComponent(component);
+ verify(component).onDetach(gltfEntity);
+
+ assertThat(loggingEntity.addComponent(component)).isTrue();
+ verify(component).onAttach(loggingEntity);
+
+ loggingEntity.removeComponent(component);
+ loggingEntity.removeComponent(component);
+ verify(component).onDetach(loggingEntity);
+ }
+
+ @Test
+ public void createInteractableComponent_returnsComponent() {
+ InputEventListener mockConsumer = mock(InputEventListener.class);
+ InteractableComponent interactableComponent =
+ realityCoreRuntime.createInteractableComponent(directExecutor(), mockConsumer);
+ assertThat(interactableComponent).isNotNull();
+ }
+
+ @Test
+ public void createPersistedAnchorEntity_returnsEntityInNominalCase() throws Exception {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.createAnchorFromUuid(any())).thenReturn(anchor);
+ assertThat(
+ realityCoreRuntime.createPersistedAnchorEntity(
+ UUID.randomUUID(), /* searchTimeout= */ Duration.ofSeconds(1)))
+ .isNotNull();
+ }
+
+ @Test
+ public void createPersistedAnchorEntity_returnsEntityForNullSession() throws Exception {
+ when(perceptionLibrary.getSession()).thenReturn(null);
+ assertThat(
+ realityCoreRuntime.createPersistedAnchorEntity(
+ UUID.randomUUID(), /* searchTimeout= */ Duration.ofSeconds(1)))
+ .isNotNull();
+ }
+
+ @Test
+ public void createPersistedAnchorEntity_returnsEntityForNullAnchor() throws Exception {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.createAnchorFromUuid(any())).thenReturn(null);
+ assertThat(
+ realityCoreRuntime.createPersistedAnchorEntity(
+ UUID.randomUUID(), /* searchTimeout= */ Duration.ofSeconds(1)))
+ .isNotNull();
+ }
+
+ @Test
+ public void createPersistedAnchorEntity_returnsEntityForNullAnchorToken() throws Exception {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.createAnchorFromUuid(any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(null);
+ UUID uuid = UUID.randomUUID();
+ assertThat(
+ realityCoreRuntime.createPersistedAnchorEntity(
+ uuid, /* searchTimeout= */ Duration.ofSeconds(1)))
+ .isNotNull();
+ verify(perceptionLibrary, times(3)).getSession();
+ verify(session).createAnchorFromUuid(uuid);
+ verify(anchor).getAnchorToken();
+ }
+
+ @Test
+ public void unpersistAnchor_failsWhenSessionIsNotInitialized() {
+ when(perceptionLibrary.getSession()).thenReturn(null);
+ assertThat(realityCoreRuntime.unpersistAnchor(UUID.randomUUID())).isFalse();
+ }
+
+ @Test
+ public void unpersistAnchor_sessionIsInitialized_operationSucceeds() {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ UUID uuid = UUID.randomUUID();
+ when(session.unpersistAnchor(uuid)).thenReturn(true);
+ assertThat(realityCoreRuntime.unpersistAnchor(uuid)).isTrue();
+ }
+
+ @Test
+ public void unpersistAnchor_sessionIsInitialized_operationFails() {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ UUID uuid = UUID.randomUUID();
+ when(session.unpersistAnchor(uuid)).thenReturn(false);
+ assertThat(realityCoreRuntime.unpersistAnchor(uuid)).isFalse();
+ }
+
+ @Test
+ public void createMovableComponent_returnsComponent() {
+ MovableComponent movableComponent =
+ realityCoreRuntime.createMovableComponent(
+ true, true, new HashSet<AnchorPlacement>(), true);
+ assertThat(movableComponent).isNotNull();
+ }
+
+ @Test
+ public void createAnchorPlacement_returnsAnchorPlacement() {
+ AnchorPlacement anchorPlacement =
+ realityCoreRuntime.createAnchorPlacementForPlanes(
+ ImmutableSet.of(PlaneType.ANY), ImmutableSet.of(PlaneSemantic.ANY));
+ assertThat(anchorPlacement).isNotNull();
+ }
+
+ @Test
+ public void createResizableComponent_returnsComponent() {
+ ResizableComponent resizableComponent =
+ realityCoreRuntime.createResizableComponent(
+ new Dimensions(0f, 0f, 0f), new Dimensions(5f, 5f, 5f));
+ assertThat(resizableComponent).isNotNull();
+ }
+
+ @Test
+ public void createPointerCaptureComponent_returnsComponent() {
+ PointerCaptureComponent pointerCaptureComponent =
+ realityCoreRuntime.createPointerCaptureComponent(
+ null, (inputEvent) -> {}, (state) -> {});
+ assertThat(pointerCaptureComponent).isNotNull();
+ }
+
+ @Test
+ public void dispose_clearsReformOptions() {
+ AndroidXrEntity entity = (AndroidXrEntity) createContentlessEntity();
+ FakeNode node = (FakeNode) entity.getNode();
+ ReformOptions reformOptions = entity.getReformOptions();
+ assertThat(reformOptions).isNotNull();
+ reformOptions.setEnabledReform(ReformOptions.ALLOW_MOVE | ReformOptions.ALLOW_RESIZE);
+ entity.dispose();
+ assertThat(node.getReformOptions().getEnabledReform()).isEqualTo(0);
+ assertThat(node.getReformOptions().getEventCallback()).isNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNull();
+ }
+
+ @Test
+ public void dispose_clearsParents() {
+ AndroidXrEntity entity = (AndroidXrEntity) createContentlessEntity();
+ entity.setParent(realityCoreRuntime.getActivitySpaceRootImpl());
+ assertThat(entity.getParent()).isNotNull();
+
+ entity.dispose();
+ assertThat(entity.getParent()).isNull();
+ }
+
+ @Test
+ public void setFullSpaceMode_callsExtensions() {
+ Bundle bundle = Bundle.EMPTY;
+ bundle = realityCoreRuntime.setFullSpaceMode(bundle);
+ assertThat(bundle).isNotNull();
+ }
+
+ @Test
+ public void setFullSpaceModeWithEnvironmentInherited_callsExtensions() {
+ Bundle bundle = Bundle.EMPTY;
+ bundle = realityCoreRuntime.setFullSpaceModeWithEnvironmentInherited(bundle);
+ assertThat(bundle).isNotNull();
+ }
+
+ @Test
+ public void setPreferredAspectRatio_callsExtensions() {
+ realityCoreRuntime.setPreferredAspectRatio(activity, 1.23f);
+ assertThat(fakeExtensions.getPreferredAspectRatio()).isEqualTo(1.23f);
+ }
+
+ @Test
+ public void createStereoSurface_returnsStereoSurface() {
+ // Not a great test, since it returns the (non-SplitEngine) StereoSurfaceEntityImpl
+ // and that throws this from its Ctor.
+ // TODO: b/366588688 - Properly test this path once SplitEngine is fully enabled.
+ assertThrows(
+ UnsupportedOperationException.class,
+ () ->
+ realityCoreRuntime.createStereoSurfaceEntity(
+ StereoSurfaceEntity.StereoMode.SIDE_BY_SIDE,
+ new Dimensions(1.0f, 1.0f, 1.0f),
+ new Pose(),
+ realityCoreRuntime.getActivitySpaceRootImpl()));
+ }
+
+ @Test
+ public void getSurfaceFromStereoSurface_returnsSurface() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> fakeImpressApi.getSurfaceFromStereoSurface(1));
+ }
+
+ @Test
+ public void setStereoModeForStereoSurface_callsExtensions() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ fakeImpressApi.setStereoModeForStereoSurface(
+ 1, StereoSurfaceEntity.StereoMode.SIDE_BY_SIDE));
+ }
+
+ @Test
+ public void injectRootNodeAndTaskWindowLeashNode_runtimeImplUsesThoseNodes() {
+ FakeNode rootNode = (FakeNode) fakeExtensions.createNode();
+ FakeNode taskWindowLeashNode = (FakeNode) fakeExtensions.createNode();
+ JxrPlatformAdapterAxr runtime =
+ JxrPlatformAdapterAxr.create(
+ activity,
+ fakeExecutor,
+ fakeExtensions,
+ fakeImpressApi,
+ new EntityManager(),
+ perceptionLibrary,
+ splitEngineSubspaceManager,
+ splitEngineRenderer,
+ rootNode,
+ taskWindowLeashNode,
+ /* useSplitEngine= */ false);
+
+ assertThat(((AndroidXrEntity) runtime.getActivitySpace()).getNode()).isEqualTo(rootNode);
+ assertThat(((AndroidXrEntity) runtime.getMainPanelEntity()).getNode())
+ .isEqualTo(taskWindowLeashNode);
+ }
+
+ @Test
+ public void dispose_clearsResources() {
+ AndroidXrEntity entity = (AndroidXrEntity) createContentlessEntity();
+ FakeNode node = (FakeNode) entity.getNode();
+ assertThat(node).isNotNull();
+ assertThat(node.getParent()).isNotNull();
+
+ realityCoreRuntime.dispose();
+ assertThat(node.getParent()).isNull();
+ assertThat(fakeExtensions.getSpatialStateCallback()).isNull();
+ assertThat(fakeExtensions.getFakeNodeForMainWindow()).isNull();
+ assertThat(fakeExtensions.getFakeTaskNode()).isNull();
+ }
+
+ interface FakeComponent extends Component {}
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MainPanelEntityImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MainPanelEntityImplTest.java
new file mode 100644
index 0000000..b643154
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MainPanelEntityImplTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.PixelDimensions;
+import androidx.xr.scenecore.impl.perception.PerceptionLibrary;
+import androidx.xr.scenecore.impl.perception.Session;
+import androidx.xr.scenecore.testing.FakeImpressApi;
+import androidx.xr.scenecore.testing.FakeScheduledExecutorService;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+
+import com.google.androidxr.splitengine.SplitEngineSubspaceManager;
+import com.google.ar.imp.view.splitengine.ImpSplitEngineRenderer;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.android.controller.ActivityController;
+
+@RunWith(RobolectricTestRunner.class)
+public class MainPanelEntityImplTest {
+ private final FakeXrExtensions fakeExtensions = new FakeXrExtensions();
+ private final FakeImpressApi fakeImpressApi = new FakeImpressApi();
+ private final ActivityController<Activity> activityController =
+ Robolectric.buildActivity(Activity.class);
+ private final Activity hostActivity = activityController.create().start().get();
+ private final FakeScheduledExecutorService fakeExecutor = new FakeScheduledExecutorService();
+ private final PerceptionLibrary perceptionLibrary = Mockito.mock(PerceptionLibrary.class);
+ SplitEngineSubspaceManager splitEngineSubspaceManager =
+ Mockito.mock(SplitEngineSubspaceManager.class);
+ ImpSplitEngineRenderer splitEngineRenderer = Mockito.mock(ImpSplitEngineRenderer.class);
+ private JxrPlatformAdapterAxr testRuntime;
+ private MainPanelEntityImpl mainPanelEntity;
+
+ @Before
+ public void setUp() {
+ when(perceptionLibrary.initSession(eq(hostActivity), anyInt(), eq(fakeExecutor)))
+ .thenReturn(immediateFuture(Mockito.mock(Session.class)));
+
+ testRuntime =
+ JxrPlatformAdapterAxr.create(
+ hostActivity,
+ fakeExecutor,
+ fakeExtensions,
+ fakeImpressApi,
+ new EntityManager(),
+ perceptionLibrary,
+ splitEngineSubspaceManager,
+ splitEngineRenderer,
+ /* useSplitEngine= */ false);
+
+ mainPanelEntity = (MainPanelEntityImpl) testRuntime.getMainPanelEntity();
+ }
+
+ @Test
+ public void runtimeGetMainPanelEntity_returnsPanelEntityImpl() {
+ assertThat(mainPanelEntity).isNotNull();
+ }
+
+ @Test
+ public void mainPanelEntitySetPixelDimensions_callsExtensions() {
+ PixelDimensions kTestPixelDimensions = new PixelDimensions(14, 14);
+ mainPanelEntity.setPixelDimensions(kTestPixelDimensions);
+ assertThat(fakeExtensions.getMainWindowWidth()).isEqualTo(kTestPixelDimensions.width);
+ assertThat(fakeExtensions.getMainWindowHeight()).isEqualTo(kTestPixelDimensions.height);
+ }
+
+ @Test
+ public void mainPanelEntitySetSize_callsExtensions() {
+ // TODO(b/352630025): remove this once setSize is removed.
+ // This should have the same effect as setPixelDimensions, except that it has to convert
+ // from Dimensions to PixelDimensions, so it casts float to int.
+ Dimensions kTestDimensions = new Dimensions(123.0f, 123.0f, 123.0f);
+ mainPanelEntity.setSize(kTestDimensions);
+ assertThat(fakeExtensions.getMainWindowWidth()).isEqualTo((int) kTestDimensions.width);
+ assertThat(fakeExtensions.getMainWindowWidth()).isEqualTo((int) kTestDimensions.height);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/Matrix4ExtTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/Matrix4ExtTest.kt
new file mode 100644
index 0000000..9b53a03
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/Matrix4ExtTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.xr.runtime.math.Matrix4
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Vector3
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class Matrix4ExtTest {
+ @Test
+ fun unscaled_returnsUnscaledMatrix() {
+ // Column major, right handed 4x4 Transformation Matrix with
+ // translation of (4, 8, 12) and rotation 90 (@) around Z axis, and scale of 3.3.
+ // -- cos(@), sin(@), 0, 0
+ // -- -sin(@), cos(@), 0, 0
+ // -- 0, 0, 1, 0
+ // -- tx, ty, tz, 1
+ val underTest =
+ Matrix4(
+ floatArrayOf(0f, 3.3f, 0f, 0f, -3.3f, 0f, 0f, 0f, 0f, 0f, 3.3f, 0f, 4f, 8f, 12f, 1f)
+ )
+ val underTestUnscaled = underTest.getUnscaled()
+ val expected =
+ Matrix4(floatArrayOf(0f, 1f, 0f, 0f, -1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 4f, 8f, 12f, 1f))
+ assertMatrix(underTestUnscaled, expected)
+ }
+
+ @Test
+ fun unscaled_withNonUniformScale_returnsUnscaledMatrix() {
+ val underTest =
+ Matrix4.fromTrs(Vector3(1f, 2f, 3f), Quaternion(1f, 2f, 3f, 4f), Vector3(2f, 3f, 4f))
+ val underTestUnscaled = underTest.getUnscaled()
+ assertMatrix(
+ underTestUnscaled,
+ Matrix4.fromTrs(Vector3(1f, 2f, 3f), Quaternion(1f, 2f, 3f, 4f), Vector3(1f, 1f, 1f)),
+ )
+ }
+
+ @Test
+ fun unscaled_withIdentityTranslationAndRotation_returnsUnscaledMatrix() {
+ val underTest = Matrix4.fromTrs(Vector3.Zero, Quaternion.Identity, Vector3(2f, 3f, 4f))
+ val underTestUnscaled = underTest.getUnscaled()
+ assertMatrix(
+ underTestUnscaled,
+ Matrix4.fromTrs(Vector3.Zero, Quaternion.Identity, Vector3(1f, 1f, 1f)),
+ )
+ }
+
+ private fun assertMatrix(matrix: Matrix4, expected: Matrix4) {
+ assertThat(matrix.data.size).isEqualTo(expected.data.size)
+ for (i in matrix.data.indices) {
+ assertThat(matrix.data[i]).isWithin(1e-5f).of(expected.data[i])
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MediaPlayerExtensionsWrapperImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MediaPlayerExtensionsWrapperImplTest.java
new file mode 100644
index 0000000..7d8aaac
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MediaPlayerExtensionsWrapperImplTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.media.MediaPlayer;
+
+import androidx.xr.extensions.media.SpatializerExtensions;
+import androidx.xr.extensions.media.XrSpatialAudioExtensions;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.scenecore.JxrPlatformAdapter;
+import androidx.xr.scenecore.JxrPlatformAdapter.MediaPlayerExtensionsWrapper;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeMediaPlayerExtensions;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class MediaPlayerExtensionsWrapperImplTest {
+ FakeXrExtensions fakeXrExtensions;
+ XrSpatialAudioExtensions spatialAudioExtensions;
+ FakeMediaPlayerExtensions fakeMediaPlayerExtensions;
+
+ @Before
+ public void setUp() {
+ fakeXrExtensions = new FakeXrExtensions();
+ spatialAudioExtensions = fakeXrExtensions.fakeSpatialAudioExtensions;
+ fakeMediaPlayerExtensions =
+ (FakeMediaPlayerExtensions) spatialAudioExtensions.getMediaPlayerExtensions();
+ }
+
+ @Test
+ public void setPointSourceAttr_callsExtensionsSetPointSourceAttr() {
+ MediaPlayer mediaPlayer = new MediaPlayer();
+
+ Node fakeNode = new FakeXrExtensions().createNode();
+ AndroidXrEntity entity = mock(AndroidXrEntity.class);
+ when(entity.getNode()).thenReturn(fakeNode);
+
+ JxrPlatformAdapter.PointSourceAttributes expectedRtAttr =
+ new JxrPlatformAdapter.PointSourceAttributes(entity);
+
+ MediaPlayerExtensionsWrapper wrapper =
+ new MediaPlayerExtensionsWrapperImpl(fakeMediaPlayerExtensions);
+ wrapper.setPointSourceAttributes(mediaPlayer, expectedRtAttr);
+
+ assertThat(fakeMediaPlayerExtensions.getPointSourceAttributes().getNode())
+ .isEqualTo(fakeNode);
+ }
+
+ @Test
+ public void setSoundFieldAttr_callsExtensionsSetSoundFieldAttr() {
+ MediaPlayer mediaPlayer = new MediaPlayer();
+
+ int expectedAmbisonicOrder = SpatializerExtensions.AMBISONICS_ORDER_THIRD_ORDER;
+ JxrPlatformAdapter.SoundFieldAttributes expectedRtAttr =
+ new JxrPlatformAdapter.SoundFieldAttributes(
+ JxrPlatformAdapter.SpatializerConstants.AMBISONICS_ORDER_THIRD_ORDER);
+
+ MediaPlayerExtensionsWrapper wrapper =
+ new MediaPlayerExtensionsWrapperImpl(fakeMediaPlayerExtensions);
+ wrapper.setSoundFieldAttributes(mediaPlayer, expectedRtAttr);
+
+ assertThat(fakeMediaPlayerExtensions.getSoundFieldAttributes().getAmbisonicsOrder())
+ .isEqualTo(expectedAmbisonicOrder);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MediaUtilsTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MediaUtilsTest.java
new file mode 100644
index 0000000..213e4b1
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MediaUtilsTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import androidx.xr.extensions.media.PointSourceAttributes;
+import androidx.xr.extensions.media.SoundFieldAttributes;
+import androidx.xr.extensions.media.SpatializerExtensions;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.scenecore.JxrPlatformAdapter;
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatializerConstants;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public final class MediaUtilsTest {
+
+ @Test
+ public void convertPointSourceAttributes_returnsExtensionsAttributes() {
+ Node expected = new FakeXrExtensions().createNode();
+
+ AndroidXrEntity entity = mock(AndroidXrEntity.class);
+ when(entity.getNode()).thenReturn(expected);
+ JxrPlatformAdapter.PointSourceAttributes rtAttributes =
+ new JxrPlatformAdapter.PointSourceAttributes(entity);
+
+ PointSourceAttributes result =
+ MediaUtils.convertPointSourceAttributesToExtensions(rtAttributes);
+
+ assertThat(result.getNode()).isSameInstanceAs(expected);
+ }
+
+ @Test
+ public void convertSoundFieldAttributes_returnsExtensionsAttributes() {
+ int extAmbisonicsOrder = SpatializerExtensions.AMBISONICS_ORDER_THIRD_ORDER;
+
+ JxrPlatformAdapter.SoundFieldAttributes rtAttributes =
+ new JxrPlatformAdapter.SoundFieldAttributes(
+ SpatializerConstants.AMBISONICS_ORDER_THIRD_ORDER);
+
+ SoundFieldAttributes result =
+ MediaUtils.convertSoundFieldAttributesToExtensions(rtAttributes);
+
+ assertThat(result.getAmbisonicsOrder()).isEqualTo(extAmbisonicsOrder);
+ }
+
+ @Test
+ public void convertAmbisonicsOrderToExtensions_returnsExtensionsAmbisonicsOrder() {
+ assertThat(
+ MediaUtils.convertAmbisonicsOrderToExtensions(
+ SpatializerConstants.AMBISONICS_ORDER_FIRST_ORDER))
+ .isEqualTo(SpatializerExtensions.AMBISONICS_ORDER_FIRST_ORDER);
+ assertThat(
+ MediaUtils.convertAmbisonicsOrderToExtensions(
+ SpatializerConstants.AMBISONICS_ORDER_SECOND_ORDER))
+ .isEqualTo(SpatializerExtensions.AMBISONICS_ORDER_SECOND_ORDER);
+ assertThat(
+ MediaUtils.convertAmbisonicsOrderToExtensions(
+ SpatializerConstants.AMBISONICS_ORDER_THIRD_ORDER))
+ .isEqualTo(SpatializerExtensions.AMBISONICS_ORDER_THIRD_ORDER);
+ }
+
+ @Test
+ public void convertAmbisonicsOrderToExtensions_throwsExceptionForInvalidValue() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> MediaUtils.convertAmbisonicsOrderToExtensions(100));
+ }
+
+ @Test
+ public void convertExtensionsToSourceType_returnsRtSourceType() {
+ assertThat(
+ MediaUtils.convertExtensionsToSourceType(
+ SpatializerExtensions.SOURCE_TYPE_BYPASS))
+ .isEqualTo(SpatializerConstants.SOURCE_TYPE_BYPASS);
+ assertThat(
+ MediaUtils.convertExtensionsToSourceType(
+ SpatializerExtensions.SOURCE_TYPE_POINT_SOURCE))
+ .isEqualTo(SpatializerConstants.SOURCE_TYPE_POINT_SOURCE);
+ assertThat(
+ MediaUtils.convertExtensionsToSourceType(
+ SpatializerExtensions.SOURCE_TYPE_SOUND_FIELD))
+ .isEqualTo(SpatializerConstants.SOURCE_TYPE_SOUND_FIELD);
+ }
+
+ @Test
+ public void convertExtensionsToSourceType_throwsExceptionForInvalidValue() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> MediaUtils.convertExtensionsToSourceType(100));
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MovableComponentImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MovableComponentImplTest.java
new file mode 100644
index 0000000..2cef0e9
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MovableComponentImplTest.java
@@ -0,0 +1,2480 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static androidx.xr.runtime.testing.math.MathAssertions.assertPose;
+import static androidx.xr.runtime.testing.math.MathAssertions.assertVector3;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.Binder;
+import android.os.IBinder;
+import android.view.Display;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+
+import androidx.xr.extensions.node.Mat4f;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.extensions.node.Quatf;
+import androidx.xr.extensions.node.ReformEvent;
+import androidx.xr.extensions.node.ReformOptions;
+import androidx.xr.extensions.node.Vec3;
+import androidx.xr.runtime.math.Matrix4;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Quaternion;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter;
+import androidx.xr.scenecore.JxrPlatformAdapter.AnchorEntity;
+import androidx.xr.scenecore.JxrPlatformAdapter.AnchorPlacement;
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.MovableComponent;
+import androidx.xr.scenecore.JxrPlatformAdapter.MoveEvent;
+import androidx.xr.scenecore.JxrPlatformAdapter.MoveEventListener;
+import androidx.xr.scenecore.JxrPlatformAdapter.PanelEntity;
+import androidx.xr.scenecore.JxrPlatformAdapter.PixelDimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.PlaneSemantic;
+import androidx.xr.scenecore.JxrPlatformAdapter.PlaneType;
+import androidx.xr.scenecore.impl.perception.Anchor;
+import androidx.xr.scenecore.impl.perception.PerceptionLibrary;
+import androidx.xr.scenecore.impl.perception.Plane;
+import androidx.xr.scenecore.impl.perception.Plane.PlaneData;
+import androidx.xr.scenecore.impl.perception.Session;
+import androidx.xr.scenecore.testing.FakeImpressApi;
+import androidx.xr.scenecore.testing.FakeScheduledExecutorService;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeNode;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeNodeTransform;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeReformEvent;
+
+import com.google.androidxr.splitengine.SplitEngineSubspaceManager;
+import com.google.ar.imp.view.splitengine.ImpSplitEngineRenderer;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.truth.Expect;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.android.controller.ActivityController;
+
+import java.util.Objects;
+
+@RunWith(RobolectricTestRunner.class)
+public class MovableComponentImplTest {
+ private final ActivityController<Activity> activityController =
+ Robolectric.buildActivity(Activity.class);
+ private final Activity activity = activityController.create().start().get();
+ private final FakeScheduledExecutorService fakeExecutor = new FakeScheduledExecutorService();
+ private final PerceptionLibrary perceptionLibrary = mock(PerceptionLibrary.class);
+ private final FakeXrExtensions fakeExtensions = new FakeXrExtensions();
+ private final FakeImpressApi fakeImpressApi = new FakeImpressApi();
+ private final EntityManager entityManager = new EntityManager();
+
+ private final SplitEngineSubspaceManager splitEngineSubspaceManager =
+ Mockito.mock(SplitEngineSubspaceManager.class);
+ private final ImpSplitEngineRenderer splitEngineRenderer =
+ Mockito.mock(ImpSplitEngineRenderer.class);
+ private JxrPlatformAdapter fakeRuntime;
+ private ActivitySpaceImpl activitySpaceImpl;
+ private Node activitySpaceNode;
+ private final AndroidXrEntity activitySpaceRoot = Mockito.mock(AndroidXrEntity.class);
+ private PerceptionSpaceActivityPoseImpl perceptionSpaceActivityPose;
+ private final PanelShadowRenderer panelShadowRenderer = Mockito.mock(PanelShadowRenderer.class);
+
+ @Rule public final Expect expect = Expect.create();
+
+ @Before
+ public void setUp() {
+ when(perceptionLibrary.initSession(eq(activity), anyInt(), eq(fakeExecutor)))
+ .thenReturn(immediateFuture(mock(Session.class)));
+ fakeRuntime =
+ JxrPlatformAdapterAxr.create(
+ activity,
+ fakeExecutor,
+ fakeExtensions,
+ fakeImpressApi,
+ entityManager,
+ perceptionLibrary,
+ splitEngineSubspaceManager,
+ splitEngineRenderer,
+ /* useSplitEngine= */ false);
+ activitySpaceImpl = (ActivitySpaceImpl) fakeRuntime.getActivitySpace();
+ activitySpaceNode = activitySpaceImpl.getNode();
+ perceptionSpaceActivityPose =
+ (PerceptionSpaceActivityPoseImpl) fakeRuntime.getPerceptionSpaceActivityPose();
+ }
+
+ private Entity createTestEntity() {
+ return fakeRuntime.createEntity(new Pose(), "test", fakeRuntime.getActivitySpace());
+ }
+
+ private PanelEntity createTestPanelEntity() {
+ Display display = activity.getSystemService(DisplayManager.class).getDisplays()[0];
+ Context displayContext = activity.createDisplayContext(display);
+ View view = new View(displayContext);
+ view.setLayoutParams(new LayoutParams(640, 480));
+ SurfaceControlViewHost surfaceControlViewHost =
+ new SurfaceControlViewHost(
+ displayContext,
+ Objects.requireNonNull(displayContext.getDisplay()),
+ new Binder());
+ surfaceControlViewHost.setView(view, 10, 10);
+ Node node = fakeExtensions.createNode();
+
+ PanelEntityImpl panelEntity =
+ new PanelEntityImpl(
+ node,
+ fakeExtensions,
+ entityManager,
+ surfaceControlViewHost,
+ new PixelDimensions(10, 10),
+ fakeExecutor);
+ panelEntity.setParent(activitySpaceImpl);
+ return panelEntity;
+ }
+
+ private void setActivitySpacePose(Pose pose, float scale) {
+ Matrix4 poseMatrix = Matrix4.fromPose(pose);
+ Matrix4 scaleMatrix = Matrix4.fromScale(scale);
+ Matrix4 scaledPoseMatrix = poseMatrix.times(scaleMatrix);
+ Mat4f mat4f = new Mat4f(scaledPoseMatrix.getData());
+ FakeNodeTransform nodeTransformEvent = new FakeNodeTransform(mat4f);
+
+ ((FakeNode) activitySpaceNode).sendTransformEvent(nodeTransformEvent);
+ fakeExecutor.runAll();
+ }
+
+ private ImmutableSet<JxrPlatformAdapter.AnchorPlacement> createAnyAnchorPlacement() {
+ JxrPlatformAdapter.AnchorPlacement anchorPlacement =
+ fakeRuntime.createAnchorPlacementForPlanes(
+ ImmutableSet.of(PlaneType.ANY), ImmutableSet.of(PlaneSemantic.ANY));
+ return ImmutableSet.of(anchorPlacement);
+ }
+
+ @Test
+ public void addMovableComponent_addsReformOptionsToNode() {
+ Entity entity = createTestEntity();
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ false,
+ /* scaleInZ= */ false,
+ /* anchorPlacement= */ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+
+ assertThat(node.getReformOptions().getEnabledReform()).isEqualTo(ReformOptions.ALLOW_MOVE);
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(ReformOptions.FLAG_POSE_RELATIVE_TO_PARENT);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+ }
+
+ @Test
+ public void addMovableComponent_addsSystemMovableFlagToNode() {
+ Entity entity = createTestEntity();
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ false,
+ /* anchorPlacement= */ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+
+ assertThat(node.getReformOptions().getEnabledReform()).isEqualTo(ReformOptions.ALLOW_MOVE);
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(
+ ReformOptions.FLAG_ALLOW_SYSTEM_MOVEMENT
+ | ReformOptions.FLAG_POSE_RELATIVE_TO_PARENT);
+ }
+
+ @Test
+ public void addMovableComponent_addsScaleInZFlagToNode() {
+ Entity entity = createTestEntity();
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ false,
+ /* scaleInZ= */ true,
+ /* anchorPlacement= */ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+
+ assertThat(node.getReformOptions().getEnabledReform()).isEqualTo(ReformOptions.ALLOW_MOVE);
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(
+ ReformOptions.FLAG_SCALE_WITH_DISTANCE
+ | ReformOptions.FLAG_POSE_RELATIVE_TO_PARENT);
+ }
+
+ @Test
+ public void addMovableComponent_addsAllFlagsToNode() {
+ Entity entity = createTestEntity();
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ /* anchorPlacement= */ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+
+ assertThat(node.getReformOptions().getEnabledReform()).isEqualTo(ReformOptions.ALLOW_MOVE);
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(
+ ReformOptions.FLAG_SCALE_WITH_DISTANCE
+ | ReformOptions.FLAG_ALLOW_SYSTEM_MOVEMENT
+ | ReformOptions.FLAG_POSE_RELATIVE_TO_PARENT);
+ }
+
+ @Test
+ public void setSystemMovableFlag_alsoUpdatesEntityPoseAndScale() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ MovableComponentImpl movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ /* anchorPlacement= */ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ Pose expectedPose =
+ new Pose(new Vector3(2f, 2f, 2f), new Quaternion(0.5f, 0.5f, 0.5f, 0.5f));
+ Vector3 expectedScale = new Vector3(1.2f, 1.2f, 1.2f);
+
+ entity.setPose(new Pose(new Vector3(1f, 1f, 1f), new Quaternion(0f, 0f, 0f, 1f)));
+ entity.setScale(new Vector3(1f, 1f, 1f));
+
+ FakeNode node = (FakeNode) entity.getNode();
+ FakeReformEvent reformEvent = new FakeReformEvent();
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setProposedPosition(new Vec3(2f, 2f, 2f));
+ reformEvent.setProposedOrientation(new Quatf(0.5f, 0.5f, 0.5f, 0.5f));
+ reformEvent.setProposedScale(new Vec3(1.2f, 1.2f, 1.2f));
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+
+ expect.that(entity.getPose()).isEqualTo(expectedPose);
+ expect.that(entity.getScale()).isEqualTo(expectedScale);
+ }
+
+ @Test
+ public void systemMovableFlagNotSet_doesNotUpdateEntityPoseAndScale() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ false,
+ /* scaleInZ= */ true,
+ /* anchorPlacement= */ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ Pose expectedPose = new Pose(new Vector3(1f, 1f, 1f), new Quaternion(0f, 0f, 0f, 1f));
+ Vector3 expectedScale = new Vector3(1f, 1f, 1f);
+ entity.setPose(new Pose(new Vector3(1f, 1f, 1f), new Quaternion(0f, 0f, 0f, 1f)));
+ entity.setScale(new Vector3(1f, 1f, 1f));
+
+ FakeNode node = (FakeNode) entity.getNode();
+ FakeReformEvent reformEvent = new FakeReformEvent();
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setProposedPosition(new Vec3(2f, 2f, 2f));
+ reformEvent.setProposedOrientation(new Quatf(0.5f, 0.5f, 0.5f, 0.5f));
+ reformEvent.setProposedScale(new Vec3(1.2f, 1.2f, 1.2f));
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+
+ expect.that(entity.getPose()).isEqualTo(expectedPose);
+ expect.that(entity.getScale()).isEqualTo(expectedScale);
+ }
+
+ @Test
+ public void setSizeOnMovableComponent_setsSizeOnNodeReformOptions() {
+ Entity entity = createTestEntity();
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ /* anchorPlacement= */ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+
+ movableComponent.setSize(new Dimensions(2f, 2f, 2f));
+ assertThat(node.getReformOptions().getCurrentSize().x).isEqualTo(2f);
+ assertThat(node.getReformOptions().getCurrentSize().y).isEqualTo(2f);
+ assertThat(node.getReformOptions().getCurrentSize().z).isEqualTo(2f);
+ }
+
+ @Test
+ public void scaleWithDistanceOnMovableComponent_defaultsToDefaultMode() {
+ Entity entity = createTestEntity();
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ /* anchorPlacement= */ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+
+ // Default value for scaleWithDistanceMode is DEFAULT.
+ assertThat(movableComponent.getScaleWithDistanceMode())
+ .isEqualTo(MovableComponent.ScaleWithDistanceMode.DEFAULT);
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ assertThat(node.getReformOptions().getScaleWithDistanceMode())
+ .isEqualTo(ReformOptions.SCALE_WITH_DISTANCE_MODE_DEFAULT);
+ }
+
+ @Test
+ public void setScaleWithDistanceOnMovableComponent_setsScaleWithDistanceOnNodeReformOptions() {
+ Entity entity = createTestEntity();
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ /* anchorPlacement= */ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+
+ movableComponent.setScaleWithDistanceMode(MovableComponent.ScaleWithDistanceMode.DMM);
+
+ assertThat(movableComponent.getScaleWithDistanceMode())
+ .isEqualTo(MovableComponent.ScaleWithDistanceMode.DMM);
+ assertThat(node.getReformOptions().getScaleWithDistanceMode())
+ .isEqualTo(ReformOptions.SCALE_WITH_DISTANCE_MODE_DMM);
+ }
+
+ @Test
+ public void setPropertiesOnMovableComponentAttachLater_setsPropertiesOnNodeReformOptions() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ MovableComponentImpl movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ /* anchorPlacement= */ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ movableComponent.setSize(new Dimensions(2f, 2f, 2f));
+ MoveEventListener mockMoveEventListener = mock(MoveEventListener.class);
+ movableComponent.addMoveEventListener(directExecutor(), mockMoveEventListener);
+ assertThat(movableComponent.reformEventConsumer).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) entity.getNode();
+
+ assertThat(node.getReformOptions().getCurrentSize().x).isEqualTo(2f);
+ assertThat(node.getReformOptions().getCurrentSize().y).isEqualTo(2f);
+ assertThat(node.getReformOptions().getCurrentSize().z).isEqualTo(2f);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+ assertThat(entity.reformEventConsumerMap).isNotEmpty();
+ }
+
+ @Test
+ public void addMoveEventListener_onlyInvokedOnMoveEvent() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ Vector3 initialTranslation = new Vector3(1f, 2f, 3f);
+ Vector3 initialScale = new Vector3(1.1f, 1.1f, 1.1f);
+ entity.setPose(new Pose(initialTranslation, new Quaternion()));
+ entity.setScale(initialScale);
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ /* anchorPlacement= */ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) entity.getNode();
+ MoveEventListener mockMoveEventListener = mock(MoveEventListener.class);
+
+ movableComponent.addMoveEventListener(directExecutor(), mockMoveEventListener);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+ assertThat(entity.reformEventConsumerMap).isNotEmpty();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+ reformEvent.setType(ReformEvent.REFORM_TYPE_RESIZE);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ verify(mockMoveEventListener, never()).onMoveEvent(any());
+
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ ArgumentCaptor<MoveEvent> moveEventCaptor = ArgumentCaptor.forClass(MoveEvent.class);
+ verify(mockMoveEventListener).onMoveEvent(moveEventCaptor.capture());
+ MoveEvent moveEvent = moveEventCaptor.getValue();
+ assertThat(moveEvent.previousPose.getTranslation()).isEqualTo(initialTranslation);
+ assertThat(moveEvent.previousScale).isEqualTo(initialScale);
+ }
+
+ @Test
+ public void addMoveEventListenerWithExecutor_invokesListenerOnGivenExecutor() {
+ Entity entity = createTestEntity();
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ /* anchorPlacement= */ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ MoveEventListener mockMoveEventListener = mock(MoveEventListener.class);
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ movableComponent.addMoveEventListener(executorService, mockMoveEventListener);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+ verify(mockMoveEventListener).onMoveEvent(any());
+ }
+
+ @Test
+ public void addMoveEventListenerMultiple_invokesAllListeners() {
+ Entity entity = createTestEntity();
+ MovableComponentImpl movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ /* anchorPlacement= */ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ MoveEventListener mockMoveEventListener1 = mock(MoveEventListener.class);
+ MoveEventListener mockMoveEventListener2 = mock(MoveEventListener.class);
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ movableComponent.addMoveEventListener(executorService, mockMoveEventListener1);
+ movableComponent.addMoveEventListener(executorService, mockMoveEventListener2);
+ FakeReformEvent reformEvent = new FakeReformEvent();
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ verify(mockMoveEventListener1).onMoveEvent(any());
+ verify(mockMoveEventListener2).onMoveEvent(any());
+ }
+
+ @Test
+ public void removeMoveEventListenerMultiple_removesGivenListener() {
+ Entity entity = createTestEntity();
+ MovableComponentImpl movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ /* anchorPlacement= */ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ MoveEventListener mockMoveEventListener1 = mock(MoveEventListener.class);
+ MoveEventListener mockMoveEventListener2 = mock(MoveEventListener.class);
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ movableComponent.addMoveEventListener(executorService, mockMoveEventListener1);
+ movableComponent.addMoveEventListener(executorService, mockMoveEventListener2);
+ FakeReformEvent reformEvent = new FakeReformEvent();
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ fakeExecutor.runAll();
+ executorService.runAll();
+
+ // Verify both listeners are invoked.
+ verify(mockMoveEventListener1).onMoveEvent(any());
+ verify(mockMoveEventListener2).onMoveEvent(any());
+
+ movableComponent.removeMoveEventListener(mockMoveEventListener1);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // The first listener, which we removed, should not be invoked again.
+ verify(mockMoveEventListener1).onMoveEvent(any());
+ verify(mockMoveEventListener2, times(2)).onMoveEvent(any());
+ }
+
+ @Test
+ public void removeMovableComponent_clearsMoveReformOptionsAndMoveEventListeners() {
+ Entity entity = createTestEntity();
+ MovableComponentImpl movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ /* anchorPlacement= */ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ MoveEventListener mockMoveEventListener = mock(MoveEventListener.class);
+
+ movableComponent.addMoveEventListener(directExecutor(), mockMoveEventListener);
+ assertThat(movableComponent.reformEventConsumer).isNotNull();
+ assertThat(((AndroidXrEntity) entity).reformEventConsumerMap).isNotEmpty();
+
+ entity.removeComponent(movableComponent);
+ assertThat(node.getReformOptions().getEnabledReform() & ReformOptions.ALLOW_MOVE)
+ .isEqualTo(0);
+ assertThat(
+ node.getReformOptions().getFlags()
+ & (ReformOptions.FLAG_SCALE_WITH_DISTANCE
+ | ReformOptions.FLAG_ALLOW_SYSTEM_MOVEMENT))
+ .isEqualTo(0);
+ assertThat(movableComponent.reformEventConsumer).isNull();
+ assertThat(((AndroidXrEntity) entity).reformEventConsumerMap).isEmpty();
+ }
+
+ @Test
+ public void movableComponent_canAttachAgainAfterDetach() {
+ Entity entity = createTestEntity();
+ assertThat(entity).isNotNull();
+ MovableComponentImpl movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ /* anchorPlacement= */ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ entity.removeComponent(movableComponent);
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ }
+
+ @Test
+ public void anchorable_updatesThePoseBasedOnPlanes() {
+ // Set the activity space pose to be 1 unit to the left of the origin.
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), 1f);
+ Session session = Mockito.mock(Session.class);
+ Plane plane = mock(Plane.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+
+ // Create a perception plane that is 2 units above the origin.
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(0f, 2f, 0f, 0f, 0f, 0f, 1f);
+ PlaneData planeData =
+ new PlaneData(
+ perceptionPose,
+ 1f,
+ 1f,
+ Plane.Type.HORIZONTAL_UPWARD_FACING.intValue,
+ Plane.Label.FLOOR.intValue);
+ when(plane.getData(any())).thenReturn(planeData);
+
+ Entity entity = createTestEntity();
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ false,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ TestMoveEventListener moveEventListener = new TestMoveEventListener();
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ movableComponent.addMoveEventListener(executorService, moveEventListener);
+ // The reform options for parenting and moving should not be set when it is anchorable.
+ assertThat(node.getReformOptions().getFlags()).isEqualTo(0);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ // Put the proposed position at 1 above the origin. so it would need to move up 1 unit to
+ // be on the plane.
+ reformEvent.setProposedPosition(new Vec3(1f, 1f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_ONGOING);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // The expected position should be 3 unit above the activity in order to rest on the plane.
+ // It
+ // is 3 units because the activity space is 1 unit below of the origin and the plane is 2
+ // units
+ // above it.
+ Pose expectedPosition = new Pose(new Vector3(1f, 3f, 1f), new Quaternion(0f, 0f, 0f, 1f));
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ assertThat(moveEventListener.callCount).isEqualTo(1);
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isNull();
+
+ // The panel shadow renderer should have no interaction.
+ verify(panelShadowRenderer, never()).updatePanelPose(any(), any(), any());
+ verify(panelShadowRenderer, never()).destroy();
+ verify(panelShadowRenderer, never()).hidePlane();
+
+ // The pose should have moved since the systemMovable is true.
+ assertPose(entity.getPose(), expectedPosition);
+ }
+
+ @Test
+ public void anchorable_nullParent_updatesThePoseBasedOnPlanes() {
+ // Set the activity space pose to be 1 unit to the left of the origin.
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), 1f);
+ Session session = Mockito.mock(Session.class);
+ Plane plane = mock(Plane.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+
+ // Create a perception plane that is 2 units above the origin.
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(0f, 2f, 0f, 0f, 0f, 0f, 1f);
+ PlaneData planeData =
+ new PlaneData(
+ perceptionPose,
+ 1f,
+ 1f,
+ Plane.Type.HORIZONTAL_UPWARD_FACING.intValue,
+ Plane.Label.FLOOR.intValue);
+ when(plane.getData(any())).thenReturn(planeData);
+
+ Entity entity = createTestEntity();
+ entity.setParent(null);
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ false,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ TestMoveEventListener moveEventListener = new TestMoveEventListener();
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ movableComponent.addMoveEventListener(executorService, moveEventListener);
+ // The reform options for parenting and moving should not be set when it is anchorable.
+ assertThat(node.getReformOptions().getFlags()).isEqualTo(0);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ // Put the proposed position at 1 above the origin. so it would need to move up 1 unit to
+ // be on the plane.
+ reformEvent.setProposedPosition(new Vec3(1f, 1f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_ONGOING);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // The expected position should be 3 unit above the activity in order to rest on the plane.
+ // It
+ // is 3 units because the activity space is 1 unit below of the origin and the plane is 2
+ // units
+ // above it.
+ Pose expectedPosition = new Pose(new Vector3(1f, 3f, 1f), new Quaternion(0f, 0f, 0f, 1f));
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ assertThat(moveEventListener.callCount).isEqualTo(1);
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isNull();
+
+ // The pose should have moved since the systemMovable is true.
+ assertPose(entity.getPose(), expectedPosition);
+ }
+
+ @Test
+ public void anchorable_updatesPoseButDoesNotMove_ifNotSystemMovable() {
+ // Set the activity space pose to be 1 unit to the left of the origin.
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), 1f);
+ Session session = Mockito.mock(Session.class);
+ Plane plane = mock(Plane.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+
+ // Create a perception plane that is 2 units above the origin.
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(0f, 2f, 0f, 0f, 0f, 0f, 1f);
+ PlaneData planeData =
+ new PlaneData(
+ perceptionPose,
+ 1f,
+ 1f,
+ Plane.Type.HORIZONTAL_UPWARD_FACING.intValue,
+ Plane.Label.FLOOR.intValue);
+ when(plane.getData(any())).thenReturn(planeData);
+
+ Entity entity = createTestEntity();
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ false,
+ /* scaleInZ= */ true,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ TestMoveEventListener moveEventListener = new TestMoveEventListener();
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ movableComponent.addMoveEventListener(executorService, moveEventListener);
+ // The reform options for parenting and moving should not be set when it is anchorable.
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(ReformOptions.FLAG_SCALE_WITH_DISTANCE);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ // Put the proposed position at 1 above the origin. so it would need to move up 1 unit to
+ // be on the plane.
+ reformEvent.setProposedPosition(new Vec3(1f, 1f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_ONGOING);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // The expected position should be 3 unit above the activity in order to rest on the plane.
+ // It
+ // is 3 units because the activity space is 1 unit below of the origin and the plane is 2
+ // units
+ // above it.
+ Pose expectedPosition = new Pose(new Vector3(1f, 3f, 1f), new Quaternion(0f, 0f, 0f, 1f));
+
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isNull();
+
+ // The panel shadow renderer should have no interaction if it is not system movable.
+ verify(panelShadowRenderer, never()).updatePanelPose(any(), any(), any());
+ verify(panelShadowRenderer, never()).destroy();
+ verify(panelShadowRenderer, never()).hidePlane();
+
+ // The pose should not have moved since the systemMovable is false.
+ assertPose(entity.getPose(), Pose.Identity);
+ }
+
+ @Test
+ public void anchorable_withNonActivityParent_updatesPoseBasedOnPlanesAndParent() {
+ // Set the activity space pose to be 1 unit to the left of the origin.
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), 1f);
+ Session session = Mockito.mock(Session.class);
+ Plane plane = mock(Plane.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+
+ // Create a perception plane that is 2 units above the origin.
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(0f, 2f, 0f, 0f, 0f, 0f, 1f);
+ PlaneData planeData =
+ new PlaneData(
+ perceptionPose,
+ 1f,
+ 1f,
+ Plane.Type.HORIZONTAL_UPWARD_FACING.intValue,
+ Plane.Label.FLOOR.intValue);
+ when(plane.getData(any())).thenReturn(planeData);
+
+ // Create a parent entity whose pose is below the activity space pose.
+ Entity parentEntity = createTestEntity();
+ parentEntity.setPose(new Pose(new Vector3(0f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)));
+ PanelEntity entity = createTestPanelEntity();
+ entity.setParent(parentEntity);
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ TestMoveEventListener moveEventListener = new TestMoveEventListener();
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ movableComponent.addMoveEventListener(executorService, moveEventListener);
+ // The reform options for parenting and moving should not be set when it is anchorable.
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(ReformOptions.FLAG_SCALE_WITH_DISTANCE);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ // Put the proposed position at 1 above the origin. so it would need to move up 1 unit to
+ // be on the plane.
+ reformEvent.setProposedPosition(new Vec3(1f, 1f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_ONGOING);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // The expected position should be 3 unit above the activity in order to rest on the plane.
+ // It
+ // is 3 units because the activity space is 1 unit below of the origin and the plane is 2
+ // units
+ // above it. Since the parent is 1 unit below the activity space, the expected position
+ // should
+ // be 4 units above the parent.
+ Pose expectedPosition = new Pose(new Vector3(1f, 4f, 1f), new Quaternion(0f, 0f, 0f, 1f));
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ assertThat(moveEventListener.callCount).isEqualTo(1);
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isNull();
+
+ // The pose should have moved since the systemMovable is true.
+ assertPose(entity.getPose(), expectedPosition);
+ }
+
+ @Test
+ public void anchorableAndScaledParent_updatesThePoseBasedOnPlanes() {
+ // Set the activity space pose to be 1 unit to the left of the origin. with a scale of 2.
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), 2f);
+ Session session = Mockito.mock(Session.class);
+ Plane plane = mock(Plane.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+
+ // Create a perception plane that is 2 units above the origin.
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(0f, 2f, 0f, 0f, 0f, 0f, 1f);
+ PlaneData planeData =
+ new PlaneData(
+ perceptionPose,
+ 1f,
+ 1f,
+ Plane.Type.HORIZONTAL_UPWARD_FACING.intValue,
+ Plane.Label.FLOOR.intValue);
+ when(plane.getData(any())).thenReturn(planeData);
+
+ PanelEntity entity = createTestPanelEntity();
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ TestMoveEventListener moveEventListener = new TestMoveEventListener();
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ movableComponent.addMoveEventListener(executorService, moveEventListener);
+ // The reform options for parenting and moving should not be set when it is anchorable.
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(ReformOptions.FLAG_SCALE_WITH_DISTANCE);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ // Put the proposed position at 1 above the origin. so it would need to move up 1 unit to
+ // be on the plane. This needs to be divided by the scale of the activity space.
+ reformEvent.setProposedPosition(new Vec3(.5f, .5f, .5f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_ONGOING);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // The expected position should be 3 unit above the activity in order to rest on the plane.
+ // It
+ // is 1.5 units because the activity space is 1 unit below of the origin and the plane is 2
+ // units above it and the activity space is scaled by 2.
+ Pose expectedPosition =
+ new Pose(new Vector3(.5f, 1.5f, .5f), new Quaternion(0f, 0f, 0f, 1f));
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ assertThat(moveEventListener.callCount).isEqualTo(1);
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isNull();
+ }
+
+ @Test
+ public void anchorable_withinAnchorDistance_setsAnchorEntity() {
+ // Set the activity space pose to be 1 unit to the left of the origin.
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), 1f);
+ Session session = Mockito.mock(Session.class);
+ Plane plane = mock(Plane.class);
+ Anchor anchor = mock(Anchor.class);
+ IBinder sharedAnchorToken = Mockito.mock(IBinder.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+ when(plane.createAnchor(any(), any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ // Create a perception plane that is 2 units above the origin.
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(0f, 2f, 0f, 0f, 0f, 0f, 1f);
+ PlaneData planeData =
+ new PlaneData(
+ perceptionPose,
+ 1f,
+ 1f,
+ Plane.Type.VERTICAL.intValue,
+ Plane.Label.WALL.intValue);
+ when(plane.getData(any())).thenReturn(planeData);
+
+ PanelEntity entity = createTestPanelEntity();
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ TestMoveEventListener moveEventListener = new TestMoveEventListener();
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ // Add the move event listener and the anchored event listener.
+ movableComponent.addMoveEventListener(executorService, moveEventListener);
+ // The reform options for parenting and moving should not be set when it is anchorable.
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(ReformOptions.FLAG_SCALE_WITH_DISTANCE);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ // Put the proposed position at 1 above the origin. so it would need to move up 1 unit to
+ // be on the plane.
+ reformEvent.setProposedPosition(new Vec3(1f, 1f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_END);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // Moving from (1, 3, 1) relative to the activity space to be relative to the anchor which
+ // is
+ // (1, 3, 0) relative to the activity space. which results in an updated pose of (0, 0, 1)
+ // relative to the anchor.The (-0.707f, 0f, 0f, 0.707f) Quaternion represents a 90 degree
+ // rotation around the x-axis Which is expected when the panel is rotated into the plane's
+ // reference space.
+ Pose expectedPosition =
+ new Pose(new Vector3(0f, 0f, 1f), new Quaternion(-0.707f, 0f, 0f, 0.707f));
+
+ assertThat(moveEventListener.callCount).isEqualTo(1);
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ // Check that the anchor entity was set.
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isInstanceOf(AnchorEntity.class);
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isEqualTo(entity.getParent());
+ }
+
+ @Test
+ public void anchorable_withinAnchorDistanceAboveAnchor_resetsPose() {
+ // Set the activity space pose to be 1 unit to the left of the origin.
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), 1f);
+ Session session = Mockito.mock(Session.class);
+ Plane plane = mock(Plane.class);
+ Anchor anchor = mock(Anchor.class);
+ IBinder sharedAnchorToken = Mockito.mock(IBinder.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+ when(plane.createAnchor(any(), any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ // Create a perception plane that is 2 units above the origin.
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(0f, 2f, 0f, 0f, 0f, 0f, 1f);
+ PlaneData planeData =
+ new PlaneData(
+ perceptionPose,
+ 1f,
+ 1f,
+ Plane.Type.VERTICAL.intValue,
+ Plane.Label.WALL.intValue);
+ when(plane.getData(any())).thenReturn(planeData);
+
+ PanelEntity entity = createTestPanelEntity();
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ TestMoveEventListener moveEventListener = new TestMoveEventListener();
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ // Add the move event listener and the anchored event listener.
+ movableComponent.addMoveEventListener(executorService, moveEventListener);
+ // The reform options for parenting and moving should not be set when it is anchorable.
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(ReformOptions.FLAG_SCALE_WITH_DISTANCE);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ // Put the proposed position at 2 + half the MIN_PLANE_ANCHOR_DISTANCE above the origin. So
+ // it
+ // would be right above the plane.
+ reformEvent.setProposedPosition(
+ new Vec3(1f, 3f + MovableComponentImpl.MIN_PLANE_ANCHOR_DISTANCE / 2f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_END);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // Moving from (1, 3, 1) relative to the activity space to be relative to the anchor which
+ // is
+ // (1, 3, 0) relative to the activity space. which results in an updated pose of (0, 0, 1)
+ // relative to the anchor.The (-0.707f, 0f, 0f, 0.707f) Quaternion represents a 90 degree
+ // rotation around the x-axis Which is expected when the panel is rotated into the plane's
+ // reference space.
+ Pose expectedPosition =
+ new Pose(new Vector3(0f, 0f, 1f), new Quaternion(-0.707f, 0f, 0f, 0.707f));
+
+ assertThat(moveEventListener.callCount).isEqualTo(1);
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ // Check that the anchor entity was set.
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isInstanceOf(AnchorEntity.class);
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isEqualTo(entity.getParent());
+ }
+
+ @Test
+ public void anchorable_withIncorrectPlaneType_doesNotCreateAnchor() {
+ // Set the activity space pose to be 1 unit to the left of the origin.
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), 1f);
+ Session session = Mockito.mock(Session.class);
+ Plane plane = mock(Plane.class);
+ Anchor anchor = mock(Anchor.class);
+ IBinder sharedAnchorToken = Mockito.mock(IBinder.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+ when(plane.createAnchor(any(), any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ // Create a perception plane that is 2 units above the origin.
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(0f, 2f, 0f, 0f, 0f, 0f, 1f);
+ PlaneData planeData =
+ new PlaneData(
+ perceptionPose,
+ 1f,
+ 1f,
+ Plane.Type.VERTICAL.intValue,
+ Plane.Label.WALL.intValue);
+ when(plane.getData(any())).thenReturn(planeData);
+
+ // Set the anchor placement to be a table.
+ AnchorPlacement anchorPlacement =
+ fakeRuntime.createAnchorPlacementForPlanes(
+ ImmutableSet.of(PlaneType.ANY), ImmutableSet.of(PlaneSemantic.TABLE));
+ ImmutableSet<AnchorPlacement> anchorPlacementSet = ImmutableSet.of(anchorPlacement);
+
+ PanelEntity entity = createTestPanelEntity();
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ anchorPlacementSet,
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ TestMoveEventListener moveEventListener = new TestMoveEventListener();
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ // Add the move event listener and the anchored event listener.
+ movableComponent.addMoveEventListener(executorService, moveEventListener);
+ // The reform options for parenting and moving should not be set when it is anchorable.
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(ReformOptions.FLAG_SCALE_WITH_DISTANCE);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ // Put the proposed position at 1 above the origin. so it would need to move up 1 unit to
+ // be on the plane.
+ reformEvent.setProposedPosition(new Vec3(1f, 1f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_END);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // The expected position should be 3 unit above the activity in order to rest on the plane.
+ // It
+ // is 3 units because the activity space is 1 unit below of the origin and the plane is 2
+ // units
+ // above it. However, since the plane is not a table plane, the anchor should not be
+ // created.
+ Pose expectedPosition = new Pose(new Vector3(1f, 3f, 1f), new Quaternion(0f, 0f, 0f, 1f));
+ assertThat(moveEventListener.callCount).isEqualTo(1);
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ // Check that the anchor entity was not set.
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isNull();
+ }
+
+ @Test
+ public void anchorable_withinAnchorDistanceAndScale_setsAnchorEntityAndScales() {
+ // Set the activity space pose to be 1 unit to the left of the OpenXR origin and add a scale
+ // of
+ // 2.
+ float activityScale = 2f;
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), activityScale);
+ Session session = Mockito.mock(Session.class);
+ Plane plane = mock(Plane.class);
+ Anchor anchor = mock(Anchor.class);
+ IBinder sharedAnchorToken = Mockito.mock(IBinder.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+ when(plane.createAnchor(any(), any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ // Create a perception plane that is 2 units above the origin.
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(0f, 2f, 0f, 0f, 0f, 0f, 1f);
+ PlaneData planeData =
+ new PlaneData(
+ perceptionPose,
+ 1f,
+ 1f,
+ Plane.Type.VERTICAL.intValue,
+ Plane.Label.WALL.intValue);
+ when(plane.getData(any())).thenReturn(planeData);
+
+ Vector3 entityScale = new Vector3(1f, 3f, 5f);
+ PanelEntity entity = createTestPanelEntity();
+ entity.setScale(entityScale);
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ TestMoveEventListener moveEventListener = new TestMoveEventListener();
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ // Add the move event listener and the anchored event listener.
+ movableComponent.addMoveEventListener(executorService, moveEventListener);
+ // The reform options for parenting and moving should not be set when it is anchorable.
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(ReformOptions.FLAG_SCALE_WITH_DISTANCE);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ // Put the proposed position at 1 above the origin. so it would need to move up 1 unit to
+ // be on the plane. This needs to be divided by the scale of the activity space.
+ reformEvent.setProposedPosition(new Vec3(.5f, .5f, .5f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_END);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // Moving from (1, 3, 1) relative to the activity space to be relative to the anchor which
+ // is
+ // (1, 3, 0) relative to the activity space. which results in an updated pose of (0, 0, 1)
+ // relative to the anchor. The (-0.707f, 0f, 0f, 0.707f) Quaternion represents a 90 degree
+ // rotation around the x-axis. Which is expected when the panel is rotated into the plane's
+ // reference space.
+ Pose expectedPosition =
+ new Pose(new Vector3(0f, 0f, 1f), new Quaternion(-0.707f, 0f, 0f, 0.707f));
+
+ assertThat(moveEventListener.callCount).isEqualTo(1);
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ // Check that the anchor entity was set.
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isInstanceOf(AnchorEntity.class);
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isEqualTo(entity.getParent());
+ assertVector3(entity.getWorldSpaceScale(), entityScale.times(activityScale));
+ }
+
+ @Test
+ public void anchorable_noPlanes_keepsProposedPose() {
+ // Set the activity space pose to be 1 unit to the left of the origin.
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), 1f);
+ Session session = Mockito.mock(Session.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of());
+
+ PanelEntity entity = createTestPanelEntity();
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ TestMoveEventListener moveEventListener = new TestMoveEventListener();
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ movableComponent.addMoveEventListener(executorService, moveEventListener);
+ // The reform options for parenting and moving should not be set when it is anchorable.
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(ReformOptions.FLAG_SCALE_WITH_DISTANCE);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ reformEvent.setProposedPosition(new Vec3(1f, 1f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // The expected position should be unchanged from the proposed event
+ Pose expectedPosition = new Pose(new Vector3(1f, 1f, 1f), new Quaternion(0f, 0f, 0f, 1f));
+
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ assertThat(moveEventListener.callCount).isEqualTo(1);
+ }
+
+ @Test
+ public void anchorable_noPlaneData_keepsProposedPose() {
+ // Set the activity space pose to be 1 unit to the left of the origin.
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), 1f);
+ Session session = Mockito.mock(Session.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ Plane plane = mock(Plane.class);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+ when(plane.getData(any())).thenReturn(null);
+
+ PanelEntity entity = createTestPanelEntity();
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ TestMoveEventListener moveEventListener = new TestMoveEventListener();
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ movableComponent.addMoveEventListener(executorService, moveEventListener);
+ // The reform options for parenting and moving should not be set when it is anchorable.
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(ReformOptions.FLAG_SCALE_WITH_DISTANCE);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ reformEvent.setProposedPosition(new Vec3(1f, 1f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // The expected position should be unchanged from the proposed event
+ Pose expectedPosition = new Pose(new Vector3(1f, 1f, 1f), new Quaternion(0f, 0f, 0f, 1f));
+
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ assertThat(moveEventListener.callCount).isEqualTo(1);
+ assertThat(moveEventListener.callCount).isEqualTo(1);
+ }
+
+ @Test
+ public void anchorable_outsideExtents_keepsProposedPose() {
+ // Set the activity space pose to be 1 unit to the left of the origin.
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), 1f);
+ Session session = Mockito.mock(Session.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ Plane plane = mock(Plane.class);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+
+ // Create a perception plane that is 2 units above the origin.
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(5f, 2f, 0f, 0f, 0f, 0f, 1f);
+ PlaneData planeData =
+ new PlaneData(
+ perceptionPose,
+ 1f,
+ 1f,
+ Plane.Type.HORIZONTAL_UPWARD_FACING.intValue,
+ Plane.Label.FLOOR.intValue);
+ when(plane.getData(any())).thenReturn(planeData);
+
+ PanelEntity entity = createTestPanelEntity();
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ TestMoveEventListener moveEventListener = new TestMoveEventListener();
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ movableComponent.addMoveEventListener(executorService, moveEventListener);
+ // The reform options for parenting and moving should not be set when it is anchorable.
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(ReformOptions.FLAG_SCALE_WITH_DISTANCE);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ reformEvent.setProposedPosition(new Vec3(1f, 1f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // The expected position should be unchanged from the proposed event
+ Pose expectedPosition = new Pose(new Vector3(1f, 1f, 1f), new Quaternion(0f, 0f, 0f, 1f));
+
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ assertThat(moveEventListener.callCount).isEqualTo(1);
+ assertThat(moveEventListener.callCount).isEqualTo(1);
+ }
+
+ @Test
+ public void anchorable_resetsToActivityPoseAfterAnchoring() {
+ // Set the activity space pose to be 1 unit to the left of the origin.
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), 1f);
+ Session session = Mockito.mock(Session.class);
+ Plane plane = mock(Plane.class);
+ Anchor anchor = mock(Anchor.class);
+ IBinder sharedAnchorToken = Mockito.mock(IBinder.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+ when(plane.createAnchor(any(), any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ // Create a perception plane that is 2 units above the origin.
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(0f, 2f, 0f, 0f, 0f, 0f, 1f);
+ PlaneData planeData =
+ new PlaneData(
+ perceptionPose,
+ 1f,
+ 1f,
+ Plane.Type.VERTICAL.intValue,
+ Plane.Label.WALL.intValue);
+ when(plane.getData(any())).thenReturn(planeData);
+
+ PanelEntity entity = createTestPanelEntity();
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ TestMoveEventListener moveEventListener = new TestMoveEventListener();
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ // Add the move event listener and the anchored event listener.
+ movableComponent.addMoveEventListener(executorService, moveEventListener);
+ // The reform options for parenting and moving should not be set when it is anchorable.
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(ReformOptions.FLAG_SCALE_WITH_DISTANCE);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ // Put the proposed position at 1 above the origin. so it would need to move up 1 unit to
+ // be on the plane.
+ reformEvent.setProposedPosition(new Vec3(1f, 1f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_END);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // Moving from (1, 3, 1) relative to the activity space to be relative to the anchor which
+ // is
+ // (1, 3, 0) relative to the activity space. which results in an updated pose of (0, 0, 1)
+ // relative to the anchor.The (-0.707f, 0f, 0f, 0.707f) Quaternion represents a 90 degree
+ // rotation around the x-axis Which is expected when the panel is rotated into the plane's
+ // reference space.
+ Pose expectedPosition =
+ new Pose(new Vector3(0f, 0f, 1f), new Quaternion(-0.707f, 0f, 0f, 0.707f));
+
+ assertThat(moveEventListener.callCount).isEqualTo(1);
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ // Check that the anchor entity was set.
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isInstanceOf(AnchorEntity.class);
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isEqualTo(entity.getParent());
+
+ // Put the proposed position at 4 above the origin so it would be off the plane. It should
+ // reset to the activity space pose and rotation.
+ reformEvent.setProposedPosition(new Vec3(1f, 4f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_END);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // Moving to (1, 4, 1) relative to the activity space. This should pull the entity away from
+ // the
+ // anchor and it should be reparented to the activity space.
+ expectedPosition = new Pose(new Vector3(1f, 4f, 1f), new Quaternion(0f, 0f, 0f, 1f));
+
+ assertThat(moveEventListener.callCount).isEqualTo(2);
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ assertPose(entity.getPose(), expectedPosition);
+ // Check that parent was updated to the activity space.
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isEqualTo(activitySpaceImpl);
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isEqualTo(entity.getParent());
+ }
+
+ @Test
+ public void anchorable_resetsAndScaleToActivityPoseAfterAnchoring() {
+ // Set the activity space pose to be 1 unit to the left of the OpenXR origin and add a scale
+ // of
+ // 2.
+ float activityScale = 2f;
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), activityScale);
+ Session session = Mockito.mock(Session.class);
+ Plane plane = mock(Plane.class);
+ Anchor anchor = mock(Anchor.class);
+ IBinder sharedAnchorToken = Mockito.mock(IBinder.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+ when(plane.createAnchor(any(), any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ // Create a perception plane that is 2 units above the origin.
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(0f, 2f, 0f, 0f, 0f, 0f, 1f);
+ PlaneData planeData =
+ new PlaneData(
+ perceptionPose,
+ 1f,
+ 1f,
+ Plane.Type.VERTICAL.intValue,
+ Plane.Label.WALL.intValue);
+ when(plane.getData(any())).thenReturn(planeData);
+
+ Vector3 entityScale = new Vector3(1f, 3f, 5f);
+ PanelEntity entity = createTestPanelEntity();
+ entity.setScale(entityScale);
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ TestMoveEventListener moveEventListener = new TestMoveEventListener();
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ // Add the move event listener and the anchored event listener.
+ movableComponent.addMoveEventListener(executorService, moveEventListener);
+ // The reform options for parenting and moving should not be set when it is anchorable.
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(ReformOptions.FLAG_SCALE_WITH_DISTANCE);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ // Put the proposed position at 1 above the origin. so it would need to move up 1 unit to
+ // be on the plane. This needs to be divided by the scale of the activity space.
+ reformEvent.setProposedPosition(new Vec3(.5f, .5f, .5f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_END);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // Moving from (1, 3, 1) relative to the activity space to be relative to the anchor which
+ // is
+ // (1, 3, 0) relative to the activity space. Which results in an updated pose of (0, 0, 1)
+ // relative to the anchor. The (-0.707f, 0f, 0f, 0.707f) Quaternion represents a 90 degree
+ // rotation around the x-axis. Which is expected when the panel is rotated into the plane's
+ // reference space.
+ Pose expectedPosition =
+ new Pose(new Vector3(0f, 0f, 1f), new Quaternion(-0.707f, 0f, 0f, 0.707f));
+
+ assertThat(moveEventListener.callCount).isEqualTo(1);
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ assertPose(entity.getPose(), expectedPosition);
+ assertVector3(entity.getScale(), entityScale.times(activityScale));
+ // Check that the anchor entity was set.
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isInstanceOf(AnchorEntity.class);
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isEqualTo(entity.getParent());
+ assertVector3(entity.getWorldSpaceScale(), entityScale.times(activityScale));
+
+ // Put the proposed position at 4 above the activity space so it would be off the plane. It
+ // should reset to the activity space pose and rotation.
+ reformEvent.setProposedPosition(new Vec3(1f, 4f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_END);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // Moving to (1, 4, 1) relative to the activity space. This should pull the entity away from
+ // the
+ // anchor and it should be reparented to the activity space.
+ expectedPosition = new Pose(new Vector3(1f, 4f, 1f), new Quaternion(0f, 0f, 0f, 1f));
+
+ assertThat(moveEventListener.callCount).isEqualTo(2);
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ assertPose(entity.getPose(), expectedPosition);
+ // Check that parent was updated to the activity space.
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isEqualTo(activitySpaceImpl);
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isEqualTo(entity.getParent());
+ // Check that the scale was updated to the original scale.
+ assertVector3(entity.getScale(), entityScale);
+ }
+
+ @Test
+ public void anchorableChildOfEntity_resetsToActivityPoseAfterAnchoring() {
+ // Set the activity space pose to be 1 unit to the left of the origin.
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), 1f);
+ Session session = Mockito.mock(Session.class);
+ Plane plane = mock(Plane.class);
+ Anchor anchor = mock(Anchor.class);
+ IBinder sharedAnchorToken = Mockito.mock(IBinder.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+ when(plane.createAnchor(any(), any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ // Create a perception plane that is 2 units above the origin.
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(0f, 2f, 0f, 0f, 0f, 0f, 1f);
+ PlaneData planeData =
+ new PlaneData(
+ perceptionPose,
+ 1f,
+ 1f,
+ Plane.Type.VERTICAL.intValue,
+ Plane.Label.WALL.intValue);
+ when(plane.getData(any())).thenReturn(planeData);
+
+ // Create a parent entity whose pose is below the activity space pose.
+ Entity parentEntity = createTestEntity();
+ parentEntity.setPose(new Pose(new Vector3(0f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)));
+ PanelEntity entity = createTestPanelEntity();
+ entity.setParent(parentEntity);
+
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ TestMoveEventListener moveEventListener = new TestMoveEventListener();
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ // Add the move event listener and the anchored event listener.
+ movableComponent.addMoveEventListener(executorService, moveEventListener);
+ // The reform options for parenting and moving should not be set when it is anchorable.
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(ReformOptions.FLAG_SCALE_WITH_DISTANCE);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ // Put the proposed position at 1 above the origin. It would need to move up 1 unit to be on
+ // the
+ // plane.
+ reformEvent.setProposedPosition(new Vec3(1f, 1f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_END);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // Moving from (1, 3, 1) relative to the activity space to be relative to the anchor which
+ // is
+ // (1, 3, 0) relative to the activity space. which results in an updated pose of (0, 0, 1)
+ // relative to the anchor.The (-0.707f, 0f, 0f, 0.707f) Quaternion represents a 90 degree
+ // rotation around the x-axis Which is expected when the panel is rotated into the plane's
+ // reference space.
+ Pose expectedPosition =
+ new Pose(new Vector3(0f, 0f, 1f), new Quaternion(-0.707f, 0f, 0f, 0.707f));
+
+ assertThat(moveEventListener.callCount).isEqualTo(1);
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ // Check that the anchor entity was set.
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isInstanceOf(AnchorEntity.class);
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isEqualTo(entity.getParent());
+
+ // Put the proposed position at 4 above the origin so it would be off the plane. It should
+ // reset to the activity space pose and rotation.
+ reformEvent.setProposedPosition(new Vec3(1f, 4f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_END);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // Moving to (1, 4, 1) relative to the activity space. This should pull the entity away from
+ // the
+ // anchor and it should be reparented to the activity space not the original parent..
+ expectedPosition = new Pose(new Vector3(1f, 4f, 1f), new Quaternion(0f, 0f, 0f, 1f));
+
+ assertThat(moveEventListener.callCount).isEqualTo(2);
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ assertPose(entity.getPose(), expectedPosition);
+ // Check that parent was updated to the activity space.
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isEqualTo(activitySpaceImpl);
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isEqualTo(entity.getParent());
+ }
+
+ @Test
+ public void anchorable_shouldDispose_disposesAnchorAfterUnparenting() {
+ // Set the activity space pose to be 1 unit to the left of the origin.
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), 1f);
+ Session session = Mockito.mock(Session.class);
+ Plane plane = mock(Plane.class);
+ Anchor anchor = mock(Anchor.class);
+ IBinder sharedAnchorToken = Mockito.mock(IBinder.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+ when(plane.createAnchor(any(), any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ // Create a perception plane that is 2 units above the origin.
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(0f, 2f, 0f, 0f, 0f, 0f, 1f);
+ PlaneData planeData =
+ new PlaneData(
+ perceptionPose,
+ 1f,
+ 1f,
+ Plane.Type.VERTICAL.intValue,
+ Plane.Label.WALL.intValue);
+ when(plane.getData(any())).thenReturn(planeData);
+
+ PanelEntity entity = createTestPanelEntity();
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ true,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ TestMoveEventListener moveEventListener = new TestMoveEventListener();
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ // Add the move event listener and the anchored event listener.
+ movableComponent.addMoveEventListener(executorService, moveEventListener);
+ // The reform options for parenting and moving should not be set when it is anchorable.
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(ReformOptions.FLAG_SCALE_WITH_DISTANCE);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ // Put the proposed position at 1 above the origin. so it would need to move up 1 unit to
+ // be on the plane.
+ reformEvent.setProposedPosition(new Vec3(1f, 1f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_END);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // Moving from (1, 3, 1) relative to the activity space to be relative to the anchor which
+ // is
+ // (1, 3, 0) relative to the activity space. which results in an updated pose of (0, 0, 1)
+ // relative to the anchor.The (-0.707f, 0f, 0f, 0.707f) Quaternion represents a 90 degree
+ // rotation around the x-axis Which is expected when the panel is rotated into the plane's
+ // reference space.
+ Pose expectedPosition =
+ new Pose(new Vector3(0f, 0f, 1f), new Quaternion(-0.707f, 0f, 0f, 0.707f));
+
+ assertThat(moveEventListener.callCount).isEqualTo(1);
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ // Check that the anchor entity was set.
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isInstanceOf(AnchorEntity.class);
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isEqualTo(entity.getParent());
+ // Cache the anchor entity.
+ Entity anchorEntity = moveEventListener.lastMoveEvent.updatedParent;
+
+ // Put the proposed position at 4 above the origin so it would be off the plane. It should
+ // reset to the activity space pose and rotation.
+ reformEvent.setProposedPosition(new Vec3(1f, 4f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_END);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // Moving to (1, 4, 1) relative to the activity space. This should pull the entity away from
+ // the
+ // anchor and it should be reparented to the activity space.
+ expectedPosition = new Pose(new Vector3(1f, 4f, 1f), new Quaternion(0f, 0f, 0f, 1f));
+
+ assertThat(moveEventListener.callCount).isEqualTo(2);
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ // Check that the anchor entity was set.
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isEqualTo(activitySpaceImpl);
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isEqualTo(entity.getParent());
+
+ // Verify that the anchor entity was disposed by checking that it is no longer in the entity
+ // manager.
+ assertThat(entityManager.getEntityForNode(((AndroidXrEntity) anchorEntity).getNode()))
+ .isNull();
+ }
+
+ @Test
+ public void anchorable_shouldDispose_doeNotDisposeIfAnchorHasChildren() {
+ // Set the activity space pose to be 1 unit to the left of the origin.
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), 1f);
+ Session session = Mockito.mock(Session.class);
+ Plane plane = mock(Plane.class);
+ Anchor anchor = mock(Anchor.class);
+ IBinder sharedAnchorToken = Mockito.mock(IBinder.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+ when(plane.createAnchor(any(), any())).thenReturn(anchor);
+ when(anchor.getAnchorToken()).thenReturn(sharedAnchorToken);
+
+ // Create a perception plane that is 2 units above the origin.
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(0f, 2f, 0f, 0f, 0f, 0f, 1f);
+ PlaneData planeData =
+ new PlaneData(
+ perceptionPose,
+ 1f,
+ 1f,
+ Plane.Type.VERTICAL.intValue,
+ Plane.Label.WALL.intValue);
+ when(plane.getData(any())).thenReturn(planeData);
+
+ PanelEntity entity = createTestPanelEntity();
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ true,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ true,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+ TestMoveEventListener moveEventListener = new TestMoveEventListener();
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ // Add the move event listener and the anchored event listener.
+ movableComponent.addMoveEventListener(executorService, moveEventListener);
+ // The reform options for parenting and moving should not be set when it is anchorable.
+ assertThat(node.getReformOptions().getFlags())
+ .isEqualTo(ReformOptions.FLAG_SCALE_WITH_DISTANCE);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ // Put the proposed position at 1 above the origin. so it would need to move up 1 unit to
+ // be on the plane.
+ reformEvent.setProposedPosition(new Vec3(1f, 1f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_END);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // Moving from (1, 3, 1) relative to the activity space to be relative to the anchor which
+ // is
+ // (1, 3, 0) relative to the activity space. which results in an updated pose of (0, 0, 1)
+ // relative to the anchor.The (-0.707f, 0f, 0f, 0.707f) Quaternion represents a 90 degree
+ // rotation around the x-axis Which is expected when the panel is rotated into the plane's
+ // reference space.
+ Pose expectedPosition =
+ new Pose(new Vector3(0f, 0f, 1f), new Quaternion(-0.707f, 0f, 0f, 0.707f));
+
+ assertThat(moveEventListener.callCount).isEqualTo(1);
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ // Check that the anchor entity was set.
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isInstanceOf(AnchorEntity.class);
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isEqualTo(entity.getParent());
+ // Cache the anchor entity.
+ Entity anchorEntity = moveEventListener.lastMoveEvent.updatedParent;
+
+ Entity child = createTestEntity();
+ anchorEntity.addChild(child);
+
+ // Put the proposed position at 4 above the origin so it would be off the plane. It should
+ // reset to the activity space pose and rotation.
+ reformEvent.setProposedPosition(new Vec3(1f, 4f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_END);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ // Moving to (1, 4, 1) relative to the activity space. This should pull the entity away from
+ // the
+ // anchor and it should be reparented to the activity space.
+ expectedPosition = new Pose(new Vector3(1f, 4f, 1f), new Quaternion(0f, 0f, 0f, 1f));
+
+ assertThat(moveEventListener.callCount).isEqualTo(2);
+ assertPose(moveEventListener.lastMoveEvent.currentPose, expectedPosition);
+ // Check that the anchor entity was set.
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isEqualTo(activitySpaceImpl);
+ assertThat(moveEventListener.lastMoveEvent.updatedParent).isEqualTo(entity.getParent());
+
+ // Verify that the anchor entity wasn't disposed by checking that it is in the entity
+ // manager.
+ assertThat(entityManager.getEntityForNode(((AndroidXrEntity) anchorEntity).getNode()))
+ .isEqualTo(anchorEntity);
+ }
+
+ @Test
+ public void anchorablePanelEntity_nearPlane_rendersShadow() {
+ // Set the activity space pose to be 1 unit to the left of the origin.
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), 1f);
+ Session session = Mockito.mock(Session.class);
+ Plane plane = mock(Plane.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+
+ // Create a perception plane that is 2 units above the origin.
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(0f, 2f, 0f, 0f, 0f, 0f, 1f);
+ PlaneData planeData =
+ new PlaneData(
+ perceptionPose,
+ 1f,
+ 1f,
+ Plane.Type.HORIZONTAL_UPWARD_FACING.intValue,
+ Plane.Label.FLOOR.intValue);
+ when(plane.getData(any())).thenReturn(planeData);
+
+ PanelEntity entity = createTestPanelEntity();
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ false,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ // Put the proposed position at 1 above the origin. so it would need to move up 1 unit to
+ // be on the plane.
+ reformEvent.setProposedPosition(new Vec3(1f, 1f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_ONGOING);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+
+ // Since it is by the plane a call should be made to the panel shadow renderer.
+ verify(panelShadowRenderer)
+ .updatePanelPose(
+ new Pose(new Vector3(0f, 2f, 1f), new Quaternion(0f, 0f, 0f, 1f)),
+ RuntimeUtils.fromPerceptionPose(perceptionPose),
+ (PanelEntityImpl) entity);
+ }
+
+ @Test
+ public void anchorablePanelEntity_awayFromPlane_hidesShadow() {
+ // Set the activity space pose to be 1 unit to the left of the origin.
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), 1f);
+ Session session = Mockito.mock(Session.class);
+ Plane plane = mock(Plane.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+
+ // Create a perception plane that is 2 units above the origin.
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(0f, 2f, 0f, 0f, 0f, 0f, 1f);
+ PlaneData planeData =
+ new PlaneData(
+ perceptionPose,
+ 1f,
+ 1f,
+ Plane.Type.HORIZONTAL_UPWARD_FACING.intValue,
+ Plane.Label.FLOOR.intValue);
+ when(plane.getData(any())).thenReturn(planeData);
+
+ PanelEntity entity = createTestPanelEntity();
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ false,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ // Put the proposed position at 5 above the origin. so it is far away from the plane.
+ reformEvent.setProposedPosition(new Vec3(1f, 5f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_ONGOING);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+
+ // Since it is by the plane a call should be made to the panel shadow renderer.
+ verify(panelShadowRenderer).hidePlane();
+ }
+
+ @Test
+ public void anchorablePanelEntity_endMovement_callsDestroy() {
+ // Set the activity space pose to be 1 unit to the left of the origin.
+ setActivitySpacePose(
+ new Pose(new Vector3(-1f, -1f, 0f), new Quaternion(0f, 0f, 0f, 1f)), 1f);
+ Session session = Mockito.mock(Session.class);
+ Plane plane = mock(Plane.class);
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ when(session.getAllPlanes()).thenReturn(ImmutableList.of(plane));
+
+ // Create a perception plane that is 2 units above the origin.
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(0f, 2f, 0f, 0f, 0f, 0f, 1f);
+ PlaneData planeData =
+ new PlaneData(
+ perceptionPose,
+ 1f,
+ 1f,
+ Plane.Type.HORIZONTAL_UPWARD_FACING.intValue,
+ Plane.Label.FLOOR.intValue);
+ when(plane.getData(any())).thenReturn(planeData);
+
+ PanelEntity entity = createTestPanelEntity();
+ // Set anchorPlacement to any plane.
+ MovableComponent movableComponent =
+ new MovableComponentImpl(
+ /* systemMovable= */ true,
+ /* scaleInZ= */ false,
+ createAnyAnchorPlacement(),
+ /* shouldDisposeParentAnchor= */ false,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ assertThat(movableComponent).isNotNull();
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ FakeNode node = (FakeNode) ((AndroidXrEntity) entity).getNode();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+
+ // Set the reform state to end so that the plane shadow gets destroyed.
+ reformEvent.setProposedPosition(new Vec3(1f, 1f, 1f));
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_END);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+
+ // Since it is by the plane a call should be made to the panel shadow renderer.
+ verify(panelShadowRenderer).destroy();
+ }
+
+ static class TestMoveEventListener implements MoveEventListener {
+ int callCount = 0;
+ MoveEvent lastMoveEvent = null;
+
+ @Override
+ public void onMoveEvent(MoveEvent event) {
+ lastMoveEvent = event;
+ callCount++;
+ }
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/OpenXrActivityPoseTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/OpenXrActivityPoseTest.java
new file mode 100644
index 0000000..dd24f63
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/OpenXrActivityPoseTest.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static androidx.xr.runtime.testing.math.MathAssertions.assertPose;
+import static androidx.xr.runtime.testing.math.MathAssertions.assertVector3;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import androidx.xr.runtime.math.Matrix4;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Quaternion;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter.CameraViewActivityPose;
+import androidx.xr.scenecore.common.BaseActivityPose;
+import androidx.xr.scenecore.impl.perception.Fov;
+import androidx.xr.scenecore.impl.perception.PerceptionLibrary;
+import androidx.xr.scenecore.impl.perception.Session;
+import androidx.xr.scenecore.impl.perception.ViewProjection;
+import androidx.xr.scenecore.impl.perception.ViewProjections;
+import androidx.xr.scenecore.testing.FakeScheduledExecutorService;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeGltfModelToken;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
+
+import java.util.Arrays;
+import java.util.List;
+
+/** Test for common behaviour for ActivityPoses whose world position is retrieved from OpenXr. */
+@RunWith(ParameterizedRobolectricTestRunner.class)
+public final class OpenXrActivityPoseTest {
+ private final AndroidXrEntity activitySpaceRoot = mock(AndroidXrEntity.class);
+ private final FakeXrExtensions fakeExtensions = new FakeXrExtensions();
+ private final PerceptionLibrary perceptionLibrary = mock(PerceptionLibrary.class);
+ private final Session session = mock(Session.class);
+ private final FakeScheduledExecutorService executor = new FakeScheduledExecutorService();
+ private final EntityManager entityManager = new EntityManager();
+ private final ActivitySpaceImpl activitySpace =
+ new ActivitySpaceImpl(
+ fakeExtensions.createNode(),
+ fakeExtensions,
+ entityManager,
+ () -> fakeExtensions.fakeSpatialState,
+ executor);
+
+ enum OpenXrActivityPoseType {
+ HEAD_ACTIVITY_POSE,
+ CAMERA_ACTIVITY_POSE,
+ }
+
+ @Parameter(0)
+ public OpenXrActivityPoseType testActivityPoseType;
+
+ BaseActivityPose testActivityPose;
+
+ @Parameters
+ public static List<Object> data() throws Exception {
+ return Arrays.asList(
+ // TODO: b/377812131 - Add OpenXrActivityPoseType.CAMERA_ACTIVITY_POSE.
+ new Object[] {OpenXrActivityPoseType.HEAD_ACTIVITY_POSE});
+ }
+
+ @Before
+ public void doBeforeEachTest() {
+ // By default, set the activity space to the root of the underlying OpenXR reference space.
+ this.activitySpace.setOpenXrReferenceSpacePose(Matrix4.Identity);
+ }
+
+ /** Creates a HeadActivityPoseImpl instance. */
+ private HeadActivityPoseImpl createHeadActivityPose(
+ ActivitySpaceImpl activitySpace, AndroidXrEntity activitySpaceRoot) {
+ return new HeadActivityPoseImpl(activitySpace, activitySpaceRoot, perceptionLibrary);
+ }
+
+ /** Creates a CameraViewActivityPoseImpl instance. */
+ private CameraViewActivityPoseImpl createCameraViewActivityPose(
+ ActivitySpaceImpl activitySpace, AndroidXrEntity activitySpaceRoot) {
+ return new CameraViewActivityPoseImpl(
+ CameraViewActivityPose.CAMERA_TYPE_LEFT_EYE,
+ activitySpace,
+ activitySpaceRoot,
+ perceptionLibrary);
+ }
+
+ private BaseActivityPose createTestActivityPose() {
+ return createTestActivityPose(activitySpace, activitySpaceRoot);
+ }
+
+ private BaseActivityPose createTestActivityPose(
+ ActivitySpaceImpl activitySpace, AndroidXrEntity activitySpaceRoot) {
+ switch (testActivityPoseType) {
+ case HEAD_ACTIVITY_POSE:
+ return createHeadActivityPose(activitySpace, activitySpaceRoot);
+ case CAMERA_ACTIVITY_POSE:
+ return createCameraViewActivityPose(activitySpace, activitySpaceRoot);
+ }
+ return null;
+ }
+
+ private void setPerceptionPose(Pose pose) {
+ when(perceptionLibrary.getSession()).thenReturn(session);
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ pose == null ? null : RuntimeUtils.poseToPerceptionPose(pose);
+ switch (testActivityPoseType) {
+ case HEAD_ACTIVITY_POSE:
+ {
+ when(session.getHeadPose()).thenReturn(perceptionPose);
+ break;
+ }
+ case CAMERA_ACTIVITY_POSE:
+ {
+ ViewProjection viewProjection =
+ new ViewProjection(perceptionPose, new Fov(0, 0, 0, 0));
+ when(session.getStereoViews())
+ .thenReturn(new ViewProjections(viewProjection, viewProjection));
+ break;
+ }
+ }
+ }
+
+ /** Creates a generic glTF entity. */
+ private GltfEntityImpl createGltfEntity() {
+ FakeGltfModelToken modelToken = new FakeGltfModelToken("model");
+ GltfModelResourceImpl model = new GltfModelResourceImpl(modelToken);
+ return new GltfEntityImpl(model, activitySpace, fakeExtensions, entityManager, executor);
+ }
+
+ @Test
+ public void
+ getPoseInActivitySpace_noActivitySpaceOpenXrReferenceSpacePose_returnsIdentityPose() {
+ testActivityPose = createTestActivityPose();
+ Pose pose = new Pose(new Vector3(1, 1, 1), new Quaternion(0, 1, 0, 1));
+ setPerceptionPose(pose);
+ activitySpace.openXrReferenceSpacePose = null;
+
+ assertPose(testActivityPose.getPoseInActivitySpace(), new Pose());
+ }
+
+ @Test
+ public void getPoseInActivitySpace_whenAtSamePose_returnsIdentityPose() {
+ testActivityPose = createTestActivityPose();
+ Pose pose = new Pose(new Vector3(1, 1, 1), new Quaternion(0, 1, 0, 1).toNormalized());
+ setPerceptionPose(pose);
+ activitySpace.setOpenXrReferenceSpacePose(Matrix4.fromPose(pose));
+
+ assertPose(testActivityPose.getPoseInActivitySpace(), new Pose());
+ }
+
+ @Test
+ public void getPoseInActivitySpace_returnsDifferencePose() {
+ testActivityPose = createTestActivityPose();
+ Pose pose = new Pose(new Vector3(1, 1, 1), new Quaternion(0, 1, 0, 1));
+ setPerceptionPose(pose);
+ activitySpace.setOpenXrReferenceSpacePose(Matrix4.Identity);
+
+ assertPose(testActivityPose.getPoseInActivitySpace(), pose);
+ }
+
+ @Test
+ public void getActivitySpaceScale_returnsInverseOfActivitySpaceWorldScale() throws Exception {
+ float activitySpaceScale = 5f;
+ this.activitySpace.setOpenXrReferenceSpacePose(Matrix4.fromScale(activitySpaceScale));
+ testActivityPose = createTestActivityPose();
+ assertVector3(
+ testActivityPose.getActivitySpaceScale(),
+ new Vector3(1f, 1f, 1f).div(activitySpaceScale));
+ }
+
+ @Test
+ public void getPoseInActivitySpace_withNoActivitySpace_returnsIdentityPose() {
+ testActivityPose = createTestActivityPose(/* activitySpace= */ null, activitySpaceRoot);
+
+ assertPose(testActivityPose.getPoseInActivitySpace(), new Pose());
+ }
+
+ @Test
+ public void getActivitySpacePose_whenAtSamePose_returnsIdentityPose() {
+ testActivityPose = createTestActivityPose();
+ Pose pose = new Pose(new Vector3(1, 1, 1), new Quaternion(0, 1, 0, 1).toNormalized());
+ setPerceptionPose(pose);
+ when(activitySpaceRoot.getPoseInActivitySpace()).thenReturn(pose);
+
+ assertPose(testActivityPose.getActivitySpacePose(), new Pose());
+ }
+
+ @Test
+ public void getActivitySpacePose_returnsDifferencePose() {
+ testActivityPose = createTestActivityPose();
+ Pose pose = new Pose(new Vector3(1, 1, 1), new Quaternion(0, 1, 0, 1));
+ setPerceptionPose(pose);
+ when(activitySpaceRoot.getPoseInActivitySpace()).thenReturn(new Pose());
+
+ assertPose(testActivityPose.getActivitySpacePose(), pose);
+ }
+
+ @Test
+ public void getActivitySpacePoseWithError_returnsLastKnownPose() {
+ testActivityPose = createTestActivityPose();
+ Pose pose = new Pose(new Vector3(1, 1, 1), new Quaternion(0, 1, 0, 1));
+ setPerceptionPose(pose);
+ when(activitySpaceRoot.getPoseInActivitySpace()).thenReturn(new Pose());
+ assertPose(testActivityPose.getActivitySpacePose(), pose);
+
+ setPerceptionPose(null);
+ assertPose(testActivityPose.getActivitySpacePose(), pose);
+ }
+
+ @Test
+ public void getActivitySpacePose_withNonAndroidXrActivitySpaceRoot_returnsIdentityPose()
+ throws Exception {
+ testActivityPose = createTestActivityPose(activitySpace, /* activitySpaceRoot= */ null);
+
+ assertPose(testActivityPose.getActivitySpacePose(), new Pose());
+ }
+
+ @Test
+ public void transformPoseTo_withActivitySpace_returnsTransformedPose() {
+ testActivityPose = createTestActivityPose();
+ Pose pose = new Pose(new Vector3(1f, 2f, 3f), Quaternion.Identity);
+ setPerceptionPose(pose);
+ activitySpace.setOpenXrReferenceSpacePose(Matrix4.Identity);
+
+ Pose userHeadSpaceOffset =
+ new Pose(
+ new Vector3(10f, 0f, 0f),
+ Quaternion.fromEulerAngles(new Vector3(0f, 0f, 90f)));
+ Pose transformedPose = testActivityPose.transformPoseTo(userHeadSpaceOffset, activitySpace);
+ assertPose(
+ transformedPose,
+ new Pose(
+ new Vector3(11f, 2f, 3f),
+ Quaternion.fromEulerAngles(new Vector3(0f, 0f, 90f))));
+ }
+
+ @Test
+ public void transformPoseTo_fromActivitySpaceChild_returnsUserHeadSpacePose() {
+ testActivityPose = createTestActivityPose();
+ GltfEntityImpl childEntity1 = createGltfEntity();
+ Pose pose = new Pose(new Vector3(1f, 2f, 3f), Quaternion.Identity);
+ Pose childPose = new Pose(new Vector3(-1f, -2f, -3f), Quaternion.Identity);
+
+ activitySpace.setOpenXrReferenceSpacePose(Matrix4.Identity);
+ activitySpace.addChild(childEntity1);
+ childEntity1.setPose(childPose);
+ setPerceptionPose(pose);
+
+ assertPose(
+ activitySpace.transformPoseTo(new Pose(), testActivityPose),
+ new Pose(new Vector3(-1f, -2f, -3f), Quaternion.Identity));
+
+ Pose transformedPose = childEntity1.transformPoseTo(new Pose(), testActivityPose);
+ assertPose(transformedPose, new Pose(new Vector3(-2f, -4f, -6f), Quaternion.Identity));
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/PanelEntityImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/PanelEntityImplTest.java
new file mode 100644
index 0000000..feeedf5
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/PanelEntityImplTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.Binder;
+import android.view.Display;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+
+import androidx.xr.extensions.node.Node;
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.PixelDimensions;
+import androidx.xr.scenecore.impl.perception.PerceptionLibrary;
+import androidx.xr.scenecore.impl.perception.Session;
+import androidx.xr.scenecore.testing.FakeImpressApi;
+import androidx.xr.scenecore.testing.FakeScheduledExecutorService;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+
+import com.google.androidxr.splitengine.SplitEngineSubspaceManager;
+import com.google.ar.imp.view.splitengine.ImpSplitEngineRenderer;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.android.controller.ActivityController;
+
+import java.util.Objects;
+
+@RunWith(RobolectricTestRunner.class)
+public class PanelEntityImplTest {
+ private static final Dimensions kVgaResolutionPx = new Dimensions(640f, 480f, 0f);
+ private static final Dimensions kHdResolutionPx = new Dimensions(1280f, 720f, 0f);
+ private final FakeXrExtensions fakeExtensions = new FakeXrExtensions();
+ FakeImpressApi fakeImpressApi = new FakeImpressApi();
+ private final ActivityController<Activity> activityController =
+ Robolectric.buildActivity(Activity.class);
+ private final Activity activity = activityController.create().start().get();
+ private final FakeScheduledExecutorService fakeExecutor = new FakeScheduledExecutorService();
+ private final PerceptionLibrary perceptionLibrary = mock(PerceptionLibrary.class);
+ private final EntityManager entityManager = new EntityManager();
+ private JxrPlatformAdapterAxr testRuntime;
+
+ SplitEngineSubspaceManager splitEngineSubspaceManager =
+ Mockito.mock(SplitEngineSubspaceManager.class);
+ ImpSplitEngineRenderer splitEngineRenderer = Mockito.mock(ImpSplitEngineRenderer.class);
+
+ @Before
+ public void setUp() {
+ when(perceptionLibrary.initSession(eq(activity), anyInt(), eq(fakeExecutor)))
+ .thenReturn(immediateFuture(Mockito.mock(Session.class)));
+
+ testRuntime =
+ JxrPlatformAdapterAxr.create(
+ activity,
+ fakeExecutor,
+ fakeExtensions,
+ fakeImpressApi,
+ new EntityManager(),
+ perceptionLibrary,
+ splitEngineSubspaceManager,
+ splitEngineRenderer,
+ /* useSplitEngine= */ false);
+ }
+
+ private PanelEntityImpl createPanelEntity(Dimensions surfaceDimensionsPx) {
+ Display display = activity.getSystemService(DisplayManager.class).getDisplays()[0];
+ Context displayContext = activity.createDisplayContext(display);
+ View view = new View(displayContext);
+ view.setLayoutParams(new LayoutParams(640, 480));
+ SurfaceControlViewHost surfaceControlViewHost =
+ new SurfaceControlViewHost(
+ displayContext,
+ Objects.requireNonNull(displayContext.getDisplay()),
+ new Binder());
+ surfaceControlViewHost.setView(
+ view, (int) surfaceDimensionsPx.width, (int) surfaceDimensionsPx.height);
+ Node node = fakeExtensions.createNode();
+
+ PanelEntityImpl panelEntity =
+ new PanelEntityImpl(
+ node,
+ fakeExtensions,
+ entityManager,
+ surfaceControlViewHost,
+ new PixelDimensions(
+ (int) surfaceDimensionsPx.width, (int) surfaceDimensionsPx.height),
+ fakeExecutor);
+
+ // TODO(b/352829122): introduce a TestRootEntity which can serve as a parent
+ panelEntity.setParent(testRuntime.getActivitySpaceRootImpl());
+ return panelEntity;
+ }
+
+ @Test
+ public void getSizeForPanelEntity_returnsSizeInMeters() {
+ PanelEntityImpl panelEntity = createPanelEntity(kVgaResolutionPx);
+
+ // The (FakeXrExtensions) test default pixel density is 1 pixel per meter.
+ assertThat(panelEntity.getSize().width).isEqualTo(640f);
+ assertThat(panelEntity.getSize().height).isEqualTo(480f);
+ assertThat(panelEntity.getSize().depth).isEqualTo(0f);
+ }
+
+ @Test
+ public void setSizeForPanelEntity_setsSize() {
+ PanelEntityImpl panelEntity = createPanelEntity(kHdResolutionPx);
+
+ // The (FakeXrExtensions) test default pixel density is 1 pixel per meter.
+ assertThat(panelEntity.getSize().width).isEqualTo(1280f);
+ assertThat(panelEntity.getSize().height).isEqualTo(720f);
+ assertThat(panelEntity.getSize().depth).isEqualTo(0f);
+
+ panelEntity.setSize(kVgaResolutionPx);
+
+ assertThat(panelEntity.getSize().width).isEqualTo(640f);
+ assertThat(panelEntity.getSize().height).isEqualTo(480f);
+ assertThat(panelEntity.getSize().depth).isEqualTo(0f);
+ }
+
+ @Test
+ public void setSizeForPanelEntity_updatesPixelDimensions() {
+ PanelEntityImpl panelEntity = createPanelEntity(kHdResolutionPx);
+
+ // The (FakeXrExtensions) test default pixel density is 1 pixel per meter.
+ assertThat(panelEntity.getSize().width).isEqualTo(1280f);
+ assertThat(panelEntity.getSize().height).isEqualTo(720f);
+ assertThat(panelEntity.getSize().depth).isEqualTo(0f);
+
+ panelEntity.setSize(kVgaResolutionPx);
+
+ assertThat(panelEntity.getSize().width).isEqualTo(640f);
+ assertThat(panelEntity.getSize().height).isEqualTo(480f);
+ assertThat(panelEntity.getSize().depth).isEqualTo(0f);
+
+ assertThat(panelEntity.getPixelDimensions().width).isEqualTo(640);
+ assertThat(panelEntity.getPixelDimensions().height).isEqualTo(480);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/PerceptionSpaceActivityPoseImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/PerceptionSpaceActivityPoseImplTest.java
new file mode 100644
index 0000000..45c4085
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/PerceptionSpaceActivityPoseImplTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static androidx.xr.runtime.testing.math.MathAssertions.assertPose;
+import static androidx.xr.runtime.testing.math.MathAssertions.assertVector3;
+
+import androidx.xr.extensions.node.Mat4f;
+import androidx.xr.runtime.math.Matrix4;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Quaternion;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.testing.FakeScheduledExecutorService;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeGltfModelToken;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeNode;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeNodeTransform;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public final class PerceptionSpaceActivityPoseImplTest {
+
+ private final AndroidXrEntity activitySpaceRoot = Mockito.mock(AndroidXrEntity.class);
+ private final FakeXrExtensions fakeExtensions = new FakeXrExtensions();
+ private final FakeScheduledExecutorService executor = new FakeScheduledExecutorService();
+ private final EntityManager entityManager = new EntityManager();
+ private final ActivitySpaceImpl activitySpace =
+ new ActivitySpaceImpl(
+ fakeExtensions.createNode(),
+ fakeExtensions,
+ entityManager,
+ () -> fakeExtensions.fakeSpatialState,
+ executor);
+
+ private PerceptionSpaceActivityPoseImpl perceptionSpaceActivityPose;
+
+ private FakeNode getActivitySpaceNode() {
+ return (FakeNode) activitySpace.getNode();
+ }
+
+ /** Creates a generic glTF entity. */
+ private GltfEntityImpl createGltfEntity() {
+ FakeGltfModelToken modelToken = new FakeGltfModelToken("model");
+ GltfModelResourceImpl model = new GltfModelResourceImpl(modelToken);
+ return new GltfEntityImpl(model, activitySpace, fakeExtensions, entityManager, executor);
+ }
+
+ @Before
+ public void setUp() {
+ perceptionSpaceActivityPose =
+ new PerceptionSpaceActivityPoseImpl(activitySpace, activitySpaceRoot);
+ }
+
+ @Test
+ public void getPoseInActivitySpace_returnsInverseOfActivitySpacePose() {
+ Matrix4 activitySpaceMatrix =
+ Matrix4.fromTrs(
+ new Vector3(1.0f, 2.0f, 3.0f),
+ Quaternion.fromEulerAngles(new Vector3(0f, 0f, 90f)),
+ new Vector3(1.0f, 1.0f, 1.0f));
+ getActivitySpaceNode()
+ .sendTransformEvent(
+ new FakeNodeTransform(new Mat4f(activitySpaceMatrix.getData())));
+ executor.runAll();
+
+ Pose poseInActivitySpace = perceptionSpaceActivityPose.getPoseInActivitySpace();
+
+ Pose expectedPose = activitySpaceMatrix.getInverse().getPose();
+ assertPose(poseInActivitySpace, expectedPose);
+ }
+
+ @Test
+ public void transformPoseTo_returnsCorrectPose() {
+ Matrix4 activitySpaceMatrix =
+ Matrix4.fromTrs(
+ new Vector3(4.0f, 5.0f, 6.0f),
+ Quaternion.fromEulerAngles(new Vector3(90f, 0f, 0f)),
+ new Vector3(1.0f, 1.0f, 1.0f));
+ getActivitySpaceNode()
+ .sendTransformEvent(
+ new FakeNodeTransform(new Mat4f(activitySpaceMatrix.getData())));
+ executor.runAll();
+
+ Pose transformedPose =
+ perceptionSpaceActivityPose.transformPoseTo(new Pose(), activitySpace);
+
+ Pose expectedPose = activitySpaceMatrix.getInverse().getPose();
+ assertPose(transformedPose, expectedPose);
+ }
+
+ @Test
+ public void transformPoseTo_toScaledEntity_returnsCorrectPose() {
+ Matrix4 activitySpaceMatrix =
+ Matrix4.fromTrs(
+ new Vector3(4.0f, 5.0f, 6.0f),
+ Quaternion.fromEulerAngles(new Vector3(90f, 0f, 0f)).toNormalized(),
+ new Vector3(1.0f, 1.0f, 1.0f));
+ getActivitySpaceNode()
+ .sendTransformEvent(
+ new FakeNodeTransform(new Mat4f(activitySpaceMatrix.getData())));
+ executor.runAll();
+ GltfEntityImpl gltfEntity = createGltfEntity();
+ gltfEntity.setScale(new Vector3(2.0f, 2.0f, 2.0f));
+
+ Pose transformedPose = perceptionSpaceActivityPose.transformPoseTo(new Pose(), gltfEntity);
+
+ Pose unscaledPose = activitySpaceMatrix.getInverse().getPose();
+ Pose expectedPose =
+ new Pose(
+ unscaledPose.getTranslation().times(new Vector3(0.5f, 0.5f, 0.5f)),
+ unscaledPose.getRotation());
+ assertPose(transformedPose, expectedPose);
+ }
+
+ @Test
+ public void getActivitySpaceScale_returnsInverseOfActivitySpaceWorldScale() throws Exception {
+ float activitySpaceScale = 5f;
+ this.activitySpace.setOpenXrReferenceSpacePose(Matrix4.fromScale(activitySpaceScale));
+ assertVector3(
+ perceptionSpaceActivityPose.getActivitySpaceScale(),
+ new Vector3(1f, 1f, 1f).div(activitySpaceScale));
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/PlaneUtilsTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/PlaneUtilsTest.java
new file mode 100644
index 0000000..264fb3b
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/PlaneUtilsTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static androidx.xr.runtime.testing.math.MathAssertions.assertRotation;
+
+import androidx.xr.runtime.math.Quaternion;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class PlaneUtilsTest {
+
+ @Test
+ public void rotateEntityToPlane_rotatesToPlan() {
+ // Moving from (0f, 0f, 0f, 1f) int the common space to (0f, 0f, 0f, 1f) in the plane
+ // rotation
+ // results in an updated rotation of (-0.707f, 0f, 0f, 0.707f). This quaternion represents a
+ // 90
+ // degree rotation around the x-axis Which is expected when the panel is rotated into the
+ // plane's reference space.
+ Quaternion planeRotation = new Quaternion(0f, 0f, 0f, 1f);
+ Quaternion proposedRotation = new Quaternion(0f, 0f, 0f, 1f);
+
+ Quaternion updatedRotation =
+ PlaneUtils.rotateEntityToPlane(proposedRotation, planeRotation);
+
+ Quaternion expectedRotation = new Quaternion(-0.707f, 0f, 0f, 0.707f);
+ assertRotation(updatedRotation, expectedRotation);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/PointerCaptureComponentImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/PointerCaptureComponentImplTest.java
new file mode 100644
index 0000000..11c5ab3
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/PointerCaptureComponentImplTest.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import androidx.xr.extensions.node.InputEvent;
+import androidx.xr.extensions.node.Node;
+import androidx.xr.extensions.node.Vec3;
+import androidx.xr.scenecore.JxrPlatformAdapter;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.InputEventListener;
+import androidx.xr.scenecore.JxrPlatformAdapter.PointerCaptureComponent;
+import androidx.xr.scenecore.JxrPlatformAdapter.PointerCaptureComponent.StateListener;
+import androidx.xr.scenecore.testing.FakeScheduledExecutorService;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeInputEvent;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeNode;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class PointerCaptureComponentImplTest {
+
+ // Static private implementation of fakes so that the last received state can be grabbed.
+ private static class FakeStateListener implements StateListener {
+ public int lastState = -1;
+
+ @Override
+ public void onStateChanged(int newState) {
+ lastState = newState;
+ }
+ }
+
+ private static class FakeInputEventListener implements InputEventListener {
+ public JxrPlatformAdapter.InputEvent lastEvent = null;
+
+ @Override
+ public void onInputEvent(JxrPlatformAdapter.InputEvent event) {
+ lastEvent = event;
+ }
+ }
+
+ private final FakeStateListener stateListener = new FakeStateListener();
+
+ private final FakeInputEventListener inputListener = new FakeInputEventListener();
+
+ private final FakeXrExtensions fakeExtensions = new FakeXrExtensions();
+ private final FakeScheduledExecutorService fakeScheduler = new FakeScheduledExecutorService();
+ private final FakeNode fakeNode = (FakeNode) fakeExtensions.createNode();
+
+ private final Entity entity =
+ new AndroidXrEntity(fakeNode, fakeExtensions, new EntityManager(), fakeScheduler) {};
+
+ @Test
+ public void onAttach_enablesPointerCapture() {
+ PointerCaptureComponentImpl component =
+ new PointerCaptureComponentImpl(directExecutor(), stateListener, inputListener);
+
+ assertThat(component.onAttach(entity)).isTrue();
+
+ assertThat(fakeNode.getPointerCaptureStateCallback()).isNotNull();
+ }
+
+ @Test
+ public void onAttach_setsUpInputEventPropagation() {
+ PointerCaptureComponentImpl component =
+ new PointerCaptureComponentImpl(directExecutor(), stateListener, inputListener);
+ assertThat(component.onAttach(entity)).isTrue();
+
+ FakeInputEvent fakeInput = new FakeInputEvent();
+ fakeInput.setDispatchFlags(InputEvent.DISPATCH_FLAG_CAPTURED_POINTER);
+ fakeInput.setOrigin(new Vec3(0, 0, 0));
+ fakeInput.setDirection(new Vec3(1, 1, 1));
+ fakeNode.sendInputEvent(fakeInput);
+ fakeScheduler.runAll();
+
+ assertThat(inputListener.lastEvent).isNotNull();
+ }
+
+ // This should really be a test on AndroidXrEntity, but that does not have tests so it is here
+ // for
+ // the meantime.
+ @Test
+ public void onAttach_onlyPropagatesCapturedEvents() {
+ PointerCaptureComponentImpl component =
+ new PointerCaptureComponentImpl(directExecutor(), stateListener, inputListener);
+ assertThat(component.onAttach(entity)).isTrue();
+
+ FakeInputEvent fakeCapturedInput = new FakeInputEvent();
+ fakeCapturedInput.setDispatchFlags(InputEvent.DISPATCH_FLAG_CAPTURED_POINTER);
+ fakeCapturedInput.setTimestamp(100);
+ fakeCapturedInput.setOrigin(new Vec3(0, 0, 0));
+ fakeCapturedInput.setDirection(new Vec3(1, 1, 1));
+
+ FakeInputEvent fakeInput = new FakeInputEvent();
+ fakeInput.setTimestamp(200);
+ fakeInput.setOrigin(new Vec3(0, 0, 0));
+ fakeInput.setDirection(new Vec3(1, 1, 1));
+
+ fakeNode.sendInputEvent(fakeCapturedInput);
+ fakeNode.sendInputEvent(fakeInput);
+
+ fakeScheduler.runAll();
+
+ assertThat(inputListener.lastEvent).isNotNull();
+ assertThat(inputListener.lastEvent.timestamp).isEqualTo(fakeCapturedInput.getTimestamp());
+ }
+
+ @Test
+ public void onAttach_propagatesInputOnCorrectThread() {
+ FakeScheduledExecutorService propagationExecutor = new FakeScheduledExecutorService();
+ PointerCaptureComponentImpl component =
+ new PointerCaptureComponentImpl(propagationExecutor, stateListener, inputListener);
+ assertThat(component.onAttach(entity)).isTrue();
+
+ FakeInputEvent fakeCapturedInput = new FakeInputEvent();
+ fakeCapturedInput.setDispatchFlags(InputEvent.DISPATCH_FLAG_CAPTURED_POINTER);
+ fakeCapturedInput.setTimestamp(100);
+ fakeCapturedInput.setOrigin(new Vec3(0, 0, 0));
+ fakeCapturedInput.setDirection(new Vec3(1, 1, 1));
+
+ fakeNode.sendInputEvent(fakeCapturedInput);
+
+ assertThat(propagationExecutor.hasNext()).isFalse();
+ // Run the scheduler associated with the Entity so that the component's executor has the
+ // task
+ // scheduled on it.
+ fakeScheduler.runAll();
+
+ assertThat(propagationExecutor.hasNext()).isTrue();
+ }
+
+ @Test
+ public void onAttach_setsUpCorrectStatePropagation() {
+ PointerCaptureComponentImpl component =
+ new PointerCaptureComponentImpl(directExecutor(), stateListener, inputListener);
+ assertThat(component.onAttach(entity)).isTrue();
+
+ fakeNode.getPointerCaptureStateCallback().accept(Node.POINTER_CAPTURE_STATE_PAUSED);
+ assertThat(stateListener.lastState)
+ .isEqualTo(PointerCaptureComponent.POINTER_CAPTURE_STATE_PAUSED);
+
+ fakeNode.getPointerCaptureStateCallback().accept(Node.POINTER_CAPTURE_STATE_ACTIVE);
+ assertThat(stateListener.lastState)
+ .isEqualTo(PointerCaptureComponent.POINTER_CAPTURE_STATE_ACTIVE);
+
+ fakeNode.getPointerCaptureStateCallback().accept(Node.POINTER_CAPTURE_STATE_STOPPED);
+ assertThat(stateListener.lastState)
+ .isEqualTo(PointerCaptureComponent.POINTER_CAPTURE_STATE_STOPPED);
+ }
+
+ @Test
+ public void onAttach_failsIfAlreadyAttached() {
+ PointerCaptureComponentImpl component =
+ new PointerCaptureComponentImpl(directExecutor(), stateListener, inputListener);
+ assertThat(component.onAttach(entity)).isTrue();
+ assertThat(component.onAttach(entity)).isFalse();
+ }
+
+ @Test
+ public void onAttach_failesIfEntityAlreadyHasAnAttachedPointerCaptureComponent() {
+ PointerCaptureComponentImpl component =
+ new PointerCaptureComponentImpl(directExecutor(), stateListener, inputListener);
+ assertThat(component.onAttach(entity)).isTrue();
+
+ PointerCaptureComponentImpl component2 =
+ new PointerCaptureComponentImpl(directExecutor(), stateListener, inputListener);
+ assertThat(component2.onAttach(entity)).isFalse();
+ }
+
+ @Test
+ public void onDetach_stopsPointerCapture() {
+ PointerCaptureComponentImpl component =
+ new PointerCaptureComponentImpl(directExecutor(), stateListener, inputListener);
+ assertThat(component.onAttach(entity)).isTrue();
+
+ component.onDetach(entity);
+
+ assertThat(fakeNode.getPointerCaptureStateCallback()).isNull();
+ }
+
+ @Test
+ public void onDetach_removesInputListener() {
+ PointerCaptureComponentImpl component =
+ new PointerCaptureComponentImpl(directExecutor(), stateListener, inputListener);
+ assertThat(component.onAttach(entity)).isTrue();
+
+ component.onDetach(entity);
+
+ assertThat(fakeNode.getListener()).isNull();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/ResizableComponentImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/ResizableComponentImplTest.java
new file mode 100644
index 0000000..25ec51e
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/ResizableComponentImplTest.java
@@ -0,0 +1,646 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+
+import androidx.xr.extensions.node.Node;
+import androidx.xr.extensions.node.ReformEvent;
+import androidx.xr.extensions.node.ReformOptions;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.scenecore.JxrPlatformAdapter;
+import androidx.xr.scenecore.JxrPlatformAdapter.Dimensions;
+import androidx.xr.scenecore.JxrPlatformAdapter.Entity;
+import androidx.xr.scenecore.JxrPlatformAdapter.MoveEventListener;
+import androidx.xr.scenecore.JxrPlatformAdapter.ResizeEvent;
+import androidx.xr.scenecore.JxrPlatformAdapter.ResizeEventListener;
+import androidx.xr.scenecore.impl.perception.PerceptionLibrary;
+import androidx.xr.scenecore.impl.perception.Session;
+import androidx.xr.scenecore.testing.FakeImpressApi;
+import androidx.xr.scenecore.testing.FakeScheduledExecutorService;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeNode;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeReformEvent;
+
+import com.google.androidxr.splitengine.SplitEngineSubspaceManager;
+import com.google.ar.imp.view.splitengine.ImpSplitEngineRenderer;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.android.controller.ActivityController;
+
+@RunWith(RobolectricTestRunner.class)
+public class ResizableComponentImplTest {
+ private static final Dimensions kMinDimensions = new Dimensions(0f, 0f, 0f);
+ private static final Dimensions kMaxDimensions = new Dimensions(10f, 10f, 10f);
+ private final ActivityController<Activity> activityController =
+ Robolectric.buildActivity(Activity.class);
+ private final Activity activity = activityController.create().start().get();
+ private final FakeScheduledExecutorService fakeExecutor = new FakeScheduledExecutorService();
+ private final PerceptionLibrary perceptionLibrary = mock(PerceptionLibrary.class);
+ private final FakeXrExtensions fakeExtensions = new FakeXrExtensions();
+ private final FakeImpressApi fakeImpressApiImpl = new FakeImpressApi();
+ private final EntityManager entityManager = new EntityManager();
+ private final Node activitySpaceNode = fakeExtensions.createNode();
+ private final ActivitySpaceImpl activitySpaceImpl =
+ new ActivitySpaceImpl(
+ activitySpaceNode,
+ fakeExtensions,
+ entityManager,
+ () -> fakeExtensions.fakeSpatialState,
+ fakeExecutor);
+ private final AndroidXrEntity activitySpaceRoot = Mockito.mock(AndroidXrEntity.class);
+ private final PerceptionSpaceActivityPoseImpl perceptionSpaceActivityPose =
+ new PerceptionSpaceActivityPoseImpl(activitySpaceImpl, activitySpaceRoot);
+ private final PanelShadowRenderer panelShadowRenderer = Mockito.mock(PanelShadowRenderer.class);
+
+ private final SplitEngineSubspaceManager splitEngineSubspaceManager =
+ Mockito.mock(SplitEngineSubspaceManager.class);
+ private final ImpSplitEngineRenderer splitEngineRenderer =
+ Mockito.mock(ImpSplitEngineRenderer.class);
+
+ private Entity createTestEntity() {
+ when(perceptionLibrary.initSession(eq(activity), anyInt(), eq(fakeExecutor)))
+ .thenReturn(immediateFuture(mock(Session.class)));
+ JxrPlatformAdapter fakeRuntime =
+ JxrPlatformAdapterAxr.create(
+ activity,
+ fakeExecutor,
+ fakeExtensions,
+ fakeImpressApiImpl,
+ entityManager,
+ perceptionLibrary,
+ splitEngineSubspaceManager,
+ splitEngineRenderer,
+ /* useSplitEngine= */ false);
+ return fakeRuntime.createEntity(new Pose(), "test", fakeRuntime.getActivitySpace());
+ }
+
+ @Test
+ public void addResizableComponentToTwoEntity_fails() {
+ Entity entity1 = createTestEntity();
+ Entity entity2 = createTestEntity();
+ ResizableComponentImpl resizableComponent =
+ new ResizableComponentImpl(
+ fakeExecutor, fakeExtensions, kMinDimensions, kMaxDimensions);
+ assertThat(resizableComponent).isNotNull();
+ assertThat(entity1.addComponent(resizableComponent)).isTrue();
+ assertThat(entity2.addComponent(resizableComponent)).isFalse();
+ }
+
+ @Test
+ public void addResizableComponent_addsReformOptionsToNode() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ assertThat(entity).isNotNull();
+ ResizableComponentImpl resizableComponent =
+ new ResizableComponentImpl(
+ fakeExecutor, fakeExtensions, kMinDimensions, kMaxDimensions);
+ assertThat(resizableComponent).isNotNull();
+
+ assertThat(entity.addComponent(resizableComponent)).isTrue();
+
+ FakeNode node = (FakeNode) entity.getNode();
+ assertThat(node.getReformOptions().getEnabledReform())
+ .isEqualTo(ReformOptions.ALLOW_RESIZE);
+ assertThat(node.getReformOptions().getMinimumSize().x).isEqualTo(kMinDimensions.width);
+ assertThat(node.getReformOptions().getMinimumSize().y).isEqualTo(kMinDimensions.height);
+ assertThat(node.getReformOptions().getMinimumSize().z).isEqualTo(kMinDimensions.depth);
+ assertThat(node.getReformOptions().getMaximumSize().x).isEqualTo(kMaxDimensions.width);
+ assertThat(node.getReformOptions().getMaximumSize().y).isEqualTo(kMaxDimensions.height);
+ assertThat(node.getReformOptions().getMaximumSize().z).isEqualTo(kMaxDimensions.depth);
+ }
+
+ @Test
+ public void setSizeOnResizableComponent_setsSizeOnNodeReformOptions() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ assertThat(entity).isNotNull();
+ ResizableComponentImpl resizableComponent =
+ new ResizableComponentImpl(
+ fakeExecutor, fakeExtensions, kMinDimensions, kMaxDimensions);
+ assertThat(resizableComponent).isNotNull();
+
+ assertThat(entity.addComponent(resizableComponent)).isTrue();
+ FakeNode node = (FakeNode) entity.getNode();
+
+ resizableComponent.setSize(kMaxDimensions);
+ assertThat(node.getReformOptions().getCurrentSize().x).isEqualTo(kMaxDimensions.width);
+ assertThat(node.getReformOptions().getCurrentSize().y).isEqualTo(kMaxDimensions.height);
+ assertThat(node.getReformOptions().getCurrentSize().z).isEqualTo(kMaxDimensions.depth);
+ }
+
+ @Test
+ public void setMinimumSizeOnResizableComponent_setsMinimumSizeOnNodeReformOptions() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ assertThat(entity).isNotNull();
+ ResizableComponentImpl resizableComponent =
+ new ResizableComponentImpl(
+ fakeExecutor, fakeExtensions, kMinDimensions, kMaxDimensions);
+ assertThat(resizableComponent).isNotNull();
+
+ assertThat(entity.addComponent(resizableComponent)).isTrue();
+ FakeNode node = (FakeNode) entity.getNode();
+
+ resizableComponent.setMinimumSize(kMaxDimensions);
+ assertThat(node.getReformOptions().getMinimumSize().x).isEqualTo(kMaxDimensions.width);
+ assertThat(node.getReformOptions().getMinimumSize().y).isEqualTo(kMaxDimensions.height);
+ assertThat(node.getReformOptions().getMinimumSize().z).isEqualTo(kMaxDimensions.depth);
+ }
+
+ @Test
+ public void setMaximumSizeOnResizableComponent_setsMaximumSizeOnNodeReformOptions() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ assertThat(entity).isNotNull();
+ ResizableComponentImpl resizableComponent =
+ new ResizableComponentImpl(
+ fakeExecutor, fakeExtensions, kMinDimensions, kMaxDimensions);
+ assertThat(resizableComponent).isNotNull();
+
+ assertThat(entity.addComponent(resizableComponent)).isTrue();
+ FakeNode node = (FakeNode) entity.getNode();
+
+ resizableComponent.setMaximumSize(kMinDimensions);
+ assertThat(node.getReformOptions().getMaximumSize().x).isEqualTo(kMinDimensions.width);
+ assertThat(node.getReformOptions().getMaximumSize().y).isEqualTo(kMinDimensions.height);
+ assertThat(node.getReformOptions().getMaximumSize().z).isEqualTo(kMinDimensions.depth);
+ }
+
+ @Test
+ public void setFixedAspectRatioOnResizableComponent_setsFixedAspectRatioOnNodeReformOptions() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ assertThat(entity).isNotNull();
+ ResizableComponentImpl resizableComponent =
+ new ResizableComponentImpl(
+ fakeExecutor, fakeExtensions, kMinDimensions, kMaxDimensions);
+ assertThat(resizableComponent).isNotNull();
+
+ assertThat(entity.addComponent(resizableComponent)).isTrue();
+ FakeNode node = (FakeNode) entity.getNode();
+
+ resizableComponent.setFixedAspectRatio(2.0f);
+ assertThat(node.getReformOptions().getFixedAspectRatio()).isEqualTo(2.0f);
+ resizableComponent.setFixedAspectRatio(0.0f);
+ assertThat(node.getReformOptions().getFixedAspectRatio()).isEqualTo(0.0f);
+ resizableComponent.setFixedAspectRatio(-1.0f);
+ assertThat(node.getReformOptions().getFixedAspectRatio()).isEqualTo(-1.0f);
+ }
+
+ @Test
+ public void addResizableComponentLater_addsReformOptionsToNode() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ assertThat(entity).isNotNull();
+ ResizableComponentImpl resizableComponent =
+ new ResizableComponentImpl(
+ fakeExecutor, fakeExtensions, kMinDimensions, kMaxDimensions);
+ assertThat(resizableComponent).isNotNull();
+ Dimensions testSize = new Dimensions(1f, 1f, 1f);
+ Dimensions testMinSize = new Dimensions(0.25f, 0.25f, 0.25f);
+ Dimensions testMaxSize = new Dimensions(5f, 5f, 5f);
+ resizableComponent.setSize(testSize);
+ resizableComponent.setMinimumSize(testMinSize);
+ resizableComponent.setMaximumSize(testMaxSize);
+
+ assertThat(entity.addComponent(resizableComponent)).isTrue();
+
+ FakeNode node = (FakeNode) entity.getNode();
+ assertThat(node.getReformOptions().getEnabledReform())
+ .isEqualTo(ReformOptions.ALLOW_RESIZE);
+ assertThat(node.getReformOptions().getCurrentSize().x).isEqualTo(testSize.width);
+ assertThat(node.getReformOptions().getCurrentSize().y).isEqualTo(testSize.height);
+ assertThat(node.getReformOptions().getCurrentSize().z).isEqualTo(testSize.depth);
+ assertThat(node.getReformOptions().getMinimumSize().x).isEqualTo(testMinSize.width);
+ assertThat(node.getReformOptions().getMinimumSize().y).isEqualTo(testMinSize.height);
+ assertThat(node.getReformOptions().getMinimumSize().z).isEqualTo(testMinSize.depth);
+ assertThat(node.getReformOptions().getMaximumSize().x).isEqualTo(testMaxSize.width);
+ assertThat(node.getReformOptions().getMaximumSize().y).isEqualTo(testMaxSize.height);
+ assertThat(node.getReformOptions().getMaximumSize().z).isEqualTo(testMaxSize.depth);
+ }
+
+ @Test
+ public void addResizeEventListener_onlyInvokedOnResizeEvent() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ assertThat(entity).isNotNull();
+ ResizableComponentImpl resizableComponent =
+ new ResizableComponentImpl(
+ fakeExecutor, fakeExtensions, kMinDimensions, kMaxDimensions);
+ assertThat(resizableComponent).isNotNull();
+ assertThat(entity.addComponent(resizableComponent)).isTrue();
+ FakeNode node = (FakeNode) entity.getNode();
+ ResizeEventListener mockResizeEventListener = mock(ResizeEventListener.class);
+
+ resizableComponent.addResizeEventListener(directExecutor(), mockResizeEventListener);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+ assertThat(entity.reformEventConsumerMap).isNotEmpty();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ verify(mockResizeEventListener, never()).onResizeEvent(any());
+
+ reformEvent.setType(ReformEvent.REFORM_TYPE_RESIZE);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ verify(mockResizeEventListener).onResizeEvent(any());
+ }
+
+ @Test
+ public void addResizeEventListenerWithExecutor_invokesListenerOnGivenExecutor() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ assertThat(entity).isNotNull();
+ ResizableComponentImpl resizableComponent =
+ new ResizableComponentImpl(
+ fakeExecutor, fakeExtensions, kMinDimensions, kMaxDimensions);
+ assertThat(resizableComponent).isNotNull();
+ assertThat(entity.addComponent(resizableComponent)).isTrue();
+ FakeNode node = (FakeNode) entity.getNode();
+ ResizeEventListener mockResizeEventListener = mock(ResizeEventListener.class);
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ resizableComponent.addResizeEventListener(executorService, mockResizeEventListener);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+ reformEvent.setType(ReformEvent.REFORM_TYPE_RESIZE);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+ verify(mockResizeEventListener).onResizeEvent(any());
+ }
+
+ @Test
+ public void addResizeEventListenerMultiple_invokesAllListeners() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ assertThat(entity).isNotNull();
+ ResizableComponentImpl resizableComponent =
+ new ResizableComponentImpl(
+ fakeExecutor, fakeExtensions, kMinDimensions, kMaxDimensions);
+ assertThat(resizableComponent).isNotNull();
+ assertThat(entity.addComponent(resizableComponent)).isTrue();
+ FakeNode node = (FakeNode) entity.getNode();
+ ResizeEventListener mockResizeEventListener1 = mock(ResizeEventListener.class);
+ ResizeEventListener mockResizeEventListener2 = mock(ResizeEventListener.class);
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ resizableComponent.addResizeEventListener(executorService, mockResizeEventListener1);
+ resizableComponent.addResizeEventListener(executorService, mockResizeEventListener2);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+ reformEvent.setType(ReformEvent.REFORM_TYPE_RESIZE);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+ verify(mockResizeEventListener1).onResizeEvent(any());
+ verify(mockResizeEventListener2).onResizeEvent(any());
+ }
+
+ @Test
+ public void removeResizeEventListenerMultiple_removesGivenListener() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ assertThat(entity).isNotNull();
+ ResizableComponentImpl resizableComponent =
+ new ResizableComponentImpl(
+ fakeExecutor, fakeExtensions, kMinDimensions, kMaxDimensions);
+ assertThat(resizableComponent).isNotNull();
+ assertThat(entity.addComponent(resizableComponent)).isTrue();
+ FakeNode node = (FakeNode) entity.getNode();
+ ResizeEventListener mockResizeEventListener1 = mock(ResizeEventListener.class);
+ ResizeEventListener mockResizeEventListener2 = mock(ResizeEventListener.class);
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ resizableComponent.addResizeEventListener(executorService, mockResizeEventListener1);
+ resizableComponent.addResizeEventListener(executorService, mockResizeEventListener2);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+ reformEvent.setType(ReformEvent.REFORM_TYPE_RESIZE);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+
+ resizableComponent.removeResizeEventListener(mockResizeEventListener1);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+ verify(mockResizeEventListener1).onResizeEvent(any());
+ verify(mockResizeEventListener2, times(2)).onResizeEvent(any());
+ }
+
+ @Test
+ public void removeAllResizeEventListeners_removesReformEventConsumer() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ assertThat(entity).isNotNull();
+ ResizableComponentImpl resizableComponent =
+ new ResizableComponentImpl(
+ fakeExecutor, fakeExtensions, kMinDimensions, kMaxDimensions);
+ assertThat(resizableComponent).isNotNull();
+ assertThat(entity.addComponent(resizableComponent)).isTrue();
+ FakeNode node = (FakeNode) entity.getNode();
+ ResizeEventListener mockResizeEventListener1 = mock(ResizeEventListener.class);
+ ResizeEventListener mockResizeEventListener2 = mock(ResizeEventListener.class);
+ FakeScheduledExecutorService executorService = new FakeScheduledExecutorService();
+
+ resizableComponent.addResizeEventListener(executorService, mockResizeEventListener1);
+ resizableComponent.addResizeEventListener(executorService, mockResizeEventListener2);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+ reformEvent.setType(ReformEvent.REFORM_TYPE_RESIZE);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ assertThat(executorService.hasNext()).isTrue();
+ executorService.runAll();
+ verify(mockResizeEventListener1).onResizeEvent(any());
+ verify(mockResizeEventListener2).onResizeEvent(any());
+
+ resizableComponent.removeResizeEventListener(mockResizeEventListener1);
+ resizableComponent.removeResizeEventListener(mockResizeEventListener2);
+
+ assertThat(entity.reformEventConsumerMap).isEmpty();
+ }
+
+ @Test
+ public void removeResizableComponent_clearsResizeReformOptionsAndResizeEventListeners() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ assertThat(entity).isNotNull();
+ ResizableComponentImpl resizableComponent =
+ new ResizableComponentImpl(
+ fakeExecutor, fakeExtensions, kMinDimensions, kMaxDimensions);
+ assertThat(resizableComponent).isNotNull();
+ assertThat(entity.addComponent(resizableComponent)).isTrue();
+ FakeNode node = (FakeNode) entity.getNode();
+ ResizeEventListener mockResizeEventListener = mock(ResizeEventListener.class);
+
+ resizableComponent.addResizeEventListener(directExecutor(), mockResizeEventListener);
+ assertThat(resizableComponent.reformEventConsumer).isNotNull();
+
+ entity.removeComponent(resizableComponent);
+ assertThat(node.getReformOptions().getEnabledReform() & ReformOptions.ALLOW_RESIZE)
+ .isEqualTo(0);
+ assertThat(entity.reformEventConsumerMap).isEmpty();
+ }
+
+ @Test
+ public void addMoveAndResizeComponents_setsCombinedReformsOptions() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ assertThat(entity).isNotNull();
+ MovableComponentImpl movableComponent =
+ new MovableComponentImpl(
+ true,
+ true,
+ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ true,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ ResizableComponentImpl resizableComponent =
+ new ResizableComponentImpl(
+ fakeExecutor,
+ fakeExtensions,
+ new Dimensions(0f, 0f, 0f),
+ new Dimensions(5f, 5f, 5f));
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ assertThat(entity.addComponent(resizableComponent)).isTrue();
+ FakeNode node = (FakeNode) entity.getNode();
+ assertThat(node.getReformOptions().getEnabledReform())
+ .isEqualTo(ReformOptions.ALLOW_MOVE | ReformOptions.ALLOW_RESIZE);
+ }
+
+ @Test
+ public void addMoveAndResizeComponents_removingMoveKeepsResize() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ assertThat(entity).isNotNull();
+ MovableComponentImpl movableComponent =
+ new MovableComponentImpl(
+ true,
+ true,
+ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ true,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ ResizableComponentImpl resizableComponent =
+ new ResizableComponentImpl(
+ fakeExecutor,
+ fakeExtensions,
+ new Dimensions(0f, 0f, 0f),
+ new Dimensions(5f, 5f, 5f));
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ assertThat(entity.addComponent(resizableComponent)).isTrue();
+ MoveEventListener moveEventListener = mock(MoveEventListener.class);
+ movableComponent.addMoveEventListener(directExecutor(), moveEventListener);
+ ResizeEventListener resizeEventListener = mock(ResizeEventListener.class);
+ resizableComponent.addResizeEventListener(directExecutor(), resizeEventListener);
+ FakeNode node = (FakeNode) entity.getNode();
+ assertThat(node.getReformOptions().getEnabledReform())
+ .isEqualTo(ReformOptions.ALLOW_MOVE | ReformOptions.ALLOW_RESIZE);
+
+ entity.removeComponent(movableComponent);
+ assertThat(node.getReformOptions().getEnabledReform())
+ .isEqualTo(ReformOptions.ALLOW_RESIZE);
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+ reformEvent.setType(ReformEvent.REFORM_TYPE_RESIZE);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ verify(resizeEventListener).onResizeEvent(any());
+
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ verify(moveEventListener, never()).onMoveEvent(any());
+ }
+
+ @Test
+ public void addMoveAndResizeComponents_removingResizeKeepsMove() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ assertThat(entity).isNotNull();
+ MovableComponentImpl movableComponent =
+ new MovableComponentImpl(
+ true,
+ true,
+ ImmutableSet.of(),
+ /* shouldDisposeParentAnchor= */ true,
+ perceptionLibrary,
+ fakeExtensions,
+ activitySpaceImpl,
+ activitySpaceRoot,
+ perceptionSpaceActivityPose,
+ entityManager,
+ panelShadowRenderer,
+ fakeExecutor);
+ ResizableComponentImpl resizableComponent =
+ new ResizableComponentImpl(
+ fakeExecutor,
+ fakeExtensions,
+ new Dimensions(0f, 0f, 0f),
+ new Dimensions(5f, 5f, 5f));
+ assertThat(entity.addComponent(movableComponent)).isTrue();
+ assertThat(entity.addComponent(resizableComponent)).isTrue();
+ MoveEventListener moveEventListener = mock(MoveEventListener.class);
+ movableComponent.addMoveEventListener(directExecutor(), moveEventListener);
+ ResizeEventListener resizeEventListener = mock(ResizeEventListener.class);
+ resizableComponent.addResizeEventListener(directExecutor(), resizeEventListener);
+ FakeNode node = (FakeNode) entity.getNode();
+ assertThat(node.getReformOptions().getEnabledReform())
+ .isEqualTo(ReformOptions.ALLOW_MOVE | ReformOptions.ALLOW_RESIZE);
+
+ entity.removeComponent(resizableComponent);
+ assertThat(node.getReformOptions().getEnabledReform()).isEqualTo(ReformOptions.ALLOW_MOVE);
+
+ FakeReformEvent reformEvent = new FakeReformEvent();
+ reformEvent.setType(ReformEvent.REFORM_TYPE_MOVE);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ verify(moveEventListener).onMoveEvent(any());
+
+ reformEvent.setType(ReformEvent.REFORM_TYPE_RESIZE);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ verify(resizeEventListener, never()).onResizeEvent(any());
+ }
+
+ @Test
+ public void resizableComponent_canAttachAgainAfterDetach() {
+ Entity entity = createTestEntity();
+ assertThat(entity).isNotNull();
+ ResizableComponentImpl resizableComponent =
+ new ResizableComponentImpl(
+ fakeExecutor, fakeExtensions, kMinDimensions, kMaxDimensions);
+ assertThat(resizableComponent).isNotNull();
+ assertThat(entity.addComponent(resizableComponent)).isTrue();
+ entity.removeComponent(resizableComponent);
+ assertThat(entity.addComponent(resizableComponent)).isTrue();
+ }
+
+ @Test
+ public void resizableComponent_hidesEntityDuringResize() {
+ AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
+ assertThat(entity).isNotNull();
+ ResizableComponentImpl resizableComponent =
+ new ResizableComponentImpl(
+ fakeExecutor, fakeExtensions, kMinDimensions, kMaxDimensions);
+ assertThat(resizableComponent).isNotNull();
+ assertThat(entity.addComponent(resizableComponent)).isTrue();
+ entity.setAlpha(0.9f);
+ assertThat(entity.getAlpha()).isEqualTo(0.9f);
+ FakeNode node = (FakeNode) entity.getNode();
+ ResizeEventListener mockResizeEventListener = mock(ResizeEventListener.class);
+
+ resizableComponent.addResizeEventListener(directExecutor(), mockResizeEventListener);
+ assertThat(node.getReformOptions().getEventCallback()).isNotNull();
+ assertThat(node.getReformOptions().getEventExecutor()).isNotNull();
+ assertThat(entity.reformEventConsumerMap).isNotEmpty();
+
+ // Start the resize.
+ FakeReformEvent reformEvent = new FakeReformEvent();
+ reformEvent.setType(ReformEvent.REFORM_TYPE_RESIZE);
+ reformEvent.setState(ReformEvent.REFORM_STATE_START);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ ArgumentCaptor<ResizeEvent> resizeEventCaptor = ArgumentCaptor.forClass(ResizeEvent.class);
+ verify(mockResizeEventListener).onResizeEvent(resizeEventCaptor.capture());
+ ResizeEvent resizeEvent = resizeEventCaptor.getValue();
+ assertThat(resizeEvent.resizeState).isEqualTo(ResizeEvent.RESIZE_STATE_START);
+ assertThat(node.getAlpha()).isEqualTo(0.0f);
+
+ // End the resize.
+ reformEvent.setState(ReformEvent.REFORM_STATE_END);
+ node.getReformOptions()
+ .getEventExecutor()
+ .execute(() -> node.getReformOptions().getEventCallback().accept(reformEvent));
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ verify(mockResizeEventListener, times(2)).onResizeEvent(resizeEventCaptor.capture());
+ resizeEvent = resizeEventCaptor.getAllValues().get(2);
+ assertThat(resizeEvent.resizeState).isEqualTo(ResizeEvent.RESIZE_STATE_END);
+ assertThat(node.getAlpha()).isEqualTo(0.9f);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/RuntimeUtilsTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/RuntimeUtilsTest.java
new file mode 100644
index 0000000..c8d8f88
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/RuntimeUtilsTest.java
@@ -0,0 +1,493 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static androidx.xr.runtime.testing.math.MathAssertions.assertPose;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.util.Log;
+
+import androidx.xr.extensions.environment.EnvironmentVisibilityState;
+import androidx.xr.extensions.environment.PassthroughVisibilityState;
+import androidx.xr.extensions.node.Mat4f;
+import androidx.xr.extensions.node.ReformEvent;
+import androidx.xr.runtime.math.Matrix4;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Quaternion;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter.InputEvent;
+import androidx.xr.scenecore.JxrPlatformAdapter.PlaneSemantic;
+import androidx.xr.scenecore.JxrPlatformAdapter.PlaneType;
+import androidx.xr.scenecore.JxrPlatformAdapter.ResizeEvent;
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatialCapabilities;
+import androidx.xr.scenecore.impl.perception.Plane;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakePassthroughVisibilityState;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.junit.rules.ExpectedLogMessagesRule;
+
+import java.util.regex.Pattern;
+
+@RunWith(RobolectricTestRunner.class)
+public final class RuntimeUtilsTest {
+
+ @Rule
+ public final ExpectedLogMessagesRule expectedLogMessagesRule = new ExpectedLogMessagesRule();
+
+ @Test
+ public void getPlaneTypeHorizontal_returnsHorizontal() {
+ assertThat(RuntimeUtils.getPlaneType(PlaneType.HORIZONTAL))
+ .isEqualTo(Plane.Type.HORIZONTAL_UPWARD_FACING);
+ }
+
+ @Test
+ public void getPlaneTypeVertical_returnsVertical() {
+ assertThat(RuntimeUtils.getPlaneType(PlaneType.VERTICAL)).isEqualTo(Plane.Type.VERTICAL);
+ }
+
+ @Test
+ public void getPlaneTypeAny_returnsArbitrary() {
+ assertThat(RuntimeUtils.getPlaneType(PlaneType.ANY)).isEqualTo(Plane.Type.ARBITRARY);
+ }
+
+ @Test
+ public void getPlaneTypeHorizontalUpwardFacingFromPerception_returnsHorizontal() {
+ assertThat(RuntimeUtils.getPlaneType(Plane.Type.HORIZONTAL_UPWARD_FACING))
+ .isEqualTo(PlaneType.HORIZONTAL);
+ }
+
+ @Test
+ public void getPlaneTypeHorizontalDownwardFacingFromPerception_returnsHorizontal() {
+ assertThat(RuntimeUtils.getPlaneType(Plane.Type.HORIZONTAL_DOWNWARD_FACING))
+ .isEqualTo(PlaneType.HORIZONTAL);
+ }
+
+ @Test
+ public void getPlaneTypeVerticalFromPerception_returnsVertical() {
+ assertThat(RuntimeUtils.getPlaneType(Plane.Type.VERTICAL)).isEqualTo(PlaneType.VERTICAL);
+ }
+
+ @Test
+ public void getPlaneTypeArbitraryFromPerception_returnsAny() {
+ assertThat(RuntimeUtils.getPlaneType(PlaneType.ANY)).isEqualTo(Plane.Type.ARBITRARY);
+ }
+
+ @Test
+ public void getPlaneLabelWall_returnsWall() {
+ assertThat(RuntimeUtils.getPlaneLabel(PlaneSemantic.WALL)).isEqualTo(Plane.Label.WALL);
+ }
+
+ @Test
+ public void getPlaneLabelFloor_returnsFloor() {
+ assertThat(RuntimeUtils.getPlaneLabel(PlaneSemantic.FLOOR)).isEqualTo(Plane.Label.FLOOR);
+ }
+
+ @Test
+ public void getPlaneLabelCeiling_returnsCeiling() {
+ assertThat(RuntimeUtils.getPlaneLabel(PlaneSemantic.CEILING))
+ .isEqualTo(Plane.Label.CEILING);
+ }
+
+ @Test
+ public void getPlaneLabelTable_returnsTable() {
+ assertThat(RuntimeUtils.getPlaneLabel(PlaneSemantic.TABLE)).isEqualTo(Plane.Label.TABLE);
+ }
+
+ @Test
+ public void getPlaneLabelAny_returnsUnknown() {
+ assertThat(RuntimeUtils.getPlaneLabel(PlaneSemantic.ANY)).isEqualTo(Plane.Label.UNKNOWN);
+ }
+
+ @Test
+ public void getPlaneSemanticlWall_returnsWall() {
+ assertThat(RuntimeUtils.getPlaneSemantic(Plane.Label.WALL)).isEqualTo(PlaneSemantic.WALL);
+ }
+
+ @Test
+ public void getPlaneSemanticFloor_returnsFloor() {
+ assertThat(RuntimeUtils.getPlaneSemantic(Plane.Label.FLOOR)).isEqualTo(PlaneSemantic.FLOOR);
+ }
+
+ @Test
+ public void getPlaneSemanticCeiling_returnsCeiling() {
+ assertThat(RuntimeUtils.getPlaneSemantic(Plane.Label.CEILING))
+ .isEqualTo(PlaneSemantic.CEILING);
+ }
+
+ @Test
+ public void getPlaneSemanticTable_returnsTable() {
+ assertThat(RuntimeUtils.getPlaneSemantic(Plane.Label.TABLE)).isEqualTo(PlaneSemantic.TABLE);
+ }
+
+ @Test
+ public void getPlaneSemanticUnknown_returnsAny() {
+ assertThat(RuntimeUtils.getPlaneSemantic(Plane.Label.UNKNOWN)).isEqualTo(PlaneSemantic.ANY);
+ }
+
+ @Test
+ public void getMatrix_returnsMatrix() {
+ float[] expected = new float[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+ Mat4f matrix = new Mat4f(expected);
+
+ assertThat(RuntimeUtils.getMatrix(matrix).getData())
+ .usingExactEquality()
+ .containsExactly(new Matrix4(expected).getData())
+ .inOrder();
+ }
+
+ @Test
+ public void fromPerceptionPose_returnsPose() {
+ Pose expectedPose = new Pose(new Vector3(1, 2, 3), new Quaternion(0, 0, 0, 1));
+ androidx.xr.scenecore.impl.perception.Pose perceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(1, 2, 3, 0, 0, 0, 1);
+
+ assertPose(RuntimeUtils.fromPerceptionPose(perceptionPose), expectedPose);
+ }
+
+ @Test
+ public void poseToPerceptionPose_returnsPerceptionPose() {
+ androidx.xr.scenecore.impl.perception.Pose expectedPerceptionPose =
+ new androidx.xr.scenecore.impl.perception.Pose(1, 2, 3, 0, 0, 0, 1);
+ Pose pose = new Pose(new Vector3(1, 2, 3), new Quaternion(0, 0, 0, 1));
+
+ assertThat(RuntimeUtils.poseToPerceptionPose(pose)).isEqualTo(expectedPerceptionPose);
+ }
+
+ private static final int CAPS_ALL = -1;
+ private static final int CAPS_NONE = -2;
+
+ private androidx.xr.extensions.space.SpatialCapabilities getCapabilities(int... caps) {
+ return new androidx.xr.extensions.space.SpatialCapabilities() {
+ @Override
+ public boolean get(int capQuery) {
+ for (int cap : caps) {
+ if (cap == CAPS_ALL || cap == capQuery) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+ }
+
+ @Test
+ public void convertSpatialCapabilities_noCapabilities() {
+ androidx.xr.extensions.space.SpatialCapabilities extensionCapabilities =
+ getCapabilities(CAPS_NONE);
+ SpatialCapabilities caps = RuntimeUtils.convertSpatialCapabilities(extensionCapabilities);
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_UI)).isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT)).isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL))
+ .isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT))
+ .isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO))
+ .isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY))
+ .isFalse();
+ }
+
+ @Test
+ public void convertSpatialCapabilities_allCapabilities() {
+ androidx.xr.extensions.space.SpatialCapabilities extensionCapabilities =
+ getCapabilities(CAPS_ALL);
+ SpatialCapabilities caps = RuntimeUtils.convertSpatialCapabilities(extensionCapabilities);
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_UI)).isTrue();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT)).isTrue();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL))
+ .isTrue();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT))
+ .isTrue();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO))
+ .isTrue();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY))
+ .isTrue();
+ }
+
+ @Test
+ public void convertSpatialCapabilities_singleCapability() {
+ // check conversions of a few different instances of the extensions SpatialCapabilities that
+ // each have exactly one capability.
+ androidx.xr.extensions.space.SpatialCapabilities extensionCapabilities =
+ getCapabilities(
+ androidx.xr.extensions.space.SpatialCapabilities.SPATIAL_UI_CAPABLE);
+ SpatialCapabilities caps = RuntimeUtils.convertSpatialCapabilities(extensionCapabilities);
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_UI)).isTrue();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT)).isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL))
+ .isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT))
+ .isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO))
+ .isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY))
+ .isFalse();
+
+ extensionCapabilities =
+ getCapabilities(
+ androidx.xr.extensions.space.SpatialCapabilities
+ .SPATIAL_3D_CONTENTS_CAPABLE);
+ caps = RuntimeUtils.convertSpatialCapabilities(extensionCapabilities);
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_UI)).isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT)).isTrue();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL))
+ .isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT))
+ .isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO))
+ .isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY))
+ .isFalse();
+
+ extensionCapabilities =
+ getCapabilities(
+ androidx.xr.extensions.space.SpatialCapabilities.SPATIAL_AUDIO_CAPABLE);
+ caps = RuntimeUtils.convertSpatialCapabilities(extensionCapabilities);
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_UI)).isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT)).isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL))
+ .isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT))
+ .isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO))
+ .isTrue();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY))
+ .isFalse();
+ }
+
+ @Test
+ public void convertSpatialCapabilities_mixedCapabilities() {
+ // Check conversions for a couple of different combinations of capabilities.
+ androidx.xr.extensions.space.SpatialCapabilities extensionCapabilities =
+ getCapabilities(
+ androidx.xr.extensions.space.SpatialCapabilities.SPATIAL_AUDIO_CAPABLE,
+ androidx.xr.extensions.space.SpatialCapabilities
+ .SPATIAL_3D_CONTENTS_CAPABLE);
+ SpatialCapabilities caps = RuntimeUtils.convertSpatialCapabilities(extensionCapabilities);
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_UI)).isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT)).isTrue();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL))
+ .isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT))
+ .isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO))
+ .isTrue();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY))
+ .isFalse();
+
+ extensionCapabilities =
+ extensionCapabilities =
+ getCapabilities(
+ androidx.xr.extensions.space.SpatialCapabilities.SPATIAL_UI_CAPABLE,
+ androidx.xr.extensions.space.SpatialCapabilities
+ .PASSTHROUGH_CONTROL_CAPABLE,
+ androidx.xr.extensions.space.SpatialCapabilities
+ .APP_ENVIRONMENTS_CAPABLE,
+ androidx.xr.extensions.space.SpatialCapabilities
+ .SPATIAL_ACTIVITY_EMBEDDING_CAPABLE);
+ caps = RuntimeUtils.convertSpatialCapabilities(extensionCapabilities);
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_UI)).isTrue();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT)).isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL))
+ .isTrue();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_APP_ENVIRONMENT))
+ .isTrue();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO))
+ .isFalse();
+ assertThat(caps.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY))
+ .isTrue();
+ }
+
+ @Test
+ public void getIsSpatialEnvironmentPreferenceActive_convertsFromExtensionState() {
+ assertThat(
+ RuntimeUtils.getIsSpatialEnvironmentPreferenceActive(
+ EnvironmentVisibilityState.INVISIBLE))
+ .isFalse();
+
+ assertThat(
+ RuntimeUtils.getIsSpatialEnvironmentPreferenceActive(
+ EnvironmentVisibilityState.HOME_VISIBLE))
+ .isFalse();
+
+ assertThat(
+ RuntimeUtils.getIsSpatialEnvironmentPreferenceActive(
+ EnvironmentVisibilityState.APP_VISIBLE))
+ .isTrue();
+ }
+
+ @Test
+ public void getPassthroughOpacity_returnsZeroFromDisabledExtensionState() {
+ PassthroughVisibilityState passthroughVisibilityState =
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.DISABLED, 0.0f);
+ assertThat(RuntimeUtils.getPassthroughOpacity(passthroughVisibilityState)).isEqualTo(0.0f);
+
+ passthroughVisibilityState =
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.DISABLED, 1.0f);
+ assertThat(RuntimeUtils.getPassthroughOpacity(passthroughVisibilityState)).isEqualTo(0.0f);
+ }
+
+ @Test
+ public void getPassthroughOpacity_convertsValidValuesFromExtensionState() {
+ PassthroughVisibilityState passthroughVisibilityState =
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.HOME, 0.5f);
+ assertThat(RuntimeUtils.getPassthroughOpacity(passthroughVisibilityState)).isEqualTo(0.5f);
+
+ passthroughVisibilityState =
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.APP, 0.75f);
+ assertThat(RuntimeUtils.getPassthroughOpacity(passthroughVisibilityState)).isEqualTo(0.75f);
+
+ passthroughVisibilityState =
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.SYSTEM, 1.0f);
+ assertThat(RuntimeUtils.getPassthroughOpacity(passthroughVisibilityState)).isEqualTo(1.0f);
+ }
+
+ @Test
+ public void getPassthroughOpacity_convertsInvalidValuesFromExtensionStateToOneAndLogsError() {
+ PassthroughVisibilityState passthroughVisibilityState =
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.HOME, 0.0f);
+ assertThat(RuntimeUtils.getPassthroughOpacity(passthroughVisibilityState)).isEqualTo(1.0f);
+
+ passthroughVisibilityState =
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.APP, -0.0000001f);
+ assertThat(RuntimeUtils.getPassthroughOpacity(passthroughVisibilityState)).isEqualTo(1.0f);
+
+ passthroughVisibilityState =
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.SYSTEM, -1.0f);
+ assertThat(RuntimeUtils.getPassthroughOpacity(passthroughVisibilityState)).isEqualTo(1.0f);
+
+ expectedLogMessagesRule.expectLogMessagePattern(
+ Log.ERROR,
+ "RuntimeUtils",
+ Pattern.compile(".* Opacity should be greater than zero.*"));
+ }
+
+ @Test
+ public void getInputEventSource_convertsFromExtensionSource() {
+ assertThat(
+ RuntimeUtils.getInputEventSource(
+ androidx.xr.extensions.node.InputEvent.SOURCE_UNKNOWN))
+ .isEqualTo(InputEvent.SOURCE_UNKNOWN);
+ assertThat(
+ RuntimeUtils.getInputEventSource(
+ androidx.xr.extensions.node.InputEvent.SOURCE_HEAD))
+ .isEqualTo(InputEvent.SOURCE_HEAD);
+ assertThat(
+ RuntimeUtils.getInputEventSource(
+ androidx.xr.extensions.node.InputEvent.SOURCE_CONTROLLER))
+ .isEqualTo(InputEvent.SOURCE_CONTROLLER);
+ assertThat(
+ RuntimeUtils.getInputEventSource(
+ androidx.xr.extensions.node.InputEvent.SOURCE_HANDS))
+ .isEqualTo(InputEvent.SOURCE_HANDS);
+ assertThat(
+ RuntimeUtils.getInputEventSource(
+ androidx.xr.extensions.node.InputEvent.SOURCE_MOUSE))
+ .isEqualTo(InputEvent.SOURCE_MOUSE);
+ assertThat(
+ RuntimeUtils.getInputEventSource(
+ androidx.xr.extensions.node.InputEvent.SOURCE_GAZE_AND_GESTURE))
+ .isEqualTo(InputEvent.SOURCE_GAZE_AND_GESTURE);
+ }
+
+ @Test
+ public void getInputEventSource_throwsExceptionForInvalidValue() {
+ assertThrows(IllegalArgumentException.class, () -> RuntimeUtils.getInputEventSource(100));
+ }
+
+ @Test
+ public void getInputEventPointerType_convertsFromExtensionPointerType() {
+ assertThat(
+ RuntimeUtils.getInputEventPointerType(
+ androidx.xr.extensions.node.InputEvent.POINTER_TYPE_DEFAULT))
+ .isEqualTo(InputEvent.POINTER_TYPE_DEFAULT);
+ assertThat(
+ RuntimeUtils.getInputEventPointerType(
+ androidx.xr.extensions.node.InputEvent.POINTER_TYPE_LEFT))
+ .isEqualTo(InputEvent.POINTER_TYPE_LEFT);
+ assertThat(
+ RuntimeUtils.getInputEventPointerType(
+ androidx.xr.extensions.node.InputEvent.POINTER_TYPE_RIGHT))
+ .isEqualTo(InputEvent.POINTER_TYPE_RIGHT);
+ }
+
+ @Test
+ public void getInputEventPointerType_throwsExceptionForInvalidValue() {
+ assertThrows(
+ IllegalArgumentException.class, () -> RuntimeUtils.getInputEventPointerType(100));
+ }
+
+ @Test
+ public void getInputEventAction_convertsFromExtensionAction() {
+ assertThat(
+ RuntimeUtils.getInputEventAction(
+ androidx.xr.extensions.node.InputEvent.ACTION_DOWN))
+ .isEqualTo(InputEvent.ACTION_DOWN);
+ assertThat(
+ RuntimeUtils.getInputEventAction(
+ androidx.xr.extensions.node.InputEvent.ACTION_UP))
+ .isEqualTo(InputEvent.ACTION_UP);
+ assertThat(
+ RuntimeUtils.getInputEventAction(
+ androidx.xr.extensions.node.InputEvent.ACTION_MOVE))
+ .isEqualTo(InputEvent.ACTION_MOVE);
+ assertThat(
+ RuntimeUtils.getInputEventAction(
+ androidx.xr.extensions.node.InputEvent.ACTION_CANCEL))
+ .isEqualTo(InputEvent.ACTION_CANCEL);
+ assertThat(
+ RuntimeUtils.getInputEventAction(
+ androidx.xr.extensions.node.InputEvent.ACTION_HOVER_MOVE))
+ .isEqualTo(InputEvent.ACTION_HOVER_MOVE);
+ assertThat(
+ RuntimeUtils.getInputEventAction(
+ androidx.xr.extensions.node.InputEvent.ACTION_HOVER_ENTER))
+ .isEqualTo(InputEvent.ACTION_HOVER_ENTER);
+ assertThat(
+ RuntimeUtils.getInputEventAction(
+ androidx.xr.extensions.node.InputEvent.ACTION_HOVER_EXIT))
+ .isEqualTo(InputEvent.ACTION_HOVER_EXIT);
+ }
+
+ @Test
+ public void getInputEventAction_throwsExceptionForInvalidValue() {
+ assertThrows(IllegalArgumentException.class, () -> RuntimeUtils.getInputEventAction(100));
+ }
+
+ @Test
+ public void getResizeEventState_convertsFromExtensionResizeState() {
+ assertThat(RuntimeUtils.getResizeEventState(ReformEvent.REFORM_STATE_UNKNOWN))
+ .isEqualTo(ResizeEvent.RESIZE_STATE_UNKNOWN);
+ assertThat(RuntimeUtils.getResizeEventState(ReformEvent.REFORM_STATE_START))
+ .isEqualTo(ResizeEvent.RESIZE_STATE_START);
+ assertThat(RuntimeUtils.getResizeEventState(ReformEvent.REFORM_STATE_ONGOING))
+ .isEqualTo(ResizeEvent.RESIZE_STATE_ONGOING);
+ assertThat(RuntimeUtils.getResizeEventState(ReformEvent.REFORM_STATE_END))
+ .isEqualTo(ResizeEvent.RESIZE_STATE_END);
+ }
+
+ @Test
+ public void getResizeEventState_throwsExceptionForInvalidValue() {
+ assertThrows(IllegalArgumentException.class, () -> RuntimeUtils.getResizeEventState(100));
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/SoundPoolExtensionsWrapperImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/SoundPoolExtensionsWrapperImplTest.java
new file mode 100644
index 0000000..03ca1ab
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/SoundPoolExtensionsWrapperImplTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.media.SoundPool;
+
+import androidx.xr.extensions.node.Node;
+import androidx.xr.scenecore.JxrPlatformAdapter;
+import androidx.xr.scenecore.JxrPlatformAdapter.SoundPoolExtensionsWrapper;
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatializerConstants;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeSoundPoolExtensions;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeSpatialAudioExtensions;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public final class SoundPoolExtensionsWrapperImplTest {
+
+ private static final int TEST_SOUND_ID = 0;
+ private static final float TEST_VOLUME = 0F;
+ private static final int TEST_PRIORITY = 0;
+ private static final int TEST_LOOP = 0;
+ private static final float TEST_RATE = 0F;
+
+ FakeXrExtensions fakeXrExtensions;
+ FakeSpatialAudioExtensions fakeSpatialAudioExtensions;
+ FakeSoundPoolExtensions fakeSoundPoolExtensions;
+
+ @Before
+ public void setUp() {
+ fakeXrExtensions = new FakeXrExtensions();
+ fakeSpatialAudioExtensions = fakeXrExtensions.fakeSpatialAudioExtensions;
+ fakeSoundPoolExtensions = fakeSpatialAudioExtensions.soundPoolExtensions;
+ }
+
+ @Test
+ public void playWithPointSource_callsExtensionsPlayWithPointSource() {
+ int expected = 123;
+
+ Node fakeNode = new FakeXrExtensions().createNode();
+ AndroidXrEntity entity = mock(AndroidXrEntity.class);
+ when(entity.getNode()).thenReturn(fakeNode);
+ JxrPlatformAdapter.PointSourceAttributes rtAttributes =
+ new JxrPlatformAdapter.PointSourceAttributes(entity);
+
+ SoundPool soundPool = new SoundPool.Builder().build();
+
+ fakeSoundPoolExtensions.setPlayAsPointSourceResult(expected);
+ SoundPoolExtensionsWrapper wrapper =
+ new SoundPoolExtensionsWrapperImpl(fakeSoundPoolExtensions);
+ int actual =
+ wrapper.play(
+ soundPool,
+ TEST_SOUND_ID,
+ rtAttributes,
+ TEST_VOLUME,
+ TEST_PRIORITY,
+ TEST_LOOP,
+ TEST_RATE);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void playWithSoundField_callsExtensionsPlayWithSoundField() {
+ int expected = 312;
+
+ SoundPool soundPool = new SoundPool.Builder().build();
+
+ fakeSoundPoolExtensions.setPlayAsSoundFieldResult(expected);
+ SoundPoolExtensionsWrapper wrapper =
+ new SoundPoolExtensionsWrapperImpl(fakeSoundPoolExtensions);
+ JxrPlatformAdapter.SoundFieldAttributes attributes =
+ new JxrPlatformAdapter.SoundFieldAttributes(
+ JxrPlatformAdapter.SpatializerConstants.AMBISONICS_ORDER_THIRD_ORDER);
+
+ int actual =
+ wrapper.play(
+ soundPool,
+ TEST_SOUND_ID,
+ attributes,
+ TEST_VOLUME,
+ TEST_PRIORITY,
+ TEST_LOOP,
+ TEST_RATE);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void getSpatialSourceType_returnsFromExtensions() {
+ int expected = SpatializerConstants.SOURCE_TYPE_SOUND_FIELD;
+ SoundPool soundPool = new SoundPool.Builder().build();
+
+ fakeSoundPoolExtensions.setSourceType(expected);
+ SoundPoolExtensionsWrapper wrapper =
+ new SoundPoolExtensionsWrapperImpl(fakeSoundPoolExtensions);
+ int actualSourceType = wrapper.getSpatialSourceType(soundPool, /* streamId= */ 0);
+ assertThat(actualSourceType).isEqualTo(SpatializerConstants.SOURCE_TYPE_SOUND_FIELD);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/SpatialEnvironmentImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/SpatialEnvironmentImplTest.java
new file mode 100644
index 0000000..eb2c039
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/SpatialEnvironmentImplTest.java
@@ -0,0 +1,570 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+
+import androidx.xr.extensions.environment.EnvironmentVisibilityState;
+import androidx.xr.extensions.environment.PassthroughVisibilityState;
+import androidx.xr.extensions.space.SpatialState;
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatialEnvironment.SetPassthroughOpacityPreferenceResult;
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatialEnvironment.SetSpatialEnvironmentPreferenceResult;
+import androidx.xr.scenecore.JxrPlatformAdapter.SpatialEnvironment.SpatialEnvironmentPreference;
+import androidx.xr.scenecore.testing.FakeImpressApi;
+import androidx.xr.scenecore.testing.FakeXrExtensions;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeEnvironmentVisibilityState;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeNode;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakePassthroughVisibilityState;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeSpatialState;
+
+import com.google.androidxr.splitengine.SplitEngineSubspaceManager;
+import com.google.androidxr.splitengine.SubspaceNode;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.android.controller.ActivityController;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+// Technically this doesn't need to be a Robolectric test, since it doesn't directly depend on
+// any Android subsystems. However, we're currently using an Android test runner for consistency
+// with other Android XR impl tests in this directory.
+/**
+ * Unit tests for the AndroidXR implementation of JXRCore's SpatialEnvironment module.
+ *
+ * <p>TODO(b/326748782): Update the FakeExtensions to support better asserts.
+ */
+@RunWith(RobolectricTestRunner.class)
+@SuppressWarnings({"deprecation", "UnnecessarilyFullyQualified"}) // TODO(b/373435470): Remove
+public final class SpatialEnvironmentImplTest {
+ private static final int SUBSPACE_ID = 5;
+ private final FakeImpressApi fakeImpressApi = new FakeImpressApi();
+ private ActivityController<Activity> activityController;
+ private Activity activity;
+ private FakeXrExtensions fakeExtensions = null;
+ private FakeNode subspaceNode;
+ private SubspaceNode expectedSubspace;
+ private SpatialEnvironmentImpl environment = null;
+ private SplitEngineSubspaceManager splitEngineSubspaceManager;
+
+ @Before
+ public void setUp() {
+ activityController = Robolectric.buildActivity(Activity.class);
+ activity = activityController.create().start().get();
+ // Reset our state.
+ fakeExtensions = new FakeXrExtensions();
+ FakeNode fakeSceneRootNode = (FakeNode) fakeExtensions.createNode();
+ subspaceNode = (FakeNode) fakeExtensions.createNode();
+ expectedSubspace = new SubspaceNode(SUBSPACE_ID, subspaceNode);
+
+ splitEngineSubspaceManager = Mockito.mock(SplitEngineSubspaceManager.class);
+
+ environment =
+ new SpatialEnvironmentImpl(
+ activity,
+ fakeExtensions,
+ fakeSceneRootNode,
+ this::getSpatialState,
+ /* useSplitEngine= */ false);
+ environment.onSplitEngineReady(splitEngineSubspaceManager, fakeImpressApi);
+ }
+
+ private void setupSplitEngineEnvironmentImpl() {
+ FakeNode fakeSceneRootNode = (FakeNode) fakeExtensions.createNode();
+
+ when(splitEngineSubspaceManager.createSubspace(anyString(), anyInt()))
+ .thenReturn(expectedSubspace);
+
+ environment =
+ new SpatialEnvironmentImpl(
+ activity,
+ fakeExtensions,
+ fakeSceneRootNode,
+ this::getSpatialState,
+ /* useSplitEngine= */ true);
+ environment.onSplitEngineReady(splitEngineSubspaceManager, fakeImpressApi);
+ }
+
+ @SuppressWarnings({"FutureReturnValueIgnored", "AndroidJdkLibsChecker"})
+ private androidx.xr.extensions.asset.EnvironmentToken fakeLoadEnvironment(String name) {
+ try {
+ return fakeExtensions.loadEnvironment(null, 0, 0, name).get();
+ } catch (Exception e) {
+ if (e instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ return null;
+ }
+ }
+
+ @SuppressWarnings({"FutureReturnValueIgnored", "AndroidJdkLibsChecker"})
+ private androidx.xr.extensions.asset.GltfModelToken fakeLoadGltfModel(String name) {
+ try {
+ return fakeExtensions.loadGltfModel(null, 0, 0, name).get();
+ } catch (Exception e) {
+ if (e instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ return null;
+ }
+ }
+
+ @SuppressWarnings({"FutureReturnValueIgnored", "AndroidJdkLibsChecker"})
+ private long fakeLoadGltfModelSplitEngine(String name) {
+ try {
+ return fakeImpressApi.loadGltfModel(name).get();
+ } catch (Exception e) {
+ if (e instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ return -1;
+ }
+ }
+
+ private SpatialState getSpatialState() {
+ return fakeExtensions.fakeSpatialState;
+ }
+
+ @Test
+ public void setPassthroughOpacityPreference() {
+ environment.setPassthroughOpacityPreference(null);
+ assertThat(environment.getPassthroughOpacityPreference()).isNull();
+
+ environment.setPassthroughOpacityPreference(0.1f);
+ assertThat(environment.getPassthroughOpacityPreference()).isEqualTo(0.1f);
+ }
+
+ @Test
+ public void setPassthroughOpacityPreferenceNearOrUnderZero_getsZeroOpacity() {
+ // Opacity values below 1% should be treated as zero.
+ environment.setPassthroughOpacityPreference(0.009f);
+ assertThat(environment.getPassthroughOpacityPreference()).isEqualTo(0.0f);
+
+ environment.setPassthroughOpacityPreference(-0.1f);
+ assertThat(environment.getPassthroughOpacityPreference()).isEqualTo(0.0f);
+ }
+
+ @Test
+ public void setPassthroughOpacityPreferenceNearOrOverOne_getsFullOpacity() {
+ // Opacity values above 99% should be treated as full opacity.
+ environment.setPassthroughOpacityPreference(0.991f);
+ assertThat(environment.getPassthroughOpacityPreference()).isEqualTo(1.0f);
+
+ environment.setPassthroughOpacityPreference(1.1f);
+ assertThat(environment.getPassthroughOpacityPreference()).isEqualTo(1.0f);
+ }
+
+ @Test
+ public void setPassthroughOpacityPreference_returnsAccordingToSpatialCapabilities() {
+ // Change should be applied if the spatial capabilities allow it, otherwise should be
+ // pending.
+ fakeExtensions.fakeSpatialState.setAllSpatialCapabilities(true);
+ assertThat(environment.setPassthroughOpacityPreference(0.5f))
+ .isEqualTo(SetPassthroughOpacityPreferenceResult.CHANGE_APPLIED);
+
+ fakeExtensions.fakeSpatialState.setAllSpatialCapabilities(false);
+ assertThat(environment.setPassthroughOpacityPreference(0.6f))
+ .isEqualTo(SetPassthroughOpacityPreferenceResult.CHANGE_PENDING);
+ }
+
+ @Test
+ public void getCurrentPassthroughOpacity_returnsZeroInitially() {
+ assertThat(environment.getCurrentPassthroughOpacity()).isEqualTo(0.0f);
+ }
+
+ @Test
+ public void onPassthroughOpacityChangedListener_firesOnPassthroughOpacityChange() {
+ @SuppressWarnings(value = "unchecked")
+ Consumer<Float> listener1 = (Consumer<Float>) mock(Consumer.class);
+ @SuppressWarnings(value = "unchecked")
+ Consumer<Float> listener2 = (Consumer<Float>) mock(Consumer.class);
+
+ environment.addOnPassthroughOpacityChangedListener(listener1);
+ environment.addOnPassthroughOpacityChangedListener(listener2);
+
+ environment.firePassthroughOpacityChangedEvent(0.5f);
+ verify(listener1).accept(0.5f);
+ verify(listener2).accept(0.5f);
+
+ environment.removeOnPassthroughOpacityChangedListener(listener1);
+ environment.firePassthroughOpacityChangedEvent(0.0f);
+ verify(listener1)
+ .accept(any()); // Verify the removed listener was called exactly once total
+ verify(listener2).accept(0.0f); // Verify the active listener was called again with false
+ }
+
+ @Test
+ public void getSpatialEnvironmentPreference_returnsSetSpatialEnvironmentPreference() {
+ SpatialEnvironmentPreference preference = mock(SpatialEnvironmentPreference.class);
+ environment.setSpatialEnvironmentPreference(preference);
+ assertThat(environment.getSpatialEnvironmentPreference()).isEqualTo(preference);
+ }
+
+ @Test
+ public void setSpatialEnvironmentPreference_returnsAppliedWhenCapable() {
+ // Change should be applied if the spatial capabilities allow it, otherwise should be
+ // pending.
+ fakeExtensions.fakeSpatialState.setAllSpatialCapabilities(true);
+ SpatialEnvironmentPreference preference = mock(SpatialEnvironmentPreference.class);
+ assertThat(environment.setSpatialEnvironmentPreference(preference))
+ .isEqualTo(SetSpatialEnvironmentPreferenceResult.CHANGE_APPLIED);
+
+ fakeExtensions.fakeSpatialState.setAllSpatialCapabilities(false);
+ preference = mock(SpatialEnvironmentPreference.class);
+ assertThat(environment.setSpatialEnvironmentPreference(preference))
+ .isEqualTo(SetSpatialEnvironmentPreferenceResult.CHANGE_PENDING);
+ }
+
+ @Test
+ public void setSpatialEnvironmentPreferenceNull_removesEnvironment() {
+ androidx.xr.extensions.asset.EnvironmentToken exr = fakeLoadEnvironment("fakeEnvironment");
+ androidx.xr.extensions.asset.GltfModelToken gltf = fakeLoadGltfModel("fakeGltfModel");
+
+ // Ensure that an environment is set.
+ environment.setSpatialEnvironmentPreference(
+ new SpatialEnvironmentPreference(
+ new ExrImageResourceImpl(exr), new GltfModelResourceImpl(gltf)));
+
+ FakeNode skyboxNode = fakeExtensions.testGetNodeWithEnvironmentToken(exr);
+ FakeNode geometryNode = fakeExtensions.testGetNodeWithGltfToken(gltf);
+
+ assertThat(skyboxNode).isNotNull();
+ assertThat(geometryNode).isNotNull();
+
+ assertThat(skyboxNode.getParent()).isNotNull();
+ assertThat(geometryNode.getParent()).isNotNull();
+
+ // Ensure environment is removed
+ environment.setSpatialEnvironmentPreference(null);
+
+ assertThat(skyboxNode.getParent()).isNull();
+ assertThat(geometryNode.getParent()).isNull();
+ assertThat(fakeExtensions.getFakeEnvironmentNode()).isNull();
+ }
+
+ @Test
+ public void setSpatialEnvironmentPreferenceNullWithSplitEngine_removesEnvironment() {
+ setupSplitEngineEnvironmentImpl();
+
+ androidx.xr.extensions.asset.EnvironmentToken exr = fakeLoadEnvironment("fakeEnvironment");
+ long gltf = fakeLoadGltfModelSplitEngine("fakeGltfModel");
+
+ // Ensure that an environment is set.
+ environment.setSpatialEnvironmentPreference(
+ new SpatialEnvironmentPreference(
+ new ExrImageResourceImpl(exr), new GltfModelResourceImplSplitEngine(gltf)));
+
+ FakeNode skyboxNode = fakeExtensions.testGetNodeWithEnvironmentToken(exr);
+ List<Integer> geometryNodes = fakeImpressApi.getImpressNodesForToken(gltf);
+
+ assertThat(skyboxNode).isNotNull();
+ assertThat(geometryNodes).isNotEmpty();
+
+ assertThat(skyboxNode.getParent()).isNotNull();
+ assertThat(fakeImpressApi.impressNodeHasParent(geometryNodes.get(0))).isTrue();
+
+ // Ensure environment is removed
+ environment.setSpatialEnvironmentPreference(null);
+
+ assertThat(skyboxNode.getParent()).isNull();
+ // TODO: b/354711945 - Uncomment when we can test the SetGeometrySplitEngine(null) path.
+ // assertThat(fakeImpressApi.impressNodeHasParent(geometryNodes.get(0))).isFalse();
+ assertThat(fakeExtensions.getFakeEnvironmentNode()).isNull();
+ }
+
+ @Test
+ public void
+ setSpatialEnvironmentPreferenceWithNullSkyboxAndGeometry_doesNotDetachEnvironment() {
+ androidx.xr.extensions.asset.EnvironmentToken exr = fakeLoadEnvironment("fakeEnvironment");
+ androidx.xr.extensions.asset.GltfModelToken gltf = fakeLoadGltfModel("fakeGltfModel");
+
+ // Ensure that an environment is set.
+ environment.setSpatialEnvironmentPreference(
+ new SpatialEnvironmentPreference(
+ new ExrImageResourceImpl(exr), new GltfModelResourceImpl(gltf)));
+
+ FakeNode skyboxNode = fakeExtensions.testGetNodeWithEnvironmentToken(exr);
+ FakeNode geometryNode = fakeExtensions.testGetNodeWithGltfToken(gltf);
+
+ assertThat(skyboxNode).isNotNull();
+ assertThat(geometryNode).isNotNull();
+
+ assertThat(skyboxNode.getParent()).isNotNull();
+ assertThat(geometryNode.getParent()).isNotNull();
+
+ // Ensure environment is not removed if both skybox and geometry are updated to null.
+ environment.setSpatialEnvironmentPreference(new SpatialEnvironmentPreference(null, null));
+
+ assertThat(skyboxNode.getParent()).isNull();
+ assertThat(geometryNode.getParent()).isNull();
+
+ // TODO: b/371221872 - When the behavior is changed to set the black skybox, the fake env
+ // node
+ // will no longer be null and the commented out line should replace the uncommented line.
+ // This change isn't relevant for end users but it confirms the environment implementation
+ // is working as designed.
+ // assertThat(fakeExtensions.getFakeEnvironmentNode()).isNotNull();
+ assertThat(fakeExtensions.getFakeEnvironmentNode()).isNull();
+ }
+
+ @Test
+ public void
+ setSpatialEnvironmentPreferenceWithNullSkyboxAndGeometrySplitEngine_doesNotDetachEnvironment() {
+ setupSplitEngineEnvironmentImpl();
+ androidx.xr.extensions.asset.EnvironmentToken exr = fakeLoadEnvironment("fakeEnvironment");
+ long gltf = fakeLoadGltfModelSplitEngine("fakeGltfModel");
+
+ // Ensure that an environment is set.
+ environment.setSpatialEnvironmentPreference(
+ new SpatialEnvironmentPreference(
+ new ExrImageResourceImpl(exr), new GltfModelResourceImplSplitEngine(gltf)));
+
+ FakeNode skyboxNode = fakeExtensions.testGetNodeWithEnvironmentToken(exr);
+ List<Integer> geometryNodes = fakeImpressApi.getImpressNodesForToken(gltf);
+
+ assertThat(skyboxNode).isNotNull();
+ assertThat(geometryNodes).isNotEmpty();
+
+ assertThat(skyboxNode.getParent()).isNotNull();
+ assertThat(fakeImpressApi.impressNodeHasParent(geometryNodes.get(0))).isTrue();
+
+ // Ensure environment is not removed if both skybox and geometry are updated to null.
+ environment.setSpatialEnvironmentPreference(new SpatialEnvironmentPreference(null, null));
+
+ assertThat(skyboxNode.getParent()).isNull();
+ // TODO: b/354711945 - Uncomment when we can test the SetGeometrySplitEngine(null) path.
+ // assertThat(fakeImpressApi.impressNodeHasParent(geometryNodes.get(0))).isFalse();
+
+ // TODO: b/371221872 - When the behavior is changed to set the black skybox, the fake env
+ // node
+ // will no longer be null and the commented out line should replace the uncommented line.
+ // This change isn't relevant for end users but it confirms the environment implementation
+ // is working as designed.
+ // assertThat(fakeExtensions.getFakeEnvironmentNode()).isNotNull();
+ assertThat(fakeExtensions.getFakeEnvironmentNode()).isNull();
+ }
+
+ @Test
+ public void setNewSpatialEnvironmentPreference_replacesOldSpatialEnvironmentPreference() {
+ androidx.xr.extensions.asset.EnvironmentToken exr = fakeLoadEnvironment("fakeEnvironment");
+ androidx.xr.extensions.asset.EnvironmentToken newExr =
+ fakeLoadEnvironment("newFakeEnvironment");
+ androidx.xr.extensions.asset.GltfModelToken gltf = fakeLoadGltfModel("fakeGltfModel");
+ androidx.xr.extensions.asset.GltfModelToken newGltf = fakeLoadGltfModel("newFakeGltfModel");
+
+ // Ensure that an environment is set a first time.
+ environment.setSpatialEnvironmentPreference(
+ new SpatialEnvironmentPreference(
+ new ExrImageResourceImpl(exr), new GltfModelResourceImpl(gltf)));
+
+ FakeNode skyboxNode = fakeExtensions.testGetNodeWithEnvironmentToken(exr);
+ FakeNode geometryNode = fakeExtensions.testGetNodeWithGltfToken(gltf);
+
+ // Ensure that an environment is set a second time.
+ environment.setSpatialEnvironmentPreference(
+ new SpatialEnvironmentPreference(
+ new ExrImageResourceImpl(newExr), new GltfModelResourceImpl(newGltf)));
+
+ FakeNode newSkyboxNode = fakeExtensions.testGetNodeWithEnvironmentToken(newExr);
+ FakeNode newGeometryNode = fakeExtensions.testGetNodeWithGltfToken(newGltf);
+
+ // None of the nodes should be null.
+ assertThat(skyboxNode).isNotNull();
+ assertThat(geometryNode).isNotNull();
+ assertThat(newSkyboxNode).isNotNull();
+ assertThat(newGeometryNode).isNotNull();
+
+ // Only the new nodes should have a parent.
+ assertThat(skyboxNode.getParent()).isNull();
+ assertThat(geometryNode.getParent()).isNull();
+ assertThat(newSkyboxNode.getParent()).isNotNull();
+ assertThat(newGeometryNode.getParent()).isNotNull();
+
+ // The names should be the same, but the resources should be different.
+ assertThat(skyboxNode.getEnvironment()).isNotEqualTo(newSkyboxNode.getEnvironment());
+ assertThat(skyboxNode.getName()).isEqualTo(SpatialEnvironmentImpl.SKYBOX_NODE_NAME);
+ assertThat(newSkyboxNode.getName()).isEqualTo(SpatialEnvironmentImpl.SKYBOX_NODE_NAME);
+ assertThat(geometryNode.getGltfModel()).isNotEqualTo(newGeometryNode.getGltfModel());
+ assertThat(geometryNode.getName()).isEqualTo(SpatialEnvironmentImpl.GEOMETRY_NODE_NAME);
+ assertThat(newGeometryNode.getName()).isEqualTo(SpatialEnvironmentImpl.GEOMETRY_NODE_NAME);
+
+ // The environment node should still be attached.
+ assertThat(fakeExtensions.getFakeEnvironmentNode()).isNotNull();
+ }
+
+ @Test
+ public void
+ setNewSpatialEnvironmentPreferenceSplitEngine_replacesOldSpatialEnvironmentPreference() {
+ setupSplitEngineEnvironmentImpl();
+ androidx.xr.extensions.asset.EnvironmentToken exr = fakeLoadEnvironment("fakeEnvironment");
+ androidx.xr.extensions.asset.EnvironmentToken newExr =
+ fakeLoadEnvironment("newFakeEnvironment");
+ long gltf = fakeLoadGltfModelSplitEngine("fakeGltfModel");
+ long newGltf = fakeLoadGltfModelSplitEngine("newFakeGltfModel");
+
+ // Ensure that an environment is set a first time.
+ environment.setSpatialEnvironmentPreference(
+ new SpatialEnvironmentPreference(
+ new ExrImageResourceImpl(exr), new GltfModelResourceImplSplitEngine(gltf)));
+
+ FakeNode skyboxNode = fakeExtensions.testGetNodeWithEnvironmentToken(exr);
+ List<Integer> geometryNodes = fakeImpressApi.getImpressNodesForToken(gltf);
+
+ // Ensure that an environment is set a second time.
+ environment.setSpatialEnvironmentPreference(
+ new SpatialEnvironmentPreference(
+ new ExrImageResourceImpl(newExr),
+ new GltfModelResourceImplSplitEngine(newGltf)));
+
+ FakeNode newSkyboxNode = fakeExtensions.testGetNodeWithEnvironmentToken(newExr);
+ List<Integer> newGeometryNodes = fakeImpressApi.getImpressNodesForToken(newGltf);
+
+ // None of the nodes should be null.
+ assertThat(skyboxNode).isNotNull();
+ assertThat(geometryNodes).isNotEmpty();
+ assertThat(newSkyboxNode).isNotNull();
+ assertThat(newGeometryNodes).isNotEmpty();
+
+ // Only the new nodes should have a parent.
+ assertThat(skyboxNode.getParent()).isNull();
+ // TODO: b/354711945 - Uncomment when we can test the SetGeometrySplitEngine(null) path.
+ // assertThat(fakeImpressApi.impressNodeHasParent(geometryNodes.get(0))).isFalse();
+ assertThat(newSkyboxNode.getParent()).isNotNull();
+ assertThat(fakeImpressApi.impressNodeHasParent(newGeometryNodes.get(0))).isTrue();
+
+ // The resources should be different.
+ assertThat(skyboxNode.getEnvironment()).isNotEqualTo(newSkyboxNode.getEnvironment());
+ assertThat(skyboxNode.getName()).isEqualTo(SpatialEnvironmentImpl.SKYBOX_NODE_NAME);
+ assertThat(newSkyboxNode.getName()).isEqualTo(SpatialEnvironmentImpl.SKYBOX_NODE_NAME);
+ assertThat(geometryNodes.get(0)).isNotEqualTo(newGeometryNodes.get(0));
+
+ // The environment node should still be attached.
+ assertThat(fakeExtensions.getFakeEnvironmentNode()).isNotNull();
+ }
+
+ @Test
+ public void isSpatialEnvironmentPreferenceActive_defaultsToFalse() {
+ assertThat(environment.isSpatialEnvironmentPreferenceActive()).isFalse();
+ }
+
+ @Test
+ public void onSpatialEnvironmentChangedListener_firesOnEnvironmentChange() {
+ @SuppressWarnings(value = "unchecked")
+ Consumer<Boolean> listener1 = (Consumer<Boolean>) mock(Consumer.class);
+ @SuppressWarnings(value = "unchecked")
+ Consumer<Boolean> listener2 = (Consumer<Boolean>) mock(Consumer.class);
+
+ environment.addOnSpatialEnvironmentChangedListener(listener1);
+ environment.addOnSpatialEnvironmentChangedListener(listener2);
+
+ environment.fireOnSpatialEnvironmentChangedEvent(true);
+ verify(listener1).accept(true);
+ verify(listener2).accept(true);
+
+ environment.removeOnSpatialEnvironmentChangedListener(listener1);
+ environment.fireOnSpatialEnvironmentChangedEvent(false);
+ verify(listener1)
+ .accept(any()); // Verify the removed listener was called exactly once total
+ verify(listener2).accept(false); // Verify the active listener was called again with false
+ }
+
+ @Test
+ public void dispose_clearsSpatialEnvironmentPreferenceListeners() {
+ @SuppressWarnings(value = "unchecked")
+ Consumer<Boolean> listener = (Consumer<Boolean>) mock(Consumer.class);
+ environment.addOnSpatialEnvironmentChangedListener(listener);
+
+ environment.fireOnSpatialEnvironmentChangedEvent(true);
+ verify(listener).accept(true);
+
+ environment.dispose();
+ environment.fireOnSpatialEnvironmentChangedEvent(false);
+ verify(listener, never()).accept(false);
+ }
+
+ @Test
+ public void dispose_clearsPassthroughOpacityPreferenceListeners() {
+ @SuppressWarnings(value = "unchecked")
+ Consumer<Float> listener = (Consumer<Float>) mock(Consumer.class);
+ environment.addOnPassthroughOpacityChangedListener(listener);
+
+ environment.firePassthroughOpacityChangedEvent(1.0f);
+ verify(listener).accept(1.0f);
+
+ // Ensure the listener is called exactly once, even if the event is fired after dispose.
+ environment.dispose();
+ environment.firePassthroughOpacityChangedEvent(0.5f);
+ verify(listener).accept(any());
+ }
+
+ @Test
+ public void dispose_clearsResources() {
+ androidx.xr.extensions.asset.EnvironmentToken exr = fakeLoadEnvironment("fakeEnvironment");
+ androidx.xr.extensions.asset.GltfModelToken gltf = fakeLoadGltfModel("fakeGltfModel");
+ FakeSpatialState spatialState = new FakeSpatialState();
+
+ spatialState.setEnvironmentVisibility(
+ new FakeEnvironmentVisibilityState(EnvironmentVisibilityState.APP_VISIBLE));
+ spatialState.setPassthroughVisibility(
+ new FakePassthroughVisibilityState(PassthroughVisibilityState.APP, 0.5f));
+ environment.setSpatialState(spatialState);
+
+ environment.setSpatialEnvironmentPreference(
+ new SpatialEnvironmentPreference(
+ new ExrImageResourceImpl(exr), new GltfModelResourceImpl(gltf)));
+ environment.setPassthroughOpacityPreference(0.5f);
+
+ FakeNode skyboxNode = fakeExtensions.testGetNodeWithEnvironmentToken(exr);
+ FakeNode geometryNode = fakeExtensions.testGetNodeWithGltfToken(gltf);
+
+ assertThat(skyboxNode).isNotNull();
+ assertThat(geometryNode).isNotNull();
+
+ assertThat(skyboxNode.getParent()).isNotNull();
+ assertThat(geometryNode.getParent()).isNotNull();
+
+ assertThat(environment.getSpatialEnvironmentPreference()).isNotNull();
+ assertThat(environment.isSpatialEnvironmentPreferenceActive()).isTrue();
+
+ assertThat(environment.getPassthroughOpacityPreference()).isNotNull();
+ assertThat(environment.getCurrentPassthroughOpacity()).isEqualTo(0.5f);
+
+ environment.dispose();
+ assertThat(skyboxNode.getParent()).isNull();
+ assertThat(geometryNode.getParent()).isNull();
+ assertThat(fakeExtensions.getFakeEnvironmentNode()).isNull();
+ assertThat(environment.getSpatialEnvironmentPreference()).isNull();
+ assertThat(environment.isSpatialEnvironmentPreferenceActive()).isFalse();
+ assertThat(environment.getPassthroughOpacityPreference()).isNull();
+ assertThat(environment.getCurrentPassthroughOpacity()).isEqualTo(0.0f);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/SystemSpaceEntityImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/SystemSpaceEntityImplTest.java
new file mode 100644
index 0000000..82ce0d4
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/SystemSpaceEntityImplTest.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl;
+
+import static androidx.xr.runtime.testing.math.MathAssertions.assertPose;
+import static androidx.xr.runtime.testing.math.MathAssertions.assertVector3;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+
+import androidx.xr.extensions.node.Mat4f;
+import androidx.xr.runtime.math.Matrix4;
+import androidx.xr.runtime.math.Pose;
+import androidx.xr.runtime.math.Quaternion;
+import androidx.xr.runtime.math.Vector3;
+import androidx.xr.scenecore.JxrPlatformAdapter.SystemSpaceEntity.OnSpaceUpdatedListener;
+import androidx.xr.scenecore.testing.FakeScheduledExecutorService;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeCloseable;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeNode;
+import androidx.xr.scenecore.testing.FakeXrExtensions.FakeNodeTransform;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+
+/**
+ * Abstract test class for {@link SystemSpaceEntityImpl} implementations.
+ *
+ * <p>Concrete implementations of {@link SystemSpaceEntityImpl} should extend this class and provide
+ * implementations for its abstract methods to ensure they comply with the abstract class.
+ */
+public abstract class SystemSpaceEntityImplTest {
+
+ /** Returns the {@link SystemSpaceEntityImpl} instance to test. */
+ protected abstract SystemSpaceEntityImpl getSystemSpaceEntityImpl();
+
+ /** Returns the default fake executor used by the {@link SystemSpaceEntityImpl} constructor. */
+ protected abstract FakeScheduledExecutorService getDefaultFakeExecutor();
+
+ /** Returns an arbitrary {@link AndroidXrEntity} instance which can set its parent. */
+ protected abstract AndroidXrEntity createChildAndroidXrEntity();
+
+ /** Returns the {@link ActivitySpaceImpl} instance which is the root of the Activity Space. */
+ protected abstract ActivitySpaceImpl getActivitySpaceEntity();
+
+ @Test
+ public void systemSpaceEntityImplConstructor_setsNodeTransformSubscription() {
+ SystemSpaceEntityImpl systemSpaceEntity = getSystemSpaceEntityImpl();
+ FakeScheduledExecutorService fakeExecutor = getDefaultFakeExecutor();
+ FakeNode node = (FakeNode) systemSpaceEntity.getNode();
+ assertThat(node.getTransformListener()).isNotNull();
+ assertThat(node.getTransformExecutor()).isEqualTo(fakeExecutor);
+ assertThat(systemSpaceEntity.nodeTransformCloseable).isNotNull();
+ }
+
+ @Test
+ public void dispose_closesNodeTransformSubscription() {
+ SystemSpaceEntityImpl systemSpaceEntity = getSystemSpaceEntityImpl();
+ FakeCloseable nodeTransformCloseable =
+ (FakeCloseable) systemSpaceEntity.nodeTransformCloseable;
+ assertThat(nodeTransformCloseable.isClosed()).isFalse();
+
+ systemSpaceEntity.dispose();
+ assertThat(nodeTransformCloseable.isClosed()).isTrue();
+ }
+
+ @Test
+ public void getPoseInOpenXrReferenceSpace_defaultsToNull() {
+ assertThat(getSystemSpaceEntityImpl().getPoseInOpenXrReferenceSpace()).isNull();
+ }
+
+ @Test
+ public void setOnSpaceUpdatedListener_callListenersOnActivitySpaceUpdated() {
+ SystemSpaceEntityImpl systemSpaceEntity = getSystemSpaceEntityImpl();
+ OnSpaceUpdatedListener listener1 = Mockito.mock(OnSpaceUpdatedListener.class);
+ FakeScheduledExecutorService executor1 = new FakeScheduledExecutorService();
+
+ systemSpaceEntity.setOnSpaceUpdatedListener(listener1, executor1);
+ systemSpaceEntity.onSpaceUpdated();
+ assertThat(executor1.hasNext()).isTrue();
+ executor1.runAll();
+
+ verify(listener1).onSpaceUpdated();
+ }
+
+ @Test
+ public void
+ setOnSpaceUpdatedListener_multipleListeners_callLastListenersOnActivitySpaceUpdated() {
+ SystemSpaceEntityImpl systemSpaceEntity = getSystemSpaceEntityImpl();
+ OnSpaceUpdatedListener listener1 = Mockito.mock(OnSpaceUpdatedListener.class);
+ OnSpaceUpdatedListener listener2 = Mockito.mock(OnSpaceUpdatedListener.class);
+ FakeScheduledExecutorService executor1 = new FakeScheduledExecutorService();
+ FakeScheduledExecutorService executor2 = new FakeScheduledExecutorService();
+
+ systemSpaceEntity.setOnSpaceUpdatedListener(listener1, executor1);
+ // This should override the previous listener.
+ systemSpaceEntity.setOnSpaceUpdatedListener(listener2, executor2);
+ systemSpaceEntity.onSpaceUpdated();
+
+ assertThat(executor1.hasNext()).isFalse();
+ assertThat(executor2.hasNext()).isTrue();
+
+ executor1.runAll();
+ executor2.runAll();
+
+ verify(listener1, Mockito.never()).onSpaceUpdated();
+ verify(listener2).onSpaceUpdated();
+ }
+
+ @Test
+ public void setOnSpaceUpdatedListener_withNullExecutor_usesInternalExecutor() {
+ SystemSpaceEntityImpl systemSpaceEntity = getSystemSpaceEntityImpl();
+ FakeScheduledExecutorService fakeExecutor = getDefaultFakeExecutor();
+ OnSpaceUpdatedListener listener = Mockito.mock(OnSpaceUpdatedListener.class);
+
+ systemSpaceEntity.setOnSpaceUpdatedListener(listener, null);
+ systemSpaceEntity.onSpaceUpdated();
+
+ assertThat(fakeExecutor.hasNext()).isTrue();
+ fakeExecutor.runAll();
+ verify(listener).onSpaceUpdated();
+ }
+
+ @Test
+ public void setOnSpaceUpdatedListener_withNullListener_noListenerCallOnActivitySpaceUpdated() {
+ SystemSpaceEntityImpl systemSpaceEntity = getSystemSpaceEntityImpl();
+ OnSpaceUpdatedListener listener = Mockito.mock(OnSpaceUpdatedListener.class);
+ FakeScheduledExecutorService executor = new FakeScheduledExecutorService();
+ systemSpaceEntity.setOnSpaceUpdatedListener(listener, executor);
+ systemSpaceEntity.setOnSpaceUpdatedListener(null, executor);
+
+ systemSpaceEntity.onSpaceUpdated();
+ executor.runAll();
+
+ verify(listener, Mockito.never()).onSpaceUpdated();
+ }
+
+ @Test
+ public void getPoseInOpenXrReferenceSpace_returnsPoseFromSubscribeToNodeTransform() {
+ SystemSpaceEntityImpl systemSpaceEntity = getSystemSpaceEntityImpl();
+ FakeNode node = (FakeNode) systemSpaceEntity.getNode();
+ Mat4f mat4f =
+ new Mat4f( // -- Column major, right handed 4x4 Transformation Matrix
+ // with
+ new float[] { // -- translation of (4, 8, 12) and rotation 90 (@)
+ // around Z axis
+ 0f, 1f, 0f, 0f, // -- cos(@), sin(@), 0, 0
+ -1f, 0f, 0f, 0f, // -- -sin(@), cos(@), 0, 0
+ 0f, 0f, 1f, 0f, // -- 0, 0, 1, 0
+ 4f, 8f, 12f, 1f, // -- tx, ty, tz, 1
+ });
+ FakeNodeTransform nodeTransformEvent = new FakeNodeTransform(mat4f);
+
+ node.sendTransformEvent(nodeTransformEvent);
+ getDefaultFakeExecutor().runAll();
+
+ Pose expectedPose =
+ new Pose(
+ new Vector3(4f, 8f, 12f),
+ Quaternion.fromAxisAngle(new Vector3(0f, 0f, 1f), 90f));
+
+ assertPose(systemSpaceEntity.getPoseInOpenXrReferenceSpace(), expectedPose);
+ }
+
+ @Test
+ public void setOnSpaceUpdatedListener_callsListenerOnNodeTransformEvent() {
+ SystemSpaceEntityImpl systemSpaceEntity = getSystemSpaceEntityImpl();
+ FakeNode node = (FakeNode) systemSpaceEntity.getNode();
+ Mat4f mat4f = new Mat4f(Matrix4.Identity.getData());
+ FakeNodeTransform nodeTransformEvent = new FakeNodeTransform(mat4f);
+
+ OnSpaceUpdatedListener listener = Mockito.mock(OnSpaceUpdatedListener.class);
+ FakeScheduledExecutorService executor = new FakeScheduledExecutorService();
+ systemSpaceEntity.setOnSpaceUpdatedListener(listener, executor);
+
+ node.sendTransformEvent(nodeTransformEvent);
+ getDefaultFakeExecutor().runAll();
+
+ assertThat(executor.hasNext()).isTrue();
+ executor.runAll();
+
+ verify(listener).onSpaceUpdated();
+ }
+
+ @Test
+ public void
+ setOnSpaceUpdatedListener_multipleListeners_callsLastListenerOnNodeTransformEvent() {
+ SystemSpaceEntityImpl systemSpaceEntity = getSystemSpaceEntityImpl();
+ FakeScheduledExecutorService fakeExecutor = getDefaultFakeExecutor();
+ FakeNode node = (FakeNode) systemSpaceEntity.getNode();
+ Mat4f mat4f = new Mat4f(Matrix4.Identity.getData());
+ FakeNodeTransform nodeTransformEvent = new FakeNodeTransform(mat4f);
+
+ OnSpaceUpdatedListener listener = Mockito.mock(OnSpaceUpdatedListener.class);
+ OnSpaceUpdatedListener listener2 = Mockito.mock(OnSpaceUpdatedListener.class);
+ FakeScheduledExecutorService executor = new FakeScheduledExecutorService();
+ systemSpaceEntity.setOnSpaceUpdatedListener(listener, executor);
+ systemSpaceEntity.setOnSpaceUpdatedListener(listener2, executor);
+
+ node.sendTransformEvent(nodeTransformEvent);
+ fakeExecutor.runAll();
+ assertThat(executor.hasNext()).isTrue();
+ executor.runAll();
+
+ verify(listener, Mockito.never()).onSpaceUpdated();
+ verify(listener2).onSpaceUpdated();
+ }
+
+ @Test
+ public void
+ setOnSpaceUpdatedListener_withNullExecutor_callsListenerOnNodeTransformEventExecutor() {
+ SystemSpaceEntityImpl systemSpaceEntity = getSystemSpaceEntityImpl();
+ FakeNode node = (FakeNode) systemSpaceEntity.getNode();
+ Mat4f mat4f = new Mat4f(Matrix4.Identity.getData());
+ FakeNodeTransform nodeTransformEvent = new FakeNodeTransform(mat4f);
+
+ OnSpaceUpdatedListener listener = Mockito.mock(OnSpaceUpdatedListener.class);
+ systemSpaceEntity.setOnSpaceUpdatedListener(listener, null);
+
+ node.sendTransformEvent(nodeTransformEvent);
+ getDefaultFakeExecutor().runAll();
+
+ verify(listener).onSpaceUpdated();
+ }
+
+ @Test
+ public void setOnSpaceUpdatedListener_withNullListener_noListenerCalledOnNodeTransformEvent() {
+ SystemSpaceEntityImpl systemSpaceEntity = getSystemSpaceEntityImpl();
+ FakeNode node = (FakeNode) systemSpaceEntity.getNode();
+ Mat4f mat4f = new Mat4f(Matrix4.Identity.getData());
+ FakeNodeTransform nodeTransformEvent = new FakeNodeTransform(mat4f);
+
+ OnSpaceUpdatedListener listener = Mockito.mock(OnSpaceUpdatedListener.class);
+ systemSpaceEntity.setOnSpaceUpdatedListener(listener, null);
+ systemSpaceEntity.setOnSpaceUpdatedListener(null, null);
+
+ node.sendTransformEvent(nodeTransformEvent);
+ getDefaultFakeExecutor().runAll();
+
+ verify(listener, Mockito.never()).onSpaceUpdated();
+ }
+
+ @Test
+ public void dispose_disposesChildren() throws Exception {
+ SystemSpaceEntityImpl systemSpaceEntity = getSystemSpaceEntityImpl();
+ AndroidXrEntity childEntity = createChildAndroidXrEntity();
+
+ systemSpaceEntity.addChild(childEntity);
+
+ // Verify the parent of the child node is the space node before disposing it.
+ FakeNode systemSpaceNode = (FakeNode) systemSpaceEntity.getNode();
+ FakeNode childNode = (FakeNode) childEntity.getNode();
+ assertThat(childNode.getParent()).isEqualTo(systemSpaceNode);
+
+ // Dispose the space entity and verify that the children were disposed.
+ systemSpaceEntity.dispose();
+
+ assertThat(childNode.getParent()).isNull();
+ }
+
+ @Test
+ public void setPoseInOpenXrReferenceSpace_callsOnSpaceUpdated() {
+ SystemSpaceEntityImpl systemSpaceEntity = getSystemSpaceEntityImpl();
+ OnSpaceUpdatedListener listener = Mockito.mock(OnSpaceUpdatedListener.class);
+ FakeScheduledExecutorService executor = new FakeScheduledExecutorService();
+
+ systemSpaceEntity.setOnSpaceUpdatedListener(listener, executor);
+ systemSpaceEntity.setOpenXrReferenceSpacePose(Matrix4.Identity);
+ executor.runAll();
+
+ verify(listener).onSpaceUpdated();
+ }
+
+ @Test
+ public void setPoseInOpenXrReferenceSpace_updatesPose() {
+ SystemSpaceEntityImpl systemSpaceEntity = getSystemSpaceEntityImpl();
+ Matrix4 matrix =
+ new Matrix4( // -- Column major, right handed 4x4 Transformation
+ // Matrix with
+ new float[] { // -- translation of (4, 8, 12) and rotation 90 (@)
+ // around Z axis
+ 0f, 1f, 0f, 0f, // -- cos(@), sin(@), 0, 0
+ -1f, 0f, 0f, 0f, // -- -sin(@), cos(@), 0, 0
+ 0f, 0f, 1f, 0f, // -- 0, 0, 1, 0
+ 4f, 8f, 12f, 1f, // -- tx, ty, tz, 1
+ });
+ Pose pose =
+ new Pose(
+ new Vector3(4f, 8f, 12f),
+ Quaternion.fromAxisAngle(new Vector3(0f, 0f, 1f), 90f));
+
+ systemSpaceEntity.setOpenXrReferenceSpacePose(matrix);
+ assertPose(systemSpaceEntity.getPoseInOpenXrReferenceSpace(), pose);
+ }
+
+ @Test
+ public void setPoseInOpenXrReferenceSpace_updatesScale() {
+ SystemSpaceEntityImpl systemSpaceEntity = getSystemSpaceEntityImpl();
+ Matrix4 matrix =
+ new Matrix4( // -- Column major, right handed 4x4 Transformation
+ // Matrix with
+ new float // -- translation of (4, 8, 12) and rotation 90 (@)
+ // around Z axis,
+ [] { // -- and scale of 3.3.
+ 0f, 3.3f, 0f, 0f, // -- cos(@), sin(@), 0, 0
+ -3.3f, 0f, 0f, 0f, // -- -sin(@), cos(@), 0, 0
+ 0f, 0f, 3.3f, 0f, // -- 0, 0, 1, 0
+ 4f, 8f, 12f, 1f, // -- tx, ty, tz, 1
+ });
+ Vector3 scale = new Vector3(3.3f, 3.3f, 3.3f);
+
+ systemSpaceEntity.setOpenXrReferenceSpacePose(matrix);
+ assertVector3(
+ systemSpaceEntity.getActivitySpaceScale(),
+ scale.div(getActivitySpaceEntity().getWorldSpaceScale()));
+ assertVector3(systemSpaceEntity.getWorldSpaceScale(), scale);
+ assertVector3(systemSpaceEntity.getScale(), scale);
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/fake_assets/FakeAsset.exr b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/fake_assets/FakeAsset.exr
new file mode 100644
index 0000000..5d16938
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/fake_assets/FakeAsset.exr
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+TestImage.
\ No newline at end of file
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/fake_assets/FakeAsset.glb b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/fake_assets/FakeAsset.glb
new file mode 100644
index 0000000..ab5693d
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/fake_assets/FakeAsset.glb
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.
+ */
+
+Test.
\ No newline at end of file
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/perception/FovTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/perception/FovTest.java
new file mode 100644
index 0000000..f9aaad2
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/perception/FovTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl.perception;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class FovTest {
+
+ @Test
+ public void createFov_returnsFov() {
+ Fov fov = new Fov(1.0f, 2.0f, 3.0f, 4.0f);
+ assertThat(fov.getAngleLeft()).isEqualTo(1.0f);
+ assertThat(fov.getAngleRight()).isEqualTo(2.0f);
+ assertThat(fov.getAngleUp()).isEqualTo(3.0f);
+ assertThat(fov.getAngleDown()).isEqualTo(4.0f);
+ }
+
+ @Test
+ public void equals_returnsTrue() {
+ Fov fov1 = new Fov(1.0f, 2.0f, 3.0f, 4.0f);
+ Fov fov2 = new Fov(1.0f, 2.0f, 3.0f, 4.0f);
+ assertThat(fov1.equals(fov2)).isTrue();
+ }
+
+ @Test
+ public void equals_returnsFalse() {
+ Fov fov1 = new Fov(1.0f, 2.0f, 3.0f, 4.0f);
+ Fov fov2 = new Fov(1.0f, 2.0f, 3.0f, 5.0f);
+ assertThat(fov1.equals(fov2)).isFalse();
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/perception/PerceptionLibraryTestHelper.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/perception/PerceptionLibraryTestHelper.java
new file mode 100644
index 0000000..ff32567
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/perception/PerceptionLibraryTestHelper.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl.perception;
+
+import android.os.IBinder;
+
+/**
+ * A library with an interface to native functions that are used to set up state for perception
+ * manager tests.
+ */
+final class PerceptionLibraryTestHelper {
+
+ public PerceptionLibraryTestHelper() {}
+
+ public native void reset();
+
+ public native void setCreateSessionResult(boolean success);
+
+ public native int getOpenXrSessionReferenceSpaceType();
+
+ public native void setGetAnchorResult(IBinder binder, long anchorId);
+
+ public native void setAnchorUuidBytes(byte[] anchorUuidBytes);
+
+ public native void setAnchorPersistState(int persistState);
+
+ public native void setGetCurrentHeadPoseResult(
+ float x, float y, float z, float qx, float qy, float qz, float qw);
+
+ public native void setLeftView(
+ float x,
+ float y,
+ float z,
+ float qx,
+ float qy,
+ float qz,
+ float qw,
+ float angleLeft,
+ float angleRight,
+ float angleUp,
+ float angleDown);
+
+ public native void setRightView(
+ float x,
+ float y,
+ float z,
+ float qx,
+ float qy,
+ float qz,
+ float qw,
+ float angleLeft,
+ float angleRight,
+ float angleUp,
+ float angleDown);
+
+ public native void addPlane(
+ long plane,
+ Pose centerPose,
+ float extentWidth,
+ float extentHeight,
+ int type,
+ int label);
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/perception/PoseTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/perception/PoseTest.java
new file mode 100644
index 0000000..700a0eb
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/perception/PoseTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl.perception;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class PoseTest {
+
+ @Test
+ public void identityPose_returnsPose() {
+ Pose pose = Pose.identity();
+
+ assertThat(pose.tx()).isEqualTo(0);
+ assertThat(pose.ty()).isEqualTo(0);
+ assertThat(pose.tz()).isEqualTo(0);
+ assertThat(pose.qx()).isEqualTo(0);
+ assertThat(pose.qy()).isEqualTo(0);
+ assertThat(pose.qz()).isEqualTo(0);
+ assertThat(pose.qw()).isEqualTo(1);
+ }
+
+ @Test
+ public void createPose_returnsPose() {
+ Pose pose = new Pose(1, 2, 3, 4, 5, 6, 7);
+
+ assertThat(pose.tx()).isEqualTo(1);
+ assertThat(pose.ty()).isEqualTo(2);
+ assertThat(pose.tz()).isEqualTo(3);
+ assertThat(pose.qx()).isEqualTo(4);
+ assertThat(pose.qy()).isEqualTo(5);
+ assertThat(pose.qz()).isEqualTo(6);
+ assertThat(pose.qw()).isEqualTo(7);
+ }
+
+ @Test
+ public void updateTranslation_updatseTranslation() {
+ Pose pose = new Pose(1, 2, 3, 4, 5, 6, 7);
+
+ pose.updateTranslation(10, 20, 30);
+
+ assertThat(pose.tx()).isEqualTo(10);
+ assertThat(pose.ty()).isEqualTo(20);
+ assertThat(pose.tz()).isEqualTo(30);
+ assertThat(pose.qx()).isEqualTo(4);
+ assertThat(pose.qy()).isEqualTo(5);
+ assertThat(pose.qz()).isEqualTo(6);
+ assertThat(pose.qw()).isEqualTo(7);
+ }
+
+ @Test
+ public void updateRotation_updatesRotation() {
+ Pose pose = new Pose(1, 2, 3, 4, 5, 6, 7);
+
+ pose.updateRotation(40, 50, 60, 70);
+
+ assertThat(pose.tx()).isEqualTo(1);
+ assertThat(pose.ty()).isEqualTo(2);
+ assertThat(pose.tz()).isEqualTo(3);
+ assertThat(pose.qx()).isEqualTo(40);
+ assertThat(pose.qy()).isEqualTo(50);
+ assertThat(pose.qz()).isEqualTo(60);
+ assertThat(pose.qw()).isEqualTo(70);
+ }
+
+ @Test
+ public void equals_returnsTrue() {
+ Pose pose = new Pose(1, 2, 3, 4, 5, 6, 7);
+ assertThat(pose).isEqualTo(new Pose(1, 2, 3, 4, 5, 6, 7));
+ }
+}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/perception/ViewProjectionTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/perception/ViewProjectionTest.java
new file mode 100644
index 0000000..7b39853
--- /dev/null
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/perception/ViewProjectionTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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 androidx.xr.scenecore.impl.perception;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class ViewProjectionTest {
+
+ @Test
+ public void createViewProjection_returnsViewProjection() {
+ ViewProjection viewProjection =
+ new ViewProjection(new Pose(1, 2, 3, 4, 5, 6, 7), new Fov(8, 9, 10, 11));
+
+ assertThat(viewProjection.getPose()).isEqualTo(new Pose(1, 2, 3, 4, 5, 6, 7));
+ assertThat(viewProjection.getFov()).isEqualTo(new Fov(8, 9, 10, 11));
+ }
+
+ @Test
+ public void equals_returnsTrue() {
+ ViewProjection viewProjection1 =
+ new ViewProjection(new Pose(1, 2, 3, 4, 5, 6, 7), new Fov(8, 9, 10, 11));
+ ViewProjection viewProjection2 =
+ new ViewProjection(new Pose(1, 2, 3, 4, 5, 6, 7), new Fov(8, 9, 10, 11));
+ assertThat(viewProjection1).isEqualTo(viewProjection2);
+ }
+
+ @Test
+ public void equals_returnsFalse_forDifferentFov() {
+ ViewProjection viewProjection1 =
+ new ViewProjection(new Pose(1, 2, 3, 4, 5, 6, 7), new Fov(8, 9, 10, 11));
+ ViewProjection viewProjection2 =
+ new ViewProjection(new Pose(1, 2, 3, 4, 5, 6, 7), new Fov(8, 9, 10, 13));
+ assertThat(viewProjection1).isNotEqualTo(viewProjection2);
+ }
+
+ @Test
+ public void equals_returnsFalse_forDifferentPose() {
+ ViewProjection viewProjection1 =
+ new ViewProjection(new Pose(1, 2, 3, 4, 5, 6, 77), new Fov(8, 9, 10, 11));
+ ViewProjection viewProjection2 =
+ new ViewProjection(new Pose(1, 2, 3, 4, 5, 6, 7), new Fov(8, 9, 10, 11));
+ assertThat(viewProjection1).isNotEqualTo(viewProjection2);
+ }
+}
diff --git a/camera/camera-effects-still-portrait/api/current.txt b/xr/xr-stubs/api/current.txt
similarity index 100%
copy from camera/camera-effects-still-portrait/api/current.txt
copy to xr/xr-stubs/api/current.txt
diff --git a/camera/camera-effects-still-portrait/api/res-current.txt b/xr/xr-stubs/api/res-current.txt
similarity index 100%
copy from camera/camera-effects-still-portrait/api/res-current.txt
copy to xr/xr-stubs/api/res-current.txt
diff --git a/xr/xr-stubs/api/restricted_current.txt b/xr/xr-stubs/api/restricted_current.txt
new file mode 100644
index 0000000..33fa1a3
--- /dev/null
+++ b/xr/xr-stubs/api/restricted_current.txt
@@ -0,0 +1,476 @@
+// Signature format: 4.0
+package com.android.extensions.xr {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class Config {
+ method public float defaultPixelsPerMeter(float);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class XrExtensionResult {
+ method public int getResult();
+ field @Deprecated public static final int XR_RESULT_ERROR_IGNORED = 3; // 0x3
+ field @Deprecated public static final int XR_RESULT_ERROR_INVALID_STATE = 2; // 0x2
+ field public static final int XR_RESULT_ERROR_NOT_ALLOWED = 3; // 0x3
+ field public static final int XR_RESULT_ERROR_SYSTEM = 4; // 0x4
+ field public static final int XR_RESULT_IGNORED_ALREADY_APPLIED = 2; // 0x2
+ field public static final int XR_RESULT_SUCCESS = 0; // 0x0
+ field public static final int XR_RESULT_SUCCESS_NOT_VISIBLE = 1; // 0x1
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class XrExtensions {
+ ctor public XrExtensions();
+ method public void addFindableView(android.view.View!, android.view.ViewGroup!);
+ method @Deprecated public void attachSpatialEnvironment(android.app.Activity!, com.android.extensions.xr.node.Node!);
+ method public void attachSpatialEnvironment(android.app.Activity!, com.android.extensions.xr.node.Node!, com.android.extensions.xr.function.Consumer<com.android.extensions.xr.XrExtensionResult!>!, java.util.concurrent.Executor!);
+ method @Deprecated public void attachSpatialScene(android.app.Activity!, com.android.extensions.xr.node.Node!, com.android.extensions.xr.node.Node!);
+ method public void attachSpatialScene(android.app.Activity!, com.android.extensions.xr.node.Node!, com.android.extensions.xr.node.Node!, com.android.extensions.xr.function.Consumer<com.android.extensions.xr.XrExtensionResult!>!, java.util.concurrent.Executor!);
+ method @Deprecated public boolean canEmbedActivityPanel(android.app.Activity!);
+ method public void clearSpatialStateCallback(android.app.Activity!);
+ method public com.android.extensions.xr.space.ActivityPanel! createActivityPanel(android.app.Activity!, com.android.extensions.xr.space.ActivityPanelLaunchParameters!);
+ method public com.android.extensions.xr.node.Node! createNode();
+ method public com.android.extensions.xr.node.NodeTransaction! createNodeTransaction();
+ method public com.android.extensions.xr.node.ReformOptions! createReformOptions(com.android.extensions.xr.function.Consumer<com.android.extensions.xr.node.ReformEvent!>!, java.util.concurrent.Executor!);
+ method public com.android.extensions.xr.splitengine.SplitEngineBridge! createSplitEngineBridge();
+ method public com.android.extensions.xr.subspace.Subspace! createSubspace(com.android.extensions.xr.splitengine.SplitEngineBridge!, int);
+ method @Deprecated public void detachSpatialEnvironment(android.app.Activity!);
+ method public void detachSpatialEnvironment(android.app.Activity!, com.android.extensions.xr.function.Consumer<com.android.extensions.xr.XrExtensionResult!>!, java.util.concurrent.Executor!);
+ method @Deprecated public void detachSpatialScene(android.app.Activity!);
+ method public void detachSpatialScene(android.app.Activity!, com.android.extensions.xr.function.Consumer<com.android.extensions.xr.XrExtensionResult!>!, java.util.concurrent.Executor!);
+ method @Deprecated public java.util.concurrent.CompletableFuture<com.android.extensions.xr.XrExtensions.SceneViewerResult!>! displayGltfModel(android.app.Activity!, com.android.extensions.xr.asset.GltfModelToken!);
+ method public int getApiVersion();
+ method @Deprecated public void getBounds(android.app.Activity!, com.android.extensions.xr.function.Consumer<com.android.extensions.xr.space.Bounds!>!, java.util.concurrent.Executor!);
+ method public com.android.extensions.xr.Config! getConfig();
+ method public int getOpenXrWorldReferenceSpaceType();
+ method @Deprecated public void getSpatialCapabilities(android.app.Activity!, com.android.extensions.xr.function.Consumer<com.android.extensions.xr.space.SpatialCapabilities!>!, java.util.concurrent.Executor!);
+ method public com.android.extensions.xr.space.SpatialState! getSpatialState(android.app.Activity!);
+ method public com.android.extensions.xr.node.Node! getSurfaceTrackingNode(android.view.View!);
+ method public com.android.extensions.xr.media.XrSpatialAudioExtensions! getXrSpatialAudioExtensions();
+ method public void hitTest(android.app.Activity!, com.android.extensions.xr.node.Vec3!, com.android.extensions.xr.node.Vec3!, com.android.extensions.xr.function.Consumer<com.android.extensions.xr.space.HitTestResult!>!, java.util.concurrent.Executor!);
+ method @Deprecated public java.util.concurrent.CompletableFuture<com.android.extensions.xr.asset.EnvironmentToken!>! loadEnvironment(java.io.InputStream!, int, int, String!);
+ method @Deprecated public java.util.concurrent.CompletableFuture<com.android.extensions.xr.asset.EnvironmentToken!>! loadEnvironment(java.io.InputStream!, int, int, String!, int, int);
+ method @Deprecated public java.util.concurrent.CompletableFuture<com.android.extensions.xr.asset.GltfModelToken!>! loadGltfModel(java.io.InputStream!, int, int, String!);
+ method @Deprecated public java.util.concurrent.CompletableFuture<com.android.extensions.xr.asset.SceneToken!>! loadImpressScene(java.io.InputStream!, int, int);
+ method public void removeFindableView(android.view.View!, android.view.ViewGroup!);
+ method @Deprecated public boolean requestFullSpaceMode(android.app.Activity!);
+ method public void requestFullSpaceMode(android.app.Activity!, boolean, com.android.extensions.xr.function.Consumer<com.android.extensions.xr.XrExtensionResult!>!, java.util.concurrent.Executor!);
+ method @Deprecated public boolean requestHomeSpaceMode(android.app.Activity!);
+ method public android.os.Bundle! setFullSpaceStartMode(android.os.Bundle!);
+ method public android.os.Bundle! setFullSpaceStartModeWithEnvironmentInherited(android.os.Bundle!);
+ method @Deprecated public android.os.Bundle! setMainPanelCurvatureRadius(android.os.Bundle!, float);
+ method @Deprecated public void setMainWindowCurvatureRadius(android.app.Activity!, float);
+ method @Deprecated public void setMainWindowSize(android.app.Activity!, int, int);
+ method public void setMainWindowSize(android.app.Activity!, int, int, com.android.extensions.xr.function.Consumer<com.android.extensions.xr.XrExtensionResult!>!, java.util.concurrent.Executor!);
+ method @Deprecated public void setPreferredAspectRatio(android.app.Activity!, float);
+ method public void setPreferredAspectRatio(android.app.Activity!, float, com.android.extensions.xr.function.Consumer<com.android.extensions.xr.XrExtensionResult!>!, java.util.concurrent.Executor!);
+ method public void setSpatialStateCallback(android.app.Activity!, com.android.extensions.xr.function.Consumer<com.android.extensions.xr.space.SpatialState!>!, java.util.concurrent.Executor!);
+ method @Deprecated public void setSpatialStateCallbackDeprecated(android.app.Activity!, com.android.extensions.xr.function.Consumer<com.android.extensions.xr.space.SpatialStateEvent!>!, java.util.concurrent.Executor!);
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class XrExtensions.SceneViewerResult {
+ ctor @Deprecated public XrExtensions.SceneViewerResult();
+ }
+
+}
+
+package com.android.extensions.xr.asset {
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface AssetToken {
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface EnvironmentToken extends com.android.extensions.xr.asset.AssetToken {
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface GltfAnimation {
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public enum GltfAnimation.State {
+ enum_constant @Deprecated public static final com.android.extensions.xr.asset.GltfAnimation.State LOOP;
+ enum_constant @Deprecated public static final com.android.extensions.xr.asset.GltfAnimation.State PLAY;
+ enum_constant @Deprecated public static final com.android.extensions.xr.asset.GltfAnimation.State STOP;
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface GltfModelToken extends com.android.extensions.xr.asset.AssetToken {
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SceneToken extends com.android.extensions.xr.asset.AssetToken {
+ }
+
+}
+
+package com.android.extensions.xr.environment {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class EnvironmentVisibilityState {
+ method public int getCurrentState();
+ field public static final int APP_VISIBLE = 2; // 0x2
+ field public static final int HOME_VISIBLE = 1; // 0x1
+ field public static final int INVISIBLE = 0; // 0x0
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class PassthroughVisibilityState {
+ method public int getCurrentState();
+ method public float getOpacity();
+ field public static final int APP = 2; // 0x2
+ field public static final int DISABLED = 0; // 0x0
+ field public static final int HOME = 1; // 0x1
+ field public static final int SYSTEM = 3; // 0x3
+ }
+
+}
+
+package com.android.extensions.xr.function {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.FunctionalInterface public interface Consumer<T> {
+ method public void accept(T!);
+ }
+
+}
+
+package com.android.extensions.xr.media {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class AudioManagerExtensions {
+ method public void playSoundEffectAsPointSource(android.media.AudioManager!, int, com.android.extensions.xr.media.PointSourceAttributes!);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class AudioTrackExtensions {
+ method public com.android.extensions.xr.media.PointSourceAttributes! getPointSourceAttributes(android.media.AudioTrack!);
+ method public com.android.extensions.xr.media.SoundFieldAttributes! getSoundFieldAttributes(android.media.AudioTrack!);
+ method public int getSpatialSourceType(android.media.AudioTrack!);
+ method public android.media.AudioTrack.Builder! setPointSourceAttributes(android.media.AudioTrack.Builder!, com.android.extensions.xr.media.PointSourceAttributes!);
+ method public android.media.AudioTrack.Builder! setSoundFieldAttributes(android.media.AudioTrack.Builder!, com.android.extensions.xr.media.SoundFieldAttributes!);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class MediaPlayerExtensions {
+ method public android.media.MediaPlayer! setPointSourceAttributes(android.media.MediaPlayer!, com.android.extensions.xr.media.PointSourceAttributes!);
+ method public android.media.MediaPlayer! setSoundFieldAttributes(android.media.MediaPlayer!, com.android.extensions.xr.media.SoundFieldAttributes!);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class PointSourceAttributes {
+ method public com.android.extensions.xr.node.Node! getNode();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final class PointSourceAttributes.Builder {
+ ctor public PointSourceAttributes.Builder();
+ method public com.android.extensions.xr.media.PointSourceAttributes! build() throws java.lang.UnsupportedOperationException;
+ method public com.android.extensions.xr.media.PointSourceAttributes.Builder! setNode(com.android.extensions.xr.node.Node!);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SoundFieldAttributes {
+ method public int getAmbisonicsOrder();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final class SoundFieldAttributes.Builder {
+ ctor public SoundFieldAttributes.Builder();
+ method public com.android.extensions.xr.media.SoundFieldAttributes! build() throws java.lang.UnsupportedOperationException;
+ method public com.android.extensions.xr.media.SoundFieldAttributes.Builder! setAmbisonicsOrder(int);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SoundPoolExtensions {
+ method public int getSpatialSourceType(android.media.SoundPool!, int);
+ method public int playAsPointSource(android.media.SoundPool!, int, com.android.extensions.xr.media.PointSourceAttributes!, float, int, int, float);
+ method public int playAsSoundField(android.media.SoundPool!, int, com.android.extensions.xr.media.SoundFieldAttributes!, float, int, int, float);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SpatializerExtensions {
+ field public static final int AMBISONICS_ORDER_FIRST_ORDER = 0; // 0x0
+ field public static final int AMBISONICS_ORDER_SECOND_ORDER = 1; // 0x1
+ field public static final int AMBISONICS_ORDER_THIRD_ORDER = 2; // 0x2
+ field public static final int SOURCE_TYPE_BYPASS = 0; // 0x0
+ field public static final int SOURCE_TYPE_POINT_SOURCE = 1; // 0x1
+ field public static final int SOURCE_TYPE_SOUND_FIELD = 2; // 0x2
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class XrSpatialAudioExtensions {
+ method public com.android.extensions.xr.media.AudioManagerExtensions! getAudioManagerExtensions();
+ method public com.android.extensions.xr.media.AudioTrackExtensions! getAudioTrackExtensions();
+ method public com.android.extensions.xr.media.MediaPlayerExtensions! getMediaPlayerExtensions();
+ method public com.android.extensions.xr.media.SoundPoolExtensions! getSoundPoolExtensions();
+ }
+
+}
+
+package com.android.extensions.xr.node {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class InputEvent {
+ method public int getAction();
+ method public com.android.extensions.xr.node.Vec3! getDirection();
+ method public int getDispatchFlags();
+ method public com.android.extensions.xr.node.InputEvent.HitInfo! getHitInfo();
+ method public com.android.extensions.xr.node.Vec3! getOrigin();
+ method public int getPointerType();
+ method public com.android.extensions.xr.node.InputEvent.HitInfo! getSecondaryHitInfo();
+ method public int getSource();
+ method public long getTimestamp();
+ field public static final int ACTION_CANCEL = 3; // 0x3
+ field public static final int ACTION_DOWN = 0; // 0x0
+ field public static final int ACTION_HOVER_ENTER = 5; // 0x5
+ field public static final int ACTION_HOVER_EXIT = 6; // 0x6
+ field public static final int ACTION_HOVER_MOVE = 4; // 0x4
+ field public static final int ACTION_MOVE = 2; // 0x2
+ field public static final int ACTION_UP = 1; // 0x1
+ field public static final int DISPATCH_FLAG_2D = 2; // 0x2
+ field public static final int DISPATCH_FLAG_CAPTURED_POINTER = 1; // 0x1
+ field public static final int DISPATCH_FLAG_NONE = 0; // 0x0
+ field public static final int POINTER_TYPE_DEFAULT = 0; // 0x0
+ field public static final int POINTER_TYPE_LEFT = 1; // 0x1
+ field public static final int POINTER_TYPE_RIGHT = 2; // 0x2
+ field public static final int SOURCE_CONTROLLER = 2; // 0x2
+ field public static final int SOURCE_GAZE_AND_GESTURE = 5; // 0x5
+ field public static final int SOURCE_HANDS = 3; // 0x3
+ field public static final int SOURCE_HEAD = 1; // 0x1
+ field public static final int SOURCE_MOUSE = 4; // 0x4
+ field public static final int SOURCE_UNKNOWN = 0; // 0x0
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static class InputEvent.HitInfo {
+ ctor public InputEvent.HitInfo(int, com.android.extensions.xr.node.Node!, com.android.extensions.xr.node.Mat4f!, com.android.extensions.xr.node.Vec3!);
+ method public com.android.extensions.xr.node.Vec3! getHitPosition();
+ method public com.android.extensions.xr.node.Node! getInputNode();
+ method public int getSubspaceImpressNodeId();
+ method public com.android.extensions.xr.node.Mat4f! getTransform();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class Mat4f {
+ ctor public Mat4f(float[]!);
+ method public float get(int, int);
+ method public float[]! getFlattenedMatrix();
+ method public void set(int, int, float);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class Node implements android.os.Parcelable {
+ method public int describeContents();
+ method public void listenForInput(com.android.extensions.xr.function.Consumer<com.android.extensions.xr.node.InputEvent!>!, java.util.concurrent.Executor!);
+ method public void requestPointerCapture(com.android.extensions.xr.function.Consumer<java.lang.Integer!>!, java.util.concurrent.Executor!);
+ method public void setNonPointerFocusTarget(android.view.AttachedSurfaceControl!);
+ method public void stopListeningForInput();
+ method public void stopPointerCapture();
+ method public java.io.Closeable! subscribeToTransform(com.android.extensions.xr.function.Consumer<com.android.extensions.xr.node.NodeTransform!>!, java.util.concurrent.Executor!);
+ method public void writeToParcel(android.os.Parcel!, int);
+ field public static final int POINTER_CAPTURE_STATE_ACTIVE = 1; // 0x1
+ field public static final int POINTER_CAPTURE_STATE_PAUSED = 0; // 0x0
+ field public static final int POINTER_CAPTURE_STATE_STOPPED = 2; // 0x2
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class NodeTransaction implements java.io.Closeable {
+ method public void apply();
+ method public void close();
+ method public com.android.extensions.xr.node.NodeTransaction! disableReform(com.android.extensions.xr.node.Node!);
+ method public com.android.extensions.xr.node.NodeTransaction! enableReform(com.android.extensions.xr.node.Node!, com.android.extensions.xr.node.ReformOptions!);
+ method public com.android.extensions.xr.node.NodeTransaction! merge(com.android.extensions.xr.node.NodeTransaction!);
+ method public com.android.extensions.xr.node.NodeTransaction! removeCornerRadius(com.android.extensions.xr.node.Node!);
+ method public com.android.extensions.xr.node.NodeTransaction! setAlpha(com.android.extensions.xr.node.Node!, float);
+ method public com.android.extensions.xr.node.NodeTransaction! setAnchorId(com.android.extensions.xr.node.Node!, android.os.IBinder!);
+ method public com.android.extensions.xr.node.NodeTransaction! setCornerRadius(com.android.extensions.xr.node.Node!, float);
+ method @Deprecated public com.android.extensions.xr.node.NodeTransaction! setCurvature(com.android.extensions.xr.node.Node!, float);
+ method @Deprecated public com.android.extensions.xr.node.NodeTransaction! setEnvironment(com.android.extensions.xr.node.Node!, com.android.extensions.xr.asset.EnvironmentToken!);
+ method @Deprecated public com.android.extensions.xr.node.NodeTransaction! setGltfAnimation(com.android.extensions.xr.node.Node!, String!, com.android.extensions.xr.asset.GltfAnimation.State!);
+ method @Deprecated public com.android.extensions.xr.node.NodeTransaction! setGltfModel(com.android.extensions.xr.node.Node!, com.android.extensions.xr.asset.GltfModelToken!);
+ method @Deprecated public com.android.extensions.xr.node.NodeTransaction! setImpressScene(com.android.extensions.xr.node.Node!, com.android.extensions.xr.asset.SceneToken!);
+ method public com.android.extensions.xr.node.NodeTransaction! setName(com.android.extensions.xr.node.Node!, String!);
+ method public com.android.extensions.xr.node.NodeTransaction! setOrientation(com.android.extensions.xr.node.Node!, float, float, float, float);
+ method public com.android.extensions.xr.node.NodeTransaction! setParent(com.android.extensions.xr.node.Node!, com.android.extensions.xr.node.Node!);
+ method public com.android.extensions.xr.node.NodeTransaction! setPassthroughState(com.android.extensions.xr.node.Node!, float, int);
+ method public com.android.extensions.xr.node.NodeTransaction! setPixelPositioning(com.android.extensions.xr.node.Node!, int);
+ method public com.android.extensions.xr.node.NodeTransaction! setPixelResolution(com.android.extensions.xr.node.Node!, float);
+ method public com.android.extensions.xr.node.NodeTransaction! setPosition(com.android.extensions.xr.node.Node!, float, float, float);
+ method public com.android.extensions.xr.node.NodeTransaction! setReformSize(com.android.extensions.xr.node.Node!, com.android.extensions.xr.node.Vec3!);
+ method public com.android.extensions.xr.node.NodeTransaction! setScale(com.android.extensions.xr.node.Node!, float, float, float);
+ method public com.android.extensions.xr.node.NodeTransaction! setSubspace(com.android.extensions.xr.node.Node!, com.android.extensions.xr.subspace.Subspace!);
+ method public com.android.extensions.xr.node.NodeTransaction! setSurfaceControl(com.android.extensions.xr.node.Node!, android.view.SurfaceControl!);
+ method public com.android.extensions.xr.node.NodeTransaction! setSurfacePackage(com.android.extensions.xr.node.Node!, android.view.SurfaceControlViewHost.SurfacePackage!);
+ method public com.android.extensions.xr.node.NodeTransaction! setVisibility(com.android.extensions.xr.node.Node!, boolean);
+ method public com.android.extensions.xr.node.NodeTransaction! setWindowBounds(android.view.SurfaceControl!, int, int);
+ method public com.android.extensions.xr.node.NodeTransaction! setWindowBounds(android.view.SurfaceControlViewHost.SurfacePackage!, int, int);
+ field public static final int POSITION_FROM_PARENT_TOP_LEFT = 64; // 0x40
+ field public static final int X_POSITION_IN_PIXELS = 1; // 0x1
+ field public static final int Y_POSITION_IN_PIXELS = 2; // 0x2
+ field public static final int Z_POSITION_IN_PIXELS = 4; // 0x4
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class NodeTransform {
+ method public long getTimestamp();
+ method public com.android.extensions.xr.node.Mat4f! getTransform();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class Quatf {
+ ctor public Quatf(float, float, float, float);
+ field public final float w;
+ field public final float x;
+ field public final float y;
+ field public final float z;
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class ReformEvent {
+ method public com.android.extensions.xr.node.Vec3! getCurrentRayDirection();
+ method public com.android.extensions.xr.node.Vec3! getCurrentRayOrigin();
+ method public int getId();
+ method public com.android.extensions.xr.node.Vec3! getInitialRayDirection();
+ method public com.android.extensions.xr.node.Vec3! getInitialRayOrigin();
+ method public com.android.extensions.xr.node.Quatf! getProposedOrientation();
+ method public com.android.extensions.xr.node.Vec3! getProposedPosition();
+ method public com.android.extensions.xr.node.Vec3! getProposedScale();
+ method public com.android.extensions.xr.node.Vec3! getProposedSize();
+ method public int getState();
+ method public int getType();
+ field public static final int REFORM_STATE_END = 3; // 0x3
+ field public static final int REFORM_STATE_ONGOING = 2; // 0x2
+ field public static final int REFORM_STATE_START = 1; // 0x1
+ field public static final int REFORM_STATE_UNKNOWN = 0; // 0x0
+ field public static final int REFORM_TYPE_MOVE = 1; // 0x1
+ field public static final int REFORM_TYPE_RESIZE = 2; // 0x2
+ field public static final int REFORM_TYPE_UNKNOWN = 0; // 0x0
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class ReformOptions {
+ method public com.android.extensions.xr.node.Vec3! getCurrentSize();
+ method public int getEnabledReform();
+ method public com.android.extensions.xr.function.Consumer<com.android.extensions.xr.node.ReformEvent!>! getEventCallback();
+ method public java.util.concurrent.Executor! getEventExecutor();
+ method public float getFixedAspectRatio();
+ method public int getFlags();
+ method public boolean getForceShowResizeOverlay();
+ method public com.android.extensions.xr.node.Vec3! getMaximumSize();
+ method public com.android.extensions.xr.node.Vec3! getMinimumSize();
+ method public int getScaleWithDistanceMode();
+ method public com.android.extensions.xr.node.ReformOptions! setCurrentSize(com.android.extensions.xr.node.Vec3!);
+ method public com.android.extensions.xr.node.ReformOptions! setEnabledReform(int);
+ method public com.android.extensions.xr.node.ReformOptions! setEventCallback(com.android.extensions.xr.function.Consumer<com.android.extensions.xr.node.ReformEvent!>!);
+ method public com.android.extensions.xr.node.ReformOptions! setEventExecutor(java.util.concurrent.Executor!);
+ method public com.android.extensions.xr.node.ReformOptions! setFixedAspectRatio(float);
+ method public com.android.extensions.xr.node.ReformOptions! setFlags(int);
+ method public com.android.extensions.xr.node.ReformOptions! setForceShowResizeOverlay(boolean);
+ method public com.android.extensions.xr.node.ReformOptions! setMaximumSize(com.android.extensions.xr.node.Vec3!);
+ method public com.android.extensions.xr.node.ReformOptions! setMinimumSize(com.android.extensions.xr.node.Vec3!);
+ method public com.android.extensions.xr.node.ReformOptions! setScaleWithDistanceMode(int);
+ field public static final int ALLOW_MOVE = 1; // 0x1
+ field public static final int ALLOW_RESIZE = 2; // 0x2
+ field public static final int FLAG_ALLOW_SYSTEM_MOVEMENT = 2; // 0x2
+ field public static final int FLAG_POSE_RELATIVE_TO_PARENT = 4; // 0x4
+ field public static final int FLAG_SCALE_WITH_DISTANCE = 1; // 0x1
+ field public static final int SCALE_WITH_DISTANCE_MODE_DEFAULT = 3; // 0x3
+ field public static final int SCALE_WITH_DISTANCE_MODE_DMM = 2; // 0x2
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class Vec3 {
+ ctor public Vec3(float, float, float);
+ field public final float x;
+ field public final float y;
+ field public final float z;
+ }
+
+}
+
+package com.android.extensions.xr.passthrough {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class PassthroughState {
+ field public static final int PASSTHROUGH_MODE_MAX = 1; // 0x1
+ field public static final int PASSTHROUGH_MODE_MIN = 2; // 0x2
+ field public static final int PASSTHROUGH_MODE_OFF = 0; // 0x0
+ }
+
+}
+
+package com.android.extensions.xr.space {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class ActivityPanel {
+ method public void delete();
+ method public com.android.extensions.xr.node.Node! getNode();
+ method public void launchActivity(android.content.Intent!, android.os.Bundle!);
+ method public void moveActivity(android.app.Activity!);
+ method public void setWindowBounds(android.graphics.Rect!);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class ActivityPanelLaunchParameters {
+ ctor public ActivityPanelLaunchParameters(android.graphics.Rect!);
+ method public android.graphics.Rect! getWindowBounds();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class Bounds {
+ ctor public Bounds(float, float, float);
+ method public float getDepth();
+ method public float getHeight();
+ method public float getWidth();
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class BoundsChangeEvent implements com.android.extensions.xr.space.SpatialStateEvent {
+ ctor @Deprecated public BoundsChangeEvent(com.android.extensions.xr.space.Bounds!);
+ method @Deprecated public com.android.extensions.xr.space.Bounds! getBounds();
+ method @Deprecated public float getDepth();
+ method @Deprecated public float getHeight();
+ method @Deprecated public float getWidth();
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class EnvironmentControlChangeEvent implements com.android.extensions.xr.space.SpatialStateEvent {
+ ctor @Deprecated public EnvironmentControlChangeEvent(boolean);
+ method @Deprecated public boolean getEnvironmentControlAllowed();
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class EnvironmentVisibilityChangeEvent implements com.android.extensions.xr.space.SpatialStateEvent {
+ ctor @Deprecated public EnvironmentVisibilityChangeEvent(int);
+ method @Deprecated public int getEnvironmentState();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class HitTestResult {
+ method public float getDistance();
+ method public com.android.extensions.xr.node.Vec3! getHitPosition();
+ method public com.android.extensions.xr.node.Vec3! getSurfaceNormal();
+ method public int getSurfaceType();
+ method public boolean getVirtualEnvironmentIsVisible();
+ field public static final int SURFACE_3D_OBJECT = 2; // 0x2
+ field public static final int SURFACE_PANEL = 1; // 0x1
+ field public static final int SURFACE_UNKNOWN = 0; // 0x0
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final class HitTestResult.Builder {
+ ctor public HitTestResult.Builder(float, com.android.extensions.xr.node.Vec3!, boolean, int);
+ method public com.android.extensions.xr.space.HitTestResult! build();
+ method public com.android.extensions.xr.space.HitTestResult.Builder! setSurfaceNormal(com.android.extensions.xr.node.Vec3!);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SpatialCapabilities {
+ ctor public SpatialCapabilities();
+ method public boolean get(int);
+ field public static final int APP_ENVIRONMENTS_CAPABLE = 3; // 0x3
+ field public static final int PASSTHROUGH_CONTROL_CAPABLE = 2; // 0x2
+ field public static final int SPATIAL_3D_CONTENTS_CAPABLE = 1; // 0x1
+ field public static final int SPATIAL_ACTIVITY_EMBEDDING_CAPABLE = 5; // 0x5
+ field public static final int SPATIAL_AUDIO_CAPABLE = 4; // 0x4
+ field public static final int SPATIAL_UI_CAPABLE = 0; // 0x0
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SpatialCapabilityChangeEvent implements com.android.extensions.xr.space.SpatialStateEvent {
+ ctor @Deprecated public SpatialCapabilityChangeEvent(com.android.extensions.xr.space.SpatialCapabilities!);
+ method @Deprecated public com.android.extensions.xr.space.SpatialCapabilities! getCurrentCapabilities();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SpatialState {
+ method public com.android.extensions.xr.space.Bounds! getBounds();
+ method public com.android.extensions.xr.environment.EnvironmentVisibilityState! getEnvironmentVisibility();
+ method public android.util.Size! getMainWindowSize();
+ method public com.android.extensions.xr.environment.PassthroughVisibilityState! getPassthroughVisibility();
+ method public float getPreferredAspectRatio();
+ method public com.android.extensions.xr.space.SpatialCapabilities! getSpatialCapabilities();
+ method public boolean isActiveEnvironmentNode(com.android.extensions.xr.node.Node!);
+ method public boolean isActiveSceneNode(com.android.extensions.xr.node.Node!);
+ method public boolean isActiveWindowLeashNode(com.android.extensions.xr.node.Node!);
+ method public boolean isEnvironmentInherited();
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SpatialStateEvent {
+ }
+
+}
+
+package com.android.extensions.xr.splitengine {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SplitEngineBridge {
+ method public long getNativeHandle();
+ }
+
+}
+
+package com.android.extensions.xr.subspace {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class Subspace {
+ }
+
+}
+
diff --git a/xr/xr-stubs/build.gradle b/xr/xr-stubs/build.gradle
new file mode 100644
index 0000000..5300f9a
--- /dev/null
+++ b/xr/xr-stubs/build.gradle
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 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
+ *
+ * http://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.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+
+import androidx.build.KotlinTarget
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+}
+
+dependencies {
+ implementation("androidx.annotation:annotation:1.8.1")
+}
+
+android {
+ namespace = "com.android.extensions.xr"
+}
+
+androidx {
+ name = "Android XR Extensions Stub"
+ type = LibraryType.PUBLISHED_LIBRARY
+ inceptionYear = "2024"
+ description = "Stub implementation of the Android XR Extensions."
+ // TODO: b/379715750 - Remove this flag once the deprecated methods have been removed from the API.
+ failOnDeprecationWarnings = false
+ kotlinTarget = KotlinTarget.KOTLIN_1_9
+ // TODO: b/326456246
+ optOutJSpecify = true
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/Config.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/Config.java
new file mode 100644
index 0000000..c3a767e
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/Config.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr;
+
+import androidx.annotation.RestrictTo;
+
+/** XR configuration information. */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class Config {
+
+ Config() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Returns the default pixelsPerMeter value for 2D surfaces. See
+ * NodeTransaction.setPixelResolution() for the meaning of pixelsPerMeter.
+ *
+ * @param density The logical density of the display.
+ * @return The default pixelsPerMeter value.
+ */
+ public float defaultPixelsPerMeter(float density) {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/XrExtensionResult.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/XrExtensionResult.java
new file mode 100644
index 0000000..a34f008
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/XrExtensionResult.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr;
+
+import androidx.annotation.RestrictTo;
+
+/** Represents a result of an asynchronous XR Extension call. */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class XrExtensionResult {
+
+ XrExtensionResult() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Returns the result. */
+ public int getResult() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * @deprecated Renamed. Use XR_RESULT_ERROR_NOT_ALLOWED.
+ */
+ @Deprecated public static final int XR_RESULT_ERROR_IGNORED = 3; // 0x3
+
+ /**
+ * @deprecated Renamed. Use XR_RESULT_IGNORED_ALREADY_APPLIED.
+ */
+ @Deprecated public static final int XR_RESULT_ERROR_INVALID_STATE = 2; // 0x2
+
+ /**
+ * The asynchronous call has been rejected by the system service because the caller activity
+ * does not have the required capability.
+ */
+ public static final int XR_RESULT_ERROR_NOT_ALLOWED = 3; // 0x3
+
+ /**
+ * The asynchronous call cannot be sent to the system service, or the service cannot properly
+ * handle the request. This is not a recoverable error for the client. For example, this error
+ * is sent to the client when an asynchronous call attempt has failed with a RemoteException.
+ */
+ public static final int XR_RESULT_ERROR_SYSTEM = 4; // 0x4
+
+ /**
+ * The asynchronous call has been ignored by the system service because the caller activity is
+ * already in the requested state.
+ */
+ public static final int XR_RESULT_IGNORED_ALREADY_APPLIED = 2; // 0x2
+
+ /**
+ * The asynchronous call has been accepted by the system service, and an immediate state change
+ * is expected.
+ */
+ public static final int XR_RESULT_SUCCESS = 0; // 0x0
+
+ /**
+ * The asynchronous call has been accepted by the system service, but the caller activity's
+ * spatial state won't be changed until other condition(s) are met.
+ */
+ public static final int XR_RESULT_SUCCESS_NOT_VISIBLE = 1; // 0x1
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/XrExtensions.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/XrExtensions.java
new file mode 100644
index 0000000..24e5929
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/XrExtensions.java
@@ -0,0 +1,931 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr;
+
+import android.content.Intent;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * The main extensions class that creates or provides instances of various XR Extensions components.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class XrExtensions {
+
+ public XrExtensions() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Get the current version of the {@link com.android.extensions.xr.XrExtensions XrExtensions}
+ * API.
+ */
+ public int getApiVersion() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Synchronously creates a node that can host a 2D panel or 3D subspace.
+ *
+ * @return A {@link com.android.extensions.xr.node.Node Node}.
+ */
+ public com.android.extensions.xr.node.Node createNode() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Synchronously creates a new transaction that can be used to update multiple {@link
+ * com.android.extensions.xr.node.Node Node}'s data and transformation in the 3D space.
+ *
+ * @return A {@link com.android.extensions.xr.node.NodeTransaction NodeTransaction} that can be
+ * used to queue the updates and submit to backend at once.
+ */
+ public com.android.extensions.xr.node.NodeTransaction createNodeTransaction() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Synchronously creates a subspace.
+ *
+ * @param splitEngineBridge The splitEngineBridge.
+ * @param subspaceId The unique identifier of the subspace.
+ * @return A {@link com.android.extensions.xr.subspace.Subspace Subspace} that can be used to
+ * render 3D content in.
+ */
+ public com.android.extensions.xr.subspace.Subspace createSubspace(
+ com.android.extensions.xr.splitengine.SplitEngineBridge splitEngineBridge,
+ int subspaceId) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Loads and caches the glTF model in the SpaceFlinger.
+ *
+ * @param asset The input stream data of the glTF model.
+ * @param regionSizeBytes The size of the memory region where the model is stored (in bytes).
+ * @param regionOffsetBytes The offset from the beginning of the memory region (in bytes).
+ * @param url The URL of the asset to be loaded. This string is only used for caching purposes.
+ * @return A {@link java.util.concurrent.CompletableFuture CompletableFuture} that either
+ * contains the {@link com.android.extensions.xr.asset.GltfModelToken GltfModelToken}
+ * representing the loaded model or 'null' if the asset could not be loaded successfully.
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ public java.util.concurrent.CompletableFuture<com.android.extensions.xr.asset.GltfModelToken>
+ loadGltfModel(
+ java.io.InputStream asset,
+ int regionSizeBytes,
+ int regionOffsetBytes,
+ java.lang.String url) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Views a 3D asset.
+ *
+ * @param activity The activity which relinquishes control in order to display the model..
+ * @param gltfModel The model to display.
+ * @return A {@link java.util.concurrent.CompletableFuture CompletableFuture} that notifies the
+ * caller when the session has completed.
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ public java.util.concurrent.CompletableFuture<
+ com.android.extensions.xr.XrExtensions.SceneViewerResult>
+ displayGltfModel(
+ android.app.Activity activity,
+ com.android.extensions.xr.asset.GltfModelToken gltfModel) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Loads and caches the environment in the SpaceFlinger.
+ *
+ * @param asset The input stream data of the EXR or JPEG environment.
+ * @param regionSizeBytes The size of the memory region where the environment is stored (in
+ * bytes).
+ * @param regionOffsetBytes The offset from the beginning of the memory region (in bytes).
+ * @param url The URL of the asset to be loaded. This string is only used for caching purposes.
+ * @return A {@link java.util.concurrent.CompletableFuture CompletableFuture} that either
+ * contains the {@link com.android.extensions.xr.asset.EnvironmentToken EnvironmentToken}
+ * representing the loaded environment or 'null' if the asset could not be loaded
+ * successfully.
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ public java.util.concurrent.CompletableFuture<com.android.extensions.xr.asset.EnvironmentToken>
+ loadEnvironment(
+ java.io.InputStream asset,
+ int regionSizeBytes,
+ int regionOffsetBytes,
+ java.lang.String url) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Loads and caches the environment in the SpaceFlinger.
+ *
+ * @param asset The input stream data of the EXR or JPEG environment.
+ * @param regionSizeBytes The size of the memory region where the environment is stored (in
+ * bytes).
+ * @param regionOffsetBytes The offset from the beginning of the memory region (in bytes).
+ * @param url The URL of the asset to be loaded.
+ * @param textureWidth The target width of the final texture which will be downsampled/upsampled
+ * from the original image.
+ * @param textureHeight The target height of the final texture which will be
+ * downsampled/upsampled from the original image.
+ * @return A {@link java.util.concurrent.CompletableFuture CompletableFuture} that either
+ * contains the {@link com.android.extensions.xr.asset.EnvironmentToken EnvironmentToken}
+ * representing the loaded environment or 'null' if the asset could not be loaded
+ * successfully.
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ public java.util.concurrent.CompletableFuture<com.android.extensions.xr.asset.EnvironmentToken>
+ loadEnvironment(
+ java.io.InputStream asset,
+ int regionSizeBytes,
+ int regionOffsetBytes,
+ java.lang.String url,
+ int textureWidth,
+ int textureHeight) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Loads and caches the Impress scene in the SpaceFlinger.
+ *
+ * @param asset The input stream data of the textproto Impress scene.
+ * @param regionSizeBytes The size of the memory region where the Impress scene is stored (in
+ * bytes).
+ * @param regionOffsetBytes The offset from the beginning of the memory region (in bytes).
+ * @return A {@link java.util.concurrent.CompletableFuture CompletableFuture} that either
+ * contains the {@link com.android.extensions.xr.asset.SceneToken SceneToken} representing
+ * the loaded Impress scene or 'null' if the asset could not be loaded successfully.
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ public java.util.concurrent.CompletableFuture<com.android.extensions.xr.asset.SceneToken>
+ loadImpressScene(
+ java.io.InputStream asset, int regionSizeBytes, int regionOffsetBytes) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Synchronously returns a {@link com.android.extensions.xr.splitengine.SplitEngineBridge
+ * SplitEngineBridge}.
+ *
+ * @return A {@link com.android.extensions.xr.splitengine.SplitEngineBridge SplitEngineBridge}.
+ */
+ public com.android.extensions.xr.splitengine.SplitEngineBridge createSplitEngineBridge() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Synchronously returns the implementation of the {@link
+ * com.android.extensions.xr.media.XrSpatialAudioExtensions XrSpatialAudioExtensions} component.
+ *
+ * @return The {@link com.android.extensions.xr.media.XrSpatialAudioExtensions
+ * XrSpatialAudioExtensions}.
+ */
+ public com.android.extensions.xr.media.XrSpatialAudioExtensions getXrSpatialAudioExtensions() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Attaches the given {@code sceneNode} as the presentation for the given {@code activity} in
+ * the space, and asks the system to attach the 2D content of the {@code activity} into the
+ * given {@code windowNode}.
+ *
+ * <p>The {@code sceneNode} will only be visible if the {@code activity} is visible as in a
+ * lifecycle state between {@link android.app.Activity#onStart() Activity#onStart()} and {@link
+ * android.app.Activity#onStop() Activity#onStop()} and is SPATIAL_UI_CAPABLE too.
+ *
+ * <p>One activity can only attach one scene node. When a new scene node is attached for the
+ * same {@code activity}, the previous one will be detached.
+ *
+ * @param activity the owner activity of the {@code sceneNode}.
+ * @param sceneNode the node to show as the presentation of the {@code activity}.
+ * @param windowNode a leash node to allow the app to control the position and size of the
+ * activity's main window.
+ * @deprecated Use the new interface with a callback.
+ */
+ @Deprecated
+ public void attachSpatialScene(
+ android.app.Activity activity,
+ com.android.extensions.xr.node.Node sceneNode,
+ com.android.extensions.xr.node.Node windowNode) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Attaches the given {@code sceneNode} as the presentation for the given {@code activity} in
+ * the space, and asks the system to attach the 2D content of the {@code activity} into the
+ * given {@code windowNode}.
+ *
+ * <p>The {@code sceneNode} will only be visible if the {@code activity} is visible as in a
+ * lifecycle state between {@link android.app.Activity#onStart() Activity#onStart()} and {@link
+ * android.app.Activity#onStop() Activity#onStop()} and is SPATIAL_UI_CAPABLE too.
+ *
+ * <p>One activity can only attach one scene node. When a new scene node is attached for the
+ * same {@code activity}, the previous one will be detached.
+ *
+ * @param activity the owner activity of the {@code sceneNode}.
+ * @param sceneNode the node to show as the presentation of the {@code activity}.
+ * @param windowNode a leash node to allow the app to control the position and size of the
+ * activity's main window.
+ * @param callback the callback that will be called with the result. XrExtensionResult will
+ * indicate either of the following: XrExtensionResult.XR_RESULT_SUCCESS: The request has
+ * been accepted, and the client can expect that a spatial state callback with an updated
+ * SpatialState will run shortly. XrExtensionResult.XR_RESULT_SUCCESS_NOT_VISIBLE: The
+ * request has been accepted, but will not immediately change the spatial state. A spatial
+ * state callback with an updated SpatialState won't run until the activity becomes
+ * SPATIAL_UI_CAPABLE. XrExtensionResult.XR_RESULT_IGNORED_ALREADY_APPLIED: The request has
+ * been ignored because the activity is already in the requested state.
+ * XrExtensionResult.XR_RESULT_ERROR_NOT_ALLOWED: The request has been rejected because the
+ * activity does not have the required capability (e.g. called by an embedded guest
+ * activity.) XrExtensionResult.XR_RESULT_ERROR_SYSTEM: A unrecoverable service side error
+ * has happened.
+ * @param executor the executor the callback will be called on.
+ */
+ public void attachSpatialScene(
+ android.app.Activity activity,
+ com.android.extensions.xr.node.Node sceneNode,
+ com.android.extensions.xr.node.Node windowNode,
+ com.android.extensions.xr.function.Consumer<com.android.extensions.xr.XrExtensionResult>
+ callback,
+ java.util.concurrent.Executor executor) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Detaches the {@code sceneNode} that was previously attached for the {@code activity} via
+ * {@link #attachSpatialScene}.
+ *
+ * <p>When an {@link android.app.Activity Activity} is destroyed, it must call this method to
+ * detach the scene node that was attached for itself.
+ *
+ * @param activity the owner activity of the {@code sceneNode}.
+ * @deprecated Use the new interface with a callback.
+ */
+ @Deprecated
+ public void detachSpatialScene(android.app.Activity activity) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Detaches the {@code sceneNode} that was previously attached for the {@code activity} via
+ * {@link #attachSpatialScene}.
+ *
+ * <p>When an {@link android.app.Activity Activity} is destroyed, it must call this method to
+ * detach the scene node that was attached for itself.
+ *
+ * @param activity the owner activity of the {@code sceneNode}.
+ * @param callback the callback that will be called with the result. XrExtensionResult will
+ * indicate either of the following: XrExtensionResult.XR_RESULT_SUCCESS: The request has
+ * been accepted, and the client can expect that a spatial state callback with an updated
+ * SpatialState will run shortly. XrExtensionResult.XR_RESULT_SUCCESS_NOT_VISIBLE: The
+ * request has been accepted, but will not immediately change the spatial state. A spatial
+ * state callback with an updated SpatialState won't run until the activity becomes
+ * SPATIAL_UI_CAPABLE. XrExtensionResult.XR_RESULT_IGNORED_ALREADY_APPLIED: The request has
+ * been ignored because the activity is already in the requested state.
+ * XrExtensionResult.XR_RESULT_ERROR_SYSTEM: A unrecoverable service side error has
+ * happened.
+ * @param executor the executor the callback will be called on.
+ */
+ public void detachSpatialScene(
+ android.app.Activity activity,
+ com.android.extensions.xr.function.Consumer<com.android.extensions.xr.XrExtensionResult>
+ callback,
+ java.util.concurrent.Executor executor) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Resizes the main window of the given activity to the requested size.
+ *
+ * @param activity the activity whose main window should be resized.
+ * @param width the new main window width in pixels.
+ * @param height the new main window height in pixels.
+ * @deprecated Use the new interface with a callback.
+ */
+ @Deprecated
+ public void setMainWindowSize(android.app.Activity activity, int width, int height) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Resizes the main window of the given activity to the requested size.
+ *
+ * @param activity the activity whose main window should be resized.
+ * @param width the new main window width in pixels.
+ * @param height the new main window height in pixels.
+ * @param callback the callback that will be called with the result. XrExtensionResult will
+ * indicate either of the following: XrExtensionResult.XR_RESULT_SUCCESS: The request has
+ * been accepted, and the client can expect that a spatial state callback with an updated
+ * SpatialState will run shortly. XrExtensionResult.XR_RESULT_SUCCESS_NOT_VISIBLE: The
+ * request has been accepted, but will not immediately change the spatial state. A spatial
+ * state callback with an updated SpatialState won't run until the activity becomes
+ * SPATIAL_UI_CAPABLE. XrExtensionResult.XR_RESULT_IGNORED_ALREADY_APPLIED: The request has
+ * been ignored because the activity is already in the requested state.
+ * XrExtensionResult.XR_RESULT_ERROR_NOT_ALLOWED: The request has been rejected because the
+ * activity does not have the required capability (e.g. called by an embedded guest
+ * activity.) XrExtensionResult.XR_RESULT_ERROR_SYSTEM: A unrecoverable service side error
+ * has happened.
+ * @param executor the executor the callback will be called on.
+ */
+ public void setMainWindowSize(
+ android.app.Activity activity,
+ int width,
+ int height,
+ com.android.extensions.xr.function.Consumer<com.android.extensions.xr.XrExtensionResult>
+ callback,
+ java.util.concurrent.Executor executor) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Sets the main window of the given activity to the curvature radius. Note that it's allowed
+ * only for the activity in full space mode.
+ *
+ * @param activity the activity of the main window to which the curvature should be applied.
+ * @param curvatureRadius the panel curvature radius. It is measured in "radius * 1 /
+ * curvature". A value of 0.0f means that the panel will be flat.
+ * @deprecated Use Split Engine to create a curved panel.
+ */
+ @Deprecated
+ public void setMainWindowCurvatureRadius(android.app.Activity activity, float curvatureRadius) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Attaches an environment node for a given activity to make it visible.
+ *
+ * <p>SysUI will attach the environment node to the task node when the activity gains the
+ * APP_ENVIRONMENTS_CAPABLE capability.
+ *
+ * <p>This method can be called multiple times, SysUI will attach the new environment node and
+ * detach the old environment node if it exists.
+ *
+ * @param activity the activity that provides the environment node to attach.
+ * @param environmentNode the environment node provided by the activity to be attached.
+ * @deprecated Use the new interface with a callback.
+ */
+ @Deprecated
+ public void attachSpatialEnvironment(
+ android.app.Activity activity, com.android.extensions.xr.node.Node environmentNode) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Attaches an environment node for a given activity to make it visible.
+ *
+ * <p>SysUI will attach the environment node to the task node when the activity gains the
+ * APP_ENVIRONMENTS_CAPABLE capability.
+ *
+ * <p>This method can be called multiple times, SysUI will attach the new environment node and
+ * detach the old environment node if it exists.
+ *
+ * @param activity the activity that provides the environment node to attach.
+ * @param environmentNode the environment node provided by the activity to be attached.
+ * @param callback the callback that will be called with the result. XrExtensionResult will
+ * indicate either of the following: XrExtensionResult.XR_RESULT_SUCCESS: The request has
+ * been accepted, and the client can expect that a spatial state callback with an updated
+ * SpatialState will run shortly. XrExtensionResult.XR_RESULT_SUCCESS_NOT_VISIBLE: The
+ * request has been accepted, but will not immediately change the spatial state. A spatial
+ * state callback with an updated SpatialState won't run until the activity becomes
+ * APP_ENVIRONMENTS_CAPABLE. XrExtensionResult.XR_RESULT_IGNORED_ALREADY_APPLIED: The
+ * request has been ignored because the activity is already in the requested state.
+ * XrExtensionResult.XR_RESULT_ERROR_NOT_ALLOWED: The request has been rejected because the
+ * activity does not have the required capability (e.g. called by an embedded guest
+ * activity.) XrExtensionResult.XR_RESULT_ERROR_SYSTEM: A unrecoverable service side error
+ * has happened.
+ * @param executor the executor the callback will be called on.
+ */
+ public void attachSpatialEnvironment(
+ android.app.Activity activity,
+ com.android.extensions.xr.node.Node environmentNode,
+ com.android.extensions.xr.function.Consumer<com.android.extensions.xr.XrExtensionResult>
+ callback,
+ java.util.concurrent.Executor executor) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Detaches the environment node and its sub tree for a given activity to make it invisible.
+ *
+ * <p>This method will detach and cleanup the environment node and its subtree passed from the
+ * activity.
+ *
+ * @param activity the activity with which SysUI will detach and clean up the environment node
+ * tree.
+ * @deprecated Use the new interface with a callback.
+ */
+ @Deprecated
+ public void detachSpatialEnvironment(android.app.Activity activity) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Detaches the environment node and its sub tree for a given activity to make it invisible.
+ *
+ * <p>This method will detach and cleanup the environment node and its subtree passed from the
+ * activity.
+ *
+ * @param activity the activity with which SysUI will detach and clean up the environment node
+ * tree.
+ * @param callback the callback that will be called with the result. XrExtensionResult will
+ * indicate either of the following: XrExtensionResult.XR_RESULT_SUCCESS: The request has
+ * been accepted, and the client can expect that a spatial state callback with an updated
+ * SpatialState will run shortly. XrExtensionResult.XR_RESULT_SUCCESS_NOT_VISIBLE: The
+ * request has been accepted, but will not immediately change the spatial state. A spatial
+ * state callback with an updated SpatialState won't run until the activity becomes
+ * APP_ENVIRONMENTS_CAPABLE. XrExtensionResult.XR_RESULT_IGNORED_ALREADY_APPLIED: The
+ * request has been ignored because the activity is already in the requested state.
+ * XrExtensionResult.XR_RESULT_ERROR_SYSTEM: A unrecoverable service side error has
+ * happened.
+ * @param executor the executor the callback will be called on.
+ */
+ public void detachSpatialEnvironment(
+ android.app.Activity activity,
+ com.android.extensions.xr.function.Consumer<com.android.extensions.xr.XrExtensionResult>
+ callback,
+ java.util.concurrent.Executor executor) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Sets a callback to receive {@link com.android.extensions.xr.space.SpatialStateEvent
+ * SpatialStateEvent} for the given {@code activity}.
+ *
+ * <p>One activity can only set one callback. When a new callback is set for the same {@code
+ * activity}, the previous one will be cleared.
+ *
+ * <p>The callback will be triggered immediately with the current state when it is set, for each
+ * of the possible events.
+ *
+ * @param activity the activity for the {@code callback} to listen to.
+ * @param callback the callback to set.
+ * @param executor the executor that the callback will be called on.
+ * @see #clearSpatialStateCallback
+ * @deprecated Use registerSpatialStateCallback instead.
+ */
+ @Deprecated
+ public void setSpatialStateCallbackDeprecated(
+ android.app.Activity activity,
+ com.android.extensions.xr.function.Consumer<
+ com.android.extensions.xr.space.SpatialStateEvent>
+ callback,
+ java.util.concurrent.Executor executor) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Synchronously registers a callback to receive {@link android.view.xr.SpatialState
+ * SpatialState} for the {@code activity}.
+ *
+ * <p>One activity can only set one callback. When a new callback is set for the same {@code
+ * activity}, the previous one will be cleared.
+ *
+ * <p>The {@code executor}'s execute() method will soon be called to run the callback with the
+ * current state when it is available, but it never happens directly from within this call.
+ *
+ * <p>This API throws IllegalArgumentException if it is called by an embedded (guest) activity.
+ *
+ * @param activity the activity for the {@code callback} to listen to.
+ * @param callback the callback to set.
+ * @param executor the executor that the callback will be called on.
+ * @see #clearSpatialStateCallback
+ */
+ public void setSpatialStateCallback(
+ android.app.Activity activity,
+ com.android.extensions.xr.function.Consumer<
+ com.android.extensions.xr.space.SpatialState>
+ callback,
+ java.util.concurrent.Executor executor) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Synchronously clears the {@link com.android.extensions.xr.space.SpatialStateEvent
+ * SpatialStateEvent} callback that was previously set to the {@code activity} via {@link
+ * #setSpatialStateCallback}.
+ *
+ * <p>When an {@link android.app.Activity Activity} is destroyed, it must call this method to
+ * clear the callback that was set for itself.
+ *
+ * @param activity the activity for the {@code callback} to listen to.
+ */
+ public void clearSpatialStateCallback(android.app.Activity activity) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Synchronously creates an {@link com.android.extensions.xr.space.ActivityPanel ActivityPanel}
+ * to be embedded inside the given {@code host} activity.
+ *
+ * <p>Caller must make sure the {@code host} can embed {@link
+ * com.android.extensions.xr.space.ActivityPanel ActivityPanel}. See {@link getSpatialState}.
+ * When embedding is possible, SpatialState's {@link
+ * com.android.extensions.xr.space.SpatialCapabilities SpatialCapabilities} has {@code
+ * SPATIAL_ACTIVITY_EMBEDDING_CAPABLE}.
+ *
+ * <p>For the {@link com.android.extensions.xr.space.ActivityPanel ActivityPanel} to be shown in
+ * the scene, caller needs to attach the {@link
+ * com.android.extensions.xr.space.ActivityPanel#getNode() ActivityPanel#getNode()} to the scene
+ * node attached through {@link #attachSpatialScene}.
+ *
+ * <p>This API throws IllegalArgumentException if it is called by an embedded (guest) activity.
+ *
+ * @param host the host activity to embed the {@link
+ * com.android.extensions.xr.space.ActivityPanel ActivityPanel}.
+ * @param launchParameters the parameters to define the initial state of the {@link
+ * com.android.extensions.xr.space.ActivityPanel ActivityPanel}.
+ * @return the {@link com.android.extensions.xr.space.ActivityPanel ActivityPanel} created.
+ * @throws java.lang.IllegalStateException if the {@code host} is not allowed to embed {@link
+ * com.android.extensions.xr.space.ActivityPanel ActivityPanel}.
+ */
+ public com.android.extensions.xr.space.ActivityPanel createActivityPanel(
+ android.app.Activity host,
+ com.android.extensions.xr.space.ActivityPanelLaunchParameters launchParameters) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Synchronously checks if an activity can be the host to embed an {@link
+ * com.android.extensions.xr.space.ActivityPanel ActivityPanel}.
+ *
+ * <p>Activity inside an {@link com.android.extensions.xr.space.ActivityPanel ActivityPanel}
+ * cannot be the host.
+ *
+ * @param activity the activity to check.
+ * @see #createActivityPanel
+ * @return true if the embedding is allowed.
+ * @deprecated Use {@link getSpatialState} instead. When embedding is possible, SpatialState's
+ * {@link com.android.extensions.xr.space.SpatialCapabilities SpatialCapabilities} has
+ * {@code SPATIAL_ACTIVITY_EMBEDDING_CAPABLE}.
+ */
+ @Deprecated
+ public boolean canEmbedActivityPanel(android.app.Activity activity) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Requests to put an activity in full space mode when it has focus.
+ *
+ * @param activity the activity that requires to enter full space mode.
+ * @return true when the request was sent (when the activity has focus).
+ * @deprecated Use requestFullSpaceMode with 3 arguments.
+ */
+ @Deprecated
+ public boolean requestFullSpaceMode(android.app.Activity activity) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Requests to put an activity in home space mode when it has focus.
+ *
+ * @param activity the activity that requires to enter home space mode.
+ * @return true when the request was sent (when the activity has focus).
+ * @deprecated Use requestFullSpaceMode with 3 arguments.
+ */
+ @Deprecated
+ public boolean requestHomeSpaceMode(android.app.Activity activity) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Requests to put an activity in a different mode when it has focus.
+ *
+ * @param activity the activity that requires to enter full space mode.
+ * @param requestEnter when true, activity is put in full space mode. Home space mode otherwise.
+ * @param callback the callback that will be called with the result. XrExtensionResult will
+ * indicate either of the following: XrExtensionResult.XR_RESULT_SUCCESS: The request has
+ * been accepted, and the client can expect that a spatial state callback with an updated
+ * SpatialState will run shortly. XrExtensionResult.XR_RESULT_IGNORED_ALREADY_APPLIED: The
+ * request has been ignored because the activity is already in the requested mode.
+ * XrExtensionResult.XR_RESULT_ERROR_NOT_ALLOWED: The request has been rejected because the
+ * activity does not have the required capability (e.g. not the top activity in a top task
+ * in the desktop, called by an embedded guest activity.)
+ * XrExtensionResult.XR_RESULT_ERROR_SYSTEM: A unrecoverable service side error has
+ * happened.
+ * @param executor the executor the callback will be called on.
+ */
+ public void requestFullSpaceMode(
+ android.app.Activity activity,
+ boolean requestEnter,
+ com.android.extensions.xr.function.Consumer<com.android.extensions.xr.XrExtensionResult>
+ callback,
+ java.util.concurrent.Executor executor) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Synchronously sets the full space mode flag to the given {@link android.os.Bundle Bundle}.
+ *
+ * <p>The {@link android.os.Bundle Bundle} then could be used to launch an {@link
+ * android.app.Activity Activity} with requesting to enter full space mode through {@link
+ * android.app.Activity#startActivity Activity#startActivity}. If there's a bundle used for
+ * customizing how the {@link android.app.Activity Activity} should be started by {@link
+ * ActivityOptions.toBundle} or {@link androidx.core.app.ActivityOptionsCompat.toBundle}, it's
+ * suggested to use the bundle to call this method.
+ *
+ * <p>The flag will be ignored when no {@link Intent.FLAG_ACTIVITY_NEW_TASK} is set in the
+ * bundle, or it is not started from a focused Activity context.
+ *
+ * @param bundle the input bundle to set with the full space mode flag.
+ * @return the input {@code bundle} with the full space mode flag set.
+ */
+ public android.os.Bundle setFullSpaceStartMode(android.os.Bundle bundle) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Synchronously sets the inherit full space mode environvment flag to the given {@link
+ * android.os.Bundle Bundle}.
+ *
+ * <p>The {@link android.os.Bundle Bundle} then could be used to launch an {@link
+ * android.app.Activity Activity} with requesting to enter full space mode while inherit the
+ * existing environment through {@link android.app.Activity#startActivity
+ * Activity#startActivity}. If there's a bundle used for customizing how the {@link
+ * android.app.Activity Activity} should be started by {@link ActivityOptions.toBundle} or
+ * {@link androidx.core.app.ActivityOptionsCompat.toBundle}, it's suggested to use the bundle to
+ * call this method.
+ *
+ * <p>When launched, the activity will be in full space mode and also inherits the environment
+ * from the launching activity. If the inherited environment needs to be animated, the launching
+ * activity has to continue updating the environment even after the activity is put into the
+ * stopped state.
+ *
+ * <p>The flag will be ignored when no {@link Intent.FLAG_ACTIVITY_NEW_TASK} is set in the
+ * intent, or it is not started from a focused Activity context.
+ *
+ * <p>The flag will also be ignored when there is no environment to inherit or the activity has
+ * its own environment set already.
+ *
+ * <p>For security reasons, Z testing for the new activity is disabled, and the activity is
+ * always drawn on top of the inherited environment. Because Z testing is disabled, the activity
+ * should not spatialize itself, and should not curve its panel too much either.
+ *
+ * @param bundle the input bundle to set with the inherit full space mode environment flag.
+ * @return the input {@code bundle} with the inherit full space mode flag set.
+ */
+ public android.os.Bundle setFullSpaceStartModeWithEnvironmentInherited(
+ android.os.Bundle bundle) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Sets panel curvature radius to the given {@link android.os.Bundle Bundle}.
+ *
+ * <p>The {@link android.os.Bundle Bundle} then could be used to launch an {@link
+ * android.app.Activity Activity} with requesting to a custom curvature radius for the main
+ * panel through {@link android.app.Activity#startActivity Activity#startActivity}. If there's a
+ * bundle used for customizing how the {@link android.app.Activity Activity} should be started
+ * by {@link ActivityOptions.toBundle} or {@link
+ * androidx.core.app.ActivityOptionsCompat.toBundle}, it's suggested to use the bundle to call
+ * this method.
+ *
+ * <p>The curvature radius must be used together with {@link
+ * #setFullSpaceModeWithEnvironmentInherited(android.os.Bundle)}. Otherwise, it will be ignored.
+ *
+ * @param bundle the input bundle to set with the inherit full space mode environment flag.
+ * @param panelCurvatureRadius the panel curvature radius. It is measured in "radius * 1 /
+ * curvature". A value of 0.0f means the panel is flat.
+ * @return the input {@code bundle} with the inherit full space mode flag set.
+ * @deprecated Use Split Engine to create a curved panel.
+ */
+ @Deprecated
+ public android.os.Bundle setMainPanelCurvatureRadius(
+ android.os.Bundle bundle, float panelCurvatureRadius) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Synchronously returns system config information.
+ *
+ * @return A {@link com.android.extensions.xr.Config Config} object.
+ */
+ public com.android.extensions.xr.Config getConfig() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Hit-tests a ray against the virtual scene. If the ray hits an object in the scene,
+ * information about the hit will be passed to the callback. If nothing is hit, the hit distance
+ * will be infinite. Note that attachSpatialScene() must be called before calling this method.
+ * Otherwise, an IllegalArgumentException is thrown.
+ *
+ * @param activity the requesting activity.
+ * @param origin the origin of the ray to test, in the activity's task coordinates.
+ * @param direction the direction of the ray to test, in the activity's task coordinates.
+ * @param callback the callback that will be called with the hit test result.
+ * @param executor the executor the callback will be called on.
+ */
+ public void hitTest(
+ android.app.Activity activity,
+ com.android.extensions.xr.node.Vec3 origin,
+ com.android.extensions.xr.node.Vec3 direction,
+ com.android.extensions.xr.function.Consumer<
+ com.android.extensions.xr.space.HitTestResult>
+ callback,
+ java.util.concurrent.Executor executor) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Synchronously returns the OpenXR reference space type.
+ *
+ * @return the OpenXR reference space type used as world space for the shared scene.
+ * @see <a href="https://registry.khronos.org/OpenXR/specs/1.1/html/xrspec.html#spaces-reference-spaces">
+ * OpenXR specs</a>
+ */
+ public int getOpenXrWorldReferenceSpaceType() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Synchronously creates a new ReformOptions instance.
+ *
+ * @param callback the callback that will be called with reform events.
+ * @param executor the executor the callback will be called on.
+ * @return the new builder instance.
+ */
+ public com.android.extensions.xr.node.ReformOptions createReformOptions(
+ com.android.extensions.xr.function.Consumer<com.android.extensions.xr.node.ReformEvent>
+ callback,
+ java.util.concurrent.Executor executor) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Synchronously makes a View findable via findViewById().
+ *
+ * <p>This is done without it being a child of the given group.
+ *
+ * @param view the view to add as findable.
+ * @param group a group that is part of the hierarchy that findViewById() will be called on.
+ */
+ public void addFindableView(android.view.View view, android.view.ViewGroup group) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Synchronously removes a findable view from the given group.
+ *
+ * @param view the view to remove as findable.
+ * @param group the group to remove the findable view from.
+ */
+ public void removeFindableView(android.view.View view, android.view.ViewGroup group) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Returns the surface tracking node for a view, if there is one.
+ *
+ * <p>The surface tracking node is centered on the Surface that the view is attached to, and is
+ * sized to match the surface's size. Note that the view's position in the surface can be
+ * retrieved via View.getLocationInSurface().
+ *
+ * @param view the view.
+ * @return the surface tracking node, or null if no such node exists.
+ */
+ public com.android.extensions.xr.node.Node getSurfaceTrackingNode(android.view.View view) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Sets a preferred main panel aspect ratio for home space mode.
+ *
+ * <p>The ratio is only applied to the activity. If the activity launches another activity in
+ * the same task, the ratio is not applied to the new activity. Also, while the activity is in
+ * full space mode, the preference is temporarily removed.
+ *
+ * @param activity the activity to set the preference.
+ * @param preferredRatio the aspect ratio determined by taking the panel's width over its
+ * height. A value <= 0.0f means there are no preferences.
+ * @deprecated Use the new interface with a callback.
+ */
+ @Deprecated
+ public void setPreferredAspectRatio(android.app.Activity activity, float preferredRatio) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Sets a preferred main panel aspect ratio for an activity that is not SPATIAL_UI_CAPABLE.
+ *
+ * <p>The ratio is only applied to the activity. If the activity launches another activity in
+ * the same task, the ratio is not applied to the new activity. Also, while the activity is
+ * SPATIAL_UI_CAPABLE, the preference is temporarily removed. While the activity is
+ * SPATIAL_UI_CAPABLE, use ReformOptions API instead.
+ *
+ * @param activity the activity to set the preference.
+ * @param preferredRatio the aspect ratio determined by taking the panel's width over its
+ * height. A value <= 0.0f means there are no preferences.
+ * @param callback the callback that will be called with the result. XrExtensionResult will
+ * indicate either of the following: XrExtensionResult.XR_RESULT_SUCCESS: The request has
+ * been accepted, and the client can expect that a spatial state callback with an updated
+ * SpatialState will run shortly. XrExtensionResult.XR_RESULT_SUCCESS_NOT_VISIBLE: The
+ * request has been accepted, but will not immediately change the spatial state. A spatial
+ * state callback with an updated SpatialState won't run until the activity loses the
+ * SPATIAL_UI_CAPABLE capability. XrExtensionResult.XR_RESULT_IGNORED_ALREADY_APPLIED: The
+ * request has been ignored because the activity is already in the requested state.
+ * XrExtensionResult.XR_RESULT_ERROR_NOT_ALLOWED: The request has been rejected because the
+ * activity does not have the required capability (e.g. called by an embedded guest
+ * activity.) XrExtensionResult.XR_RESULT_ERROR_SYSTEM: A unrecoverable service side error
+ * has happened.
+ * @param executor the executor the callback will be called on.
+ */
+ public void setPreferredAspectRatio(
+ android.app.Activity activity,
+ float preferredRatio,
+ com.android.extensions.xr.function.Consumer<com.android.extensions.xr.XrExtensionResult>
+ callback,
+ java.util.concurrent.Executor executor) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Gets the spatial capabilities of the activity.
+ *
+ * @param activity the activity to get the capabilities.
+ * @param callback the callback to run. If the activity is not found in SysUI, the callback runs
+ * with a null SpatialCapabilities.
+ * @param executor the executor that the callback will be called on.
+ * @deprecated Use getSpatialState synchronous getter.
+ */
+ @Deprecated
+ public void getSpatialCapabilities(
+ android.app.Activity activity,
+ com.android.extensions.xr.function.Consumer<
+ com.android.extensions.xr.space.SpatialCapabilities>
+ callback,
+ java.util.concurrent.Executor executor) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Gets the bounds of the activity.
+ *
+ * @param activity the activity to get the bounds.
+ * @param callback the callback to run. If the activity is not found in SysUI, the callback runs
+ * with a null Bounds.
+ * @param executor the executor that the callback will be called on.
+ * @deprecated Use getSpatialState synchronous getter.
+ */
+ @Deprecated
+ public void getBounds(
+ android.app.Activity activity,
+ com.android.extensions.xr.function.Consumer<com.android.extensions.xr.space.Bounds>
+ callback,
+ java.util.concurrent.Executor executor) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Synchronously gets the spatial state of the activity.
+ *
+ * <p>Do not call the API from the Binder thread. That may cause a deadlock.
+ *
+ * <p>This API throws IllegalArgumentException if it is called by an embedded (guest) activity,
+ * and also throws RuntimeException if the calling thread is interrupted.
+ *
+ * @param activity the activity to get the capabilities.
+ * @return the state of the activity.
+ */
+ public com.android.extensions.xr.space.SpatialState getSpatialState(
+ android.app.Activity activity) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * The result of a displayGltfModel request.
+ *
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @SuppressWarnings({"unchecked", "deprecation", "all"})
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @Deprecated
+ public class SceneViewerResult {
+
+ @Deprecated
+ public SceneViewerResult() {
+ throw new RuntimeException("Stub!");
+ }
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/asset/AssetToken.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/asset/AssetToken.java
new file mode 100644
index 0000000..d45c181
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/asset/AssetToken.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.asset;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Token of a spatial asset cached in the SpaceFlinger.
+ *
+ * <p>A Node can reference an {@link com.android.extensions.xr.asset.AssetToken AssetToken} such
+ * that the SpaceFlinger will render the asset inside it.
+ *
+ * <p>Note that the app needs to keep track of the {@link com.android.extensions.xr.asset.AssetToken
+ * AssetToken} so that it can continue using it, and eventually, free it when it is no longer
+ * needed.
+ *
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@Deprecated
+public interface AssetToken {}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/asset/EnvironmentToken.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/asset/EnvironmentToken.java
new file mode 100644
index 0000000..e289a67
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/asset/EnvironmentToken.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.asset;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Token of an environment cached in the SpaceFlinger.
+ *
+ * <p>A Node can reference an {@link com.android.extensions.xr.asset.EnvironmentToken
+ * EnvironmentToken} such that the SpaceFlinger will render the asset inside it.
+ *
+ * <p>Note that the app needs to keep track of the {@link
+ * com.android.extensions.xr.asset.EnvironmentToken EnvironmentToken} so that it can continue using
+ * it, and eventually, free it when it is no longer needed.
+ *
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@Deprecated
+public interface EnvironmentToken extends com.android.extensions.xr.asset.AssetToken {}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/asset/GltfAnimation.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/asset/GltfAnimation.java
new file mode 100644
index 0000000..238487f
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/asset/GltfAnimation.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.asset;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Animation configuration to be played on a glTF model.
+ *
+ * @deprecated SceneCore doesn't need this anymore as it does the same with Split Engine.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@Deprecated
+public interface GltfAnimation {
+ /**
+ * State of a glTF animation.
+ *
+ * @deprecated No longer needed by SceneCore.
+ */
+ @SuppressWarnings({"unchecked", "deprecation", "all"})
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @Deprecated
+ enum State {
+ /**
+ * Will stop the glTF animation that is currently playing or looping.
+ *
+ * @deprecated No longer needed by SceneCore.
+ */
+ @Deprecated
+ STOP,
+ /**
+ * Will restart the glTF animation if it's currently playing, looping, or is stopped.
+ *
+ * @deprecated No longer needed by SceneCore.
+ */
+ @Deprecated
+ PLAY,
+ /**
+ * Will restart and loop the glTF animation if it's currently playing, looping, or is
+ * stopped.
+ *
+ * @deprecated No longer needed by SceneCore.
+ */
+ @Deprecated
+ LOOP;
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/asset/GltfModelToken.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/asset/GltfModelToken.java
new file mode 100644
index 0000000..0b8f80e
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/asset/GltfModelToken.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.asset;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Token of a glTF model cached in the SpaceFlinger.
+ *
+ * <p>A Node can reference an {@link com.android.extensions.xr.asset.GltfModelToken GltfModelToken}
+ * such that the SpaceFlinger will render the asset inside it.
+ *
+ * <p>Note that the app needs to keep track of the {@link
+ * com.android.extensions.xr.asset.GltfModelToken GltfModelToken} so that it can continue using it,
+ * and eventually, free it when it is no longer needed.
+ *
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@Deprecated
+public interface GltfModelToken extends com.android.extensions.xr.asset.AssetToken {}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/asset/SceneToken.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/asset/SceneToken.java
new file mode 100644
index 0000000..c424750
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/asset/SceneToken.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.asset;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Token of a scene cached in the SpaceFlinger.
+ *
+ * <p>A Node can reference an {@link com.android.extensions.xr.asset.SceneToken SceneToken} such
+ * that the SpaceFlinger will render the asset inside it.
+ *
+ * <p>Note that the app needs to keep track of the {@link com.android.extensions.xr.asset.SceneToken
+ * SceneToken} so that it can continue using it, and eventually, free it when it is no longer
+ * needed.
+ *
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@Deprecated
+public interface SceneToken extends com.android.extensions.xr.asset.AssetToken {}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/environment/EnvironmentVisibilityState.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/environment/EnvironmentVisibilityState.java
new file mode 100644
index 0000000..cc42a07
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/environment/EnvironmentVisibilityState.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.environment;
+
+import androidx.annotation.RestrictTo;
+
+/** Visibility states of an environment. */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class EnvironmentVisibilityState {
+
+ EnvironmentVisibilityState() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Returns the current environment visibility state */
+ public int getCurrentState() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** equals() */
+ public boolean equals(java.lang.Object other) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** hashCode() */
+ public int hashCode() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** toString() */
+ public java.lang.String toString() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** App environment is shown. Passthrough might be on but its opacity is less than 100%. */
+ public static final int APP_VISIBLE = 2; // 0x2
+
+ /** Home environment is shown. Passthrough might be on but its opacity is less than 100%. */
+ public static final int HOME_VISIBLE = 1; // 0x1
+
+ /**
+ * No environment is shown. This could mean Passthrough is on with 100% opacity or the
+ * home environment app has crashed.
+ */
+ public static final int INVISIBLE = 0; // 0x0
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/environment/PassthroughVisibilityState.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/environment/PassthroughVisibilityState.java
new file mode 100644
index 0000000..aa48496
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/environment/PassthroughVisibilityState.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.environment;
+
+import androidx.annotation.RestrictTo;
+
+/** Visibility states of passthrough. */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class PassthroughVisibilityState {
+
+ PassthroughVisibilityState() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Returns the current passthrough visibility state */
+ public int getCurrentState() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Returns the current passthrough opacity */
+ public float getOpacity() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** equals() */
+ public boolean equals(java.lang.Object other) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** hashCode() */
+ public int hashCode() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** toString() */
+ public java.lang.String toString() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** App set Passthrough is shown with greater than 0 opacity. */
+ public static final int APP = 2; // 0x2
+
+ /** Passthrough is not shown. */
+ public static final int DISABLED = 0; // 0x0
+
+ /** Home environment Passthrough is shown with greater than 0 opacity. */
+ public static final int HOME = 1; // 0x1
+
+ /** System set Passthrough is shown with greater than 0 opacity. */
+ public static final int SYSTEM = 3; // 0x3
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/function/Consumer.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/function/Consumer.java
new file mode 100644
index 0000000..56a4e5442
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/function/Consumer.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.function;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Represents a function that accepts an argument and produces no result. It is used internally to
+ * avoid using Java 8 functional interface that leads to desugaring and Proguard shrinking.
+ *
+ * @param <T> the type of the input of the function
+ * @see java.util.function.Consumer
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
[email protected]
+public interface Consumer<T> {
+
+ /**
+ * Performs the operation on the given argument
+ *
+ * @param t the input argument
+ */
+ void accept(T t);
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/AudioManagerExtensions.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/AudioManagerExtensions.java
new file mode 100644
index 0000000..f980590
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/AudioManagerExtensions.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.media;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Provides spatial audio extensions on the framework {@link
+ * com.android.extensions.xr.media.AudioManagerExtensions AudioManagerExtensions} class.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class AudioManagerExtensions {
+
+ AudioManagerExtensions() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Play a spatialized sound effect for sound sources that will be rendered in 3D space as a
+ * point source.
+ *
+ * @param audioManager The {@link android.media.AudioManager AudioManager} to use to play the
+ * sound effect.
+ * @param effectType The type of sound effect.
+ * @param attributes attributes to specify sound source in 3D. {@link
+ * com.android.extensions.xr.media.PointSourceAttributes PointSourceAttributes}.
+ */
+ public void playSoundEffectAsPointSource(
+ android.media.AudioManager audioManager,
+ int effectType,
+ com.android.extensions.xr.media.PointSourceAttributes attributes) {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/AudioTrackExtensions.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/AudioTrackExtensions.java
new file mode 100644
index 0000000..b8e6aaa
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/AudioTrackExtensions.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.media;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Provides spatial audio extensions on the framework {@link android.media.AudioTrack AudioTrack}
+ * class.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class AudioTrackExtensions {
+
+ AudioTrackExtensions() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Gets the {@link com.android.extensions.xr.media.PointSourceAttributes PointSourceAttributes}
+ * of the provided {@link android.media.AudioTrack AudioTrack}.
+ *
+ * @param track The {@link android.media.AudioTrack AudioTrack} from which to get the {@link
+ * com.android.extensions.xr.media.PointSourceAttributes PointSourceAttributes}.
+ * @return The {@link com.android.extensions.xr.media.PointSourceAttributes
+ * PointSourceAttributes} of the provided track, null if not set.
+ */
+ public com.android.extensions.xr.media.PointSourceAttributes getPointSourceAttributes(
+ android.media.AudioTrack track) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Gets the {@link com.android.extensions.xr.media.SoundFieldAttributes SoundFieldAttributes} of
+ * the provided {@link android.media.AudioTrack AudioTrack}.
+ *
+ * @param track The {@link android.media.AudioTrack AudioTrack} from which to get the {@link
+ * com.android.extensions.xr.media.SoundFieldAttributes SoundFieldAttributes}.
+ * @return The {@link com.android.extensions.xr.media.SoundFieldAttributes SoundFieldAttributes}
+ * of the provided track, null if not set.
+ */
+ public com.android.extensions.xr.media.SoundFieldAttributes getSoundFieldAttributes(
+ android.media.AudioTrack track) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Gets the {@link SourceType} of the provided {@link android.media.AudioTrack AudioTrack}. This
+ * value is implicitly set depending one which type of attributes was used to configure the
+ * builder. Will return {@link SOURCE_TYPE_BYPASS} for tracks that didn't use spatial audio
+ * attributes.
+ *
+ * @param track The {@link android.media.AudioTrack AudioTrack} from which to get the {@link
+ * SourceType}.
+ * @return The {@link SourceType} of the provided track.
+ */
+ public int getSpatialSourceType(android.media.AudioTrack track) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Sets the {@link com.android.extensions.xr.media.PointSourceAttributes PointSourceAttributes}
+ * on the provided {@link android.media.AudioTrack.Builder AudioTrack.Builder}.
+ *
+ * @param builder The Builder on which to set the attributes.
+ * @param attributes The source attributes to be set.
+ * @return The same {AudioTrack.Builder} instance provided.
+ */
+ public android.media.AudioTrack.Builder setPointSourceAttributes(
+ android.media.AudioTrack.Builder builder,
+ com.android.extensions.xr.media.PointSourceAttributes attributes) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Sets the {@link com.android.extensions.xr.media.SoundFieldAttributes SoundFieldAttributes} on
+ * the provided {@link android.media.AudioTrack.Builder AudioTrack.Builder}.
+ *
+ * @param builder The Builder on which to set the attributes.
+ * @param attributes The sound field attributes to be set.
+ * @return The same {AudioTrack.Builder} instance provided.
+ */
+ public android.media.AudioTrack.Builder setSoundFieldAttributes(
+ android.media.AudioTrack.Builder builder,
+ com.android.extensions.xr.media.SoundFieldAttributes attributes) {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/MediaPlayerExtensions.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/MediaPlayerExtensions.java
new file mode 100644
index 0000000..52bb573
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/MediaPlayerExtensions.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.media;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Provides spatial audio extensions on the framework {@link android.media.MediaPlayer MediaPlayer}
+ * class.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class MediaPlayerExtensions {
+
+ MediaPlayerExtensions() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * @param mediaPlayer The {@link android.media.MediaPlayer MediaPlayer} on which to set the
+ * attributes.
+ * @param attributes The source attributes to be set.
+ * @return The same {@link android.media.MediaPlayer MediaPlayer} instance provided.
+ */
+ public android.media.MediaPlayer setPointSourceAttributes(
+ android.media.MediaPlayer mediaPlayer,
+ com.android.extensions.xr.media.PointSourceAttributes attributes) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Sets the {@link com.android.extensions.xr.media.SoundFieldAttributes SoundFieldAttributes} on
+ * the provided {@link android.media.MediaPlayer MediaPlayer}.
+ *
+ * @param mediaPlayer The {@link android.media.MediaPlayer MediaPlayer} on which to set the
+ * attributes.
+ * @param attributes The source attributes to be set.
+ * @return The same {@link android.media.MediaPlayer MediaPlayer} instance provided.
+ */
+ public android.media.MediaPlayer setSoundFieldAttributes(
+ android.media.MediaPlayer mediaPlayer,
+ com.android.extensions.xr.media.SoundFieldAttributes attributes) {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/PointSourceAttributes.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/PointSourceAttributes.java
new file mode 100644
index 0000000..383187d
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/PointSourceAttributes.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.media;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * {@link com.android.extensions.xr.media.PointSourceAttributes PointSourceAttributes} is used to
+ * configure a sound be spatialized as a 3D point.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class PointSourceAttributes {
+
+ PointSourceAttributes() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * The {@link com.android.extensions.xr.node.Node Node} to which this sound source is attached.
+ * The sound source will use the 3D transform of the Node. The node returned from this method
+ * must be parented to a node in the scene.
+ *
+ * @return The {@link com.android.extensions.xr.node.Node Node} to which the sound source is
+ * attached.
+ */
+ public com.android.extensions.xr.node.Node getNode() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Builder class for {@link com.android.extensions.xr.media.PointSourceAttributes
+ * PointSourceAttributes}
+ */
+ @SuppressWarnings({"unchecked", "deprecation", "all"})
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public static final class Builder {
+
+ public Builder() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * @param node The {@link com.android.extensions.xr.node.Node Node} to use to position the
+ * sound source.
+ * @return The Builder instance.
+ */
+ public com.android.extensions.xr.media.PointSourceAttributes.Builder setNode(
+ com.android.extensions.xr.node.Node node) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Creates a new {@link com.android.extensions.xr.media.PointSourceAttributes
+ * PointSourceAttributes} to be used. If no {@link com.android.extensions.xr.node.Node Node}
+ * is provided, this will create a new {@link com.android.extensions.xr.node.Node Node} that
+ * must be parented to a node in the current scene.
+ *
+ * @return A new {@link com.android.extensions.xr.media.PointSourceAttributes
+ * PointSourceAttributes} object.
+ */
+ public com.android.extensions.xr.media.PointSourceAttributes build()
+ throws java.lang.UnsupportedOperationException {
+ throw new RuntimeException("Stub!");
+ }
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/SoundFieldAttributes.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/SoundFieldAttributes.java
new file mode 100644
index 0000000..7ab72ea
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/SoundFieldAttributes.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.media;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * {@link com.android.extensions.xr.media.SoundFieldAttributes SoundFieldAttributes} is used to
+ * configure ambisonic sound sources.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SoundFieldAttributes {
+
+ SoundFieldAttributes() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * @return The {@link com.android.extensions.xr.media.SpatializerExtensions.AmbisonicsOrder
+ * SpatializerExtensions.AmbisonicsOrder} of this sound source.
+ */
+ public int getAmbisonicsOrder() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Builder class for {@link com.android.extensions.xr.media.SoundFieldAttributes
+ * SoundFieldAttributes}
+ */
+ @SuppressWarnings({"unchecked", "deprecation", "all"})
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public static final class Builder {
+
+ public Builder() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * @param ambisonicsOrder Sets the {@link
+ * com.android.extensions.xr.media.SpatializerExtensions.AmbisonicsOrder
+ * SpatializerExtensions.AmbisonicsOrder} of this sound source.
+ * @return The Builder instance.
+ */
+ public com.android.extensions.xr.media.SoundFieldAttributes.Builder setAmbisonicsOrder(
+ int ambisonicsOrder) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Creates a new {@link com.android.extensions.xr.media.PointSourceAttributes
+ * PointSourceAttributes} to be used. If no {@link Node} is provided, this will create a new
+ * {@link Node} that must be parented to a node in the current scene.
+ *
+ * @return A new {@link com.android.extensions.xr.media.PointSourceAttributes
+ * PointSourceAttributes} object.
+ */
+ public com.android.extensions.xr.media.SoundFieldAttributes build()
+ throws java.lang.UnsupportedOperationException {
+ throw new RuntimeException("Stub!");
+ }
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/SoundPoolExtensions.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/SoundPoolExtensions.java
new file mode 100644
index 0000000..841b69b
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/SoundPoolExtensions.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.media;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Provides spatial audio extensions on the framework {@link android.media.SoundPool SoundPool}
+ * class.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SoundPoolExtensions {
+
+ SoundPoolExtensions() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Plays a spatialized sound effect emitted relative {@link Node} in the {@link
+ * com.android.extensions.xr.media.PointSourceAttributes PointSourceAttributes}.
+ *
+ * @param soundPool The {@link android.media.SoundPool SoundPool} to use to the play the sound.
+ * @param soundID a soundId returned by the load() function.
+ * @param attributes attributes to specify sound source. {@link
+ * com.android.extensions.xr.media.PointSourceAttributes PointSourceAttributes}
+ * @param volume volume value (range = 0.0 to 1.0)
+ * @param priority stream priority (0 = lowest priority)
+ * @param loop loop mode (0 = no loop, -1 = loop forever)
+ * @param rate playback rate (1.0 = normal playback, range 0.5 to 2.0)
+ * @return non-zero streamID if successful, zero if failed
+ */
+ public int playAsPointSource(
+ android.media.SoundPool soundPool,
+ int soundID,
+ com.android.extensions.xr.media.PointSourceAttributes attributes,
+ float volume,
+ int priority,
+ int loop,
+ float rate) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Plays a spatialized sound effect as a sound field.
+ *
+ * @param soundPool The {@link android.media.SoundPool SoundPool} to use to the play the sound.
+ * @param soundID a soundId returned by the load() function.
+ * @param attributes attributes to specify sound source. {@link
+ * com.android.extensions.xr.media.SoundFieldAttributes SoundFieldAttributes}
+ * @param volume volume value (range = 0.0 to 1.0)
+ * @param priority stream priority (0 = lowest priority)
+ * @param loop loop mode (0 = no loop, -1 = loop forever)
+ * @param rate playback rate (1.0 = normal playback, range 0.5 to 2.0)
+ * @return non-zero streamID if successful, zero if failed
+ */
+ public int playAsSoundField(
+ android.media.SoundPool soundPool,
+ int soundID,
+ com.android.extensions.xr.media.SoundFieldAttributes attributes,
+ float volume,
+ int priority,
+ int loop,
+ float rate) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * @param soundPool The {@link android.media.SoundPool SoundPool} to use to get its SourceType.
+ * @param streamID a streamID returned by the play(), playAsPointSource(), or
+ * playAsSoundField().
+ * @return The {@link com.android.extensions.xr.media.SpatializerExtensions.SourceType
+ * SpatializerExtensions.SourceType} for the given streamID.
+ */
+ public int getSpatialSourceType(android.media.SoundPool soundPool, int streamID) {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/SpatializerExtensions.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/SpatializerExtensions.java
new file mode 100644
index 0000000..b08d6bd
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/SpatializerExtensions.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.media;
+
+import androidx.annotation.RestrictTo;
+
+/** Extensions of the existing {@link Spatializer} class. */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SpatializerExtensions {
+
+ SpatializerExtensions() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Specifies spatial rendering using First Order Ambisonics */
+ public static final int AMBISONICS_ORDER_FIRST_ORDER = 0; // 0x0
+
+ /** Specifies spatial rendering using Second Order Ambisonics */
+ public static final int AMBISONICS_ORDER_SECOND_ORDER = 1; // 0x1
+
+ /** Specifies spatial rendering using Third Order Ambisonics */
+ public static final int AMBISONICS_ORDER_THIRD_ORDER = 2; // 0x2
+
+ /** The sound source has not been spatialized with the Spatial Audio SDK. */
+ public static final int SOURCE_TYPE_BYPASS = 0; // 0x0
+
+ /** The sound source has been spatialized as a 3D point source. */
+ public static final int SOURCE_TYPE_POINT_SOURCE = 1; // 0x1
+
+ /** The sound source is an ambisonics sound field. */
+ public static final int SOURCE_TYPE_SOUND_FIELD = 2; // 0x2
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/XrSpatialAudioExtensions.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/XrSpatialAudioExtensions.java
new file mode 100644
index 0000000..acd9deb
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/media/XrSpatialAudioExtensions.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.media;
+
+import androidx.annotation.RestrictTo;
+
+/** Provides new functionality of existing framework APIs needed to Spatialize audio sources. */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class XrSpatialAudioExtensions {
+
+ XrSpatialAudioExtensions() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * @return {@link com.android.extensions.xr.media.SoundPoolExtensions SoundPoolExtensions}
+ * instance to control spatial audio from a {@link SoundPool}.
+ */
+ public com.android.extensions.xr.media.SoundPoolExtensions getSoundPoolExtensions() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * @return {@link com.android.extensions.xr.media.AudioTrackExtensions AudioTrackExtensions}
+ * instance to control spatial audio from an {@link AudioTrack}.
+ */
+ public com.android.extensions.xr.media.AudioTrackExtensions getAudioTrackExtensions() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * @return {@link com.android.extensions.xr.media.AudioManagerExtensions AudioManagerExtensions}
+ * instance to control spatial audio from an {@link AudioManager}.
+ */
+ public com.android.extensions.xr.media.AudioManagerExtensions getAudioManagerExtensions() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * @return {@link com.android.extensions.xr.media.MediaPlayerExtensions MediaPlayerExtensions}
+ * instance to control spatial audio from a {@link MediaPlayer}.
+ */
+ public com.android.extensions.xr.media.MediaPlayerExtensions getMediaPlayerExtensions() {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/InputEvent.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/InputEvent.java
new file mode 100644
index 0000000..4f39f6c
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/InputEvent.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.node;
+
+import androidx.annotation.RestrictTo;
+
+/** A single 6DOF pointer event. */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public final class InputEvent {
+
+ InputEvent() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Returns the source of this event. */
+ public int getSource() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Returns the pointer type of this event. */
+ public int getPointerType() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** The time this event occurred, in the android.os.SystemClock#uptimeMillis time base. */
+ public long getTimestamp() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** The origin of the ray, in the receiver's task coordinate space. */
+ public com.android.extensions.xr.node.Vec3 getOrigin() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * The direction the ray is pointing in, in the receiver's task coordinate space. Any point
+ * along the ray can be represented as origin + d * direction, where d is non-negative.
+ */
+ public com.android.extensions.xr.node.Vec3 getDirection() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Info about the first scene node (closest to the ray origin) that was hit by the input ray, if
+ * any. This will be null if no node was hit. Note that the hit node remains the same during an
+ * ongoing DOWN -> MOVE -> UP action, even if the pointer stops hitting the node during the
+ * action.
+ */
+ public com.android.extensions.xr.node.InputEvent.HitInfo getHitInfo() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Info about the second scene node from the same task that was hit, if any. */
+ public com.android.extensions.xr.node.InputEvent.HitInfo getSecondaryHitInfo() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Gets the dispatch flags. */
+ public int getDispatchFlags() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Returns the current action associated with this input event. */
+ public int getAction() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * While the primary action button or gesture was held, the pointer was disabled. This happens
+ * if you are using controllers and the battery runs out, or if you are using a source that
+ * transitions to a new pointer type, eg SOURCE_GAZE_AND_GESTURE.
+ */
+ public static final int ACTION_CANCEL = 3; // 0x3
+
+ /** The primary action button or gesture was just pressed / started. */
+ public static final int ACTION_DOWN = 0; // 0x0
+
+ /**
+ * The primary action button or gesture is not pressed, and the pointer ray started to hit a new
+ * node. The hit info represents the node that is being hit (may be null if pointer capture is
+ * enabled).
+ *
+ * <p>Hover input events are never provided for sensitive source types.
+ */
+ public static final int ACTION_HOVER_ENTER = 5; // 0x5
+
+ /**
+ * The primary action button or gesture is not pressed, and the pointer ray stopped hitting the
+ * node that it was previously hitting. The hit info represents the node that was being hit (may
+ * be null if pointer capture is enabled).
+ *
+ * <p>Hover input events are never provided for sensitive source types.
+ */
+ public static final int ACTION_HOVER_EXIT = 6; // 0x6
+
+ /**
+ * The primary action button or gesture is not pressed, and the pointer ray continued to hit the
+ * same node. The hit info represents the node that was hit (may be null if pointer capture is
+ * enabled).
+ *
+ * <p>Hover input events are never provided for sensitive source types.
+ */
+ public static final int ACTION_HOVER_MOVE = 4; // 0x4
+
+ /**
+ * The primary action button or gesture was pressed/active in the previous event, and is still
+ * pressed/active. The hit info represents the node that was originally hit (ie, as provided in
+ * the ACTION_DOWN event). The hit position may be null if the pointer is no longer hitting that
+ * node.
+ */
+ public static final int ACTION_MOVE = 2; // 0x2
+
+ /**
+ * The primary action button or gesture was just released / stopped. The hit info represents the
+ * node that was originally hit (ie, as provided in the ACTION_DOWN event).
+ */
+ public static final int ACTION_UP = 1; // 0x1
+
+ /** This event was also dispatched as a 2D Android input event. */
+ public static final int DISPATCH_FLAG_2D = 2; // 0x2
+
+ /** This event was dispatched to this receiver only because pointer capture was enabled. */
+ public static final int DISPATCH_FLAG_CAPTURED_POINTER = 1; // 0x1
+
+ /** Normal dispatch. */
+ public static final int DISPATCH_FLAG_NONE = 0; // 0x0
+
+ /**
+ * Default pointer type for the source (no handedness). Occurs for SOURCE_UNKNOWN, SOURCE_HEAD,
+ * SOURCE_MOUSE, and SOURCE_GAZE_AND_GESTURE.
+ */
+ public static final int POINTER_TYPE_DEFAULT = 0; // 0x0
+
+ /**
+ * Left hand / controller pointer.. Occurs for SOURCE_CONTROLLER, SOURCE_HANDS, and
+ * SOURCE_GAZE_AND_GESTURE.
+ */
+ public static final int POINTER_TYPE_LEFT = 1; // 0x1
+
+ /**
+ * Right hand / controller pointer.. Occurs for SOURCE_CONTROLLER, SOURCE_HANDS, and
+ * SOURCE_GAZE_AND_GESTURE.
+ */
+ public static final int POINTER_TYPE_RIGHT = 2; // 0x2
+
+ /**
+ * Event is based on (one of) the user's controller(s). Ray origin and direction are for a
+ * controller aim pose as defined by OpenXR.
+ * (https://registry.khronos.org/OpenXR/specs/1.1/html/xrspec.html#semantic-paths-standard-pose-identifiers)
+ * Action state is based on the primary button on the controller, usually the bottom-most face
+ * button.
+ */
+ public static final int SOURCE_CONTROLLER = 2; // 0x2
+
+ /**
+ * Event is based on a mix of the head, eyes, and hands. Ray origin is at average between eyes
+ * and points in direction based on a mix of eye gaze direction and hand motion. During a
+ * two-handed zoom/rotate gesture, left/right pointer events will be issued; otherwise, default
+ * events are issued based on the gaze ray. Action state is based on if the user has done a
+ * pinch gesture or not.
+ *
+ * <p>Events from this device type are considered sensitive and hover events are never sent.
+ */
+ public static final int SOURCE_GAZE_AND_GESTURE = 5; // 0x5
+
+ /**
+ * Event is based on one of the user's hands. Ray is a hand aim pose, with origin between thumb
+ * and forefinger and points in direction based on hand orientation. Action state is based on a
+ * pinch gesture.
+ */
+ public static final int SOURCE_HANDS = 3; // 0x3
+
+ /**
+ * Event is based on the user's head. Ray origin is at average between eyes, pushed out to the
+ * near clipping plane for both eyes and points in direction head is facing. Action state is
+ * based on volume up button being depressed.
+ *
+ * <p>Events from this device type are considered sensitive and hover events are never sent.
+ */
+ public static final int SOURCE_HEAD = 1; // 0x1
+
+ /**
+ * Event is based on a 2D mouse pointing device. Ray origin behaves the same as for
+ * DEVICE_TYPE_HEAD and points in direction based on mouse movement. During a drag, the ray
+ * origin moves approximating hand motion. The scrollwheel moves the ray away from / towards the
+ * user. Action state is based on the primary mouse button.
+ */
+ public static final int SOURCE_MOUSE = 4; // 0x4
+
+ public static final int SOURCE_UNKNOWN = 0; // 0x0
+
+ /** Info about a single ray hit. */
+ @SuppressWarnings({"unchecked", "deprecation", "all"})
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public static class HitInfo {
+
+ public HitInfo(
+ int subspaceImpressNodeId,
+ com.android.extensions.xr.node.Node inputNode,
+ com.android.extensions.xr.node.Mat4f transform,
+ com.android.extensions.xr.node.Vec3 hitPosition) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * ID of the front-end Impress node within the subspace that was hit. Used by Split-Engine
+ * to create a handle to the node with the same entity ID. In case the node doesn't belong
+ * to a subspace the value will be 0, i.e.,
+ * utils::Entity::import(subspaceImpressNodeId).IsNull() == true.
+ *
+ * <p>ACTION_MOVE, ACTION_UP, and ACTION_CANCEL events will report the same node id as was
+ * hit during the initial ACTION_DOWN.
+ */
+ public int getSubspaceImpressNodeId() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * The CPM node that was hit.
+ *
+ * <p>ACTION_MOVE, ACTION_UP, and ACTION_CANCEL events will report the same node as was hit
+ * during the initial ACTION_DOWN.
+ */
+ public com.android.extensions.xr.node.Node getInputNode() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * The ray hit position, in the receiver's task coordinate space.
+ *
+ * <p>All events may report the current ray's hit position. This can be null if there no
+ * longer is a collision between the ray and the input node (eg, during a drag event).
+ */
+ public com.android.extensions.xr.node.Vec3 getHitPosition() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** The matrix transforming task node coordinates into the hit CPM node's coordinates. */
+ public com.android.extensions.xr.node.Mat4f getTransform() {
+ throw new RuntimeException("Stub!");
+ }
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/Mat4f.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/Mat4f.java
new file mode 100644
index 0000000..e98f907
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/Mat4f.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.node;
+
+import androidx.annotation.RestrictTo;
+
+/** 4x4 matrix. */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public final class Mat4f {
+
+ public Mat4f(float[] m) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Gets a value from the matrix. */
+ public float get(int row, int column) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Sets a value in the matrix. */
+ public void set(int row, int column, float value) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Gets the values of the matrix as an array. */
+ public float[] getFlattenedMatrix() {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/Node.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/Node.java
new file mode 100644
index 0000000..0a8589e
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/Node.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.node;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Handle to a node in the SpaceFlinger scene graph that can also host a 2D Panel or 3D subspace.
+ *
+ * <p>A Node by itself does not have any visual representation. It merely defines a local space in
+ * its parent space. However, a node can also host a single 2D panel or 3D subspace. Once an element
+ * is hosted, the node must be attached to the rest of scene graph hierarchy for the element become
+ * visible and appear on-screen.
+ *
+ * <p>Note that {@link com.android.extensions.xr.node.Node Node} uses a right-hand coordinate
+ * system, i.e. +X points to the right, +Y up, and +Z points towards the camera.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class Node implements android.os.Parcelable {
+
+ Node() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Begins listening for 6DOF input events on this Node, and any descendant Nodes that do not
+ * have their own event listener set. The event listener is called on the provided Executor.
+ * Calling this method replaces any existing event listener for this node.
+ */
+ public void listenForInput(
+ com.android.extensions.xr.function.Consumer<com.android.extensions.xr.node.InputEvent>
+ listener,
+ java.util.concurrent.Executor executor) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Removes the listener for 6DOF input events from this Node. */
+ public void stopListeningForInput() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Sets the focus target for non-pointer input (eg, keyboard events) when this Node is clicked.
+ * The new target is the focusTarget's underlying View Root.
+ */
+ public void setNonPointerFocusTarget(android.view.AttachedSurfaceControl focusTarget) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Requests pointer capture. All XR input events that hit this node or any of its children are
+ * delivered as normal; any other input events that would otherwise be dispatched elsewhere will
+ * instead be delivered to the input queue of this node (without hit info).
+ *
+ * <p>The stateCallback is called immediately with the current state of this pointer capture.
+ * Whenever this node is visible and a descendant of a task that is not bounded (is in FSM or
+ * overlay space), pointer capture will be active; otherwise it will be paused.
+ *
+ * <p>If pointer capture is explicitly stopped by a new call to requestPointerCapture() on the
+ * same node, or by a call to stopPointerCapture(), POINTER_CAPTURE_STATE_STOPPED is passed (and
+ * the stateCallback will not be called subsequently; also, the app can be sure that no more
+ * captured pointer events will be delivered based on that request). This also occurs if the
+ * node is destroyed without explicitly stopping pointer capture, or if a new call to
+ * requestPointerCapture() is made on the same node without stopping the previous request.
+ *
+ * <p>If there are multiple pointer capture requests (eg from other apps) that could be active
+ * at the same time, the most recently requested one is activated; all other requests stay
+ * paused.
+ *
+ * <p>There can only be a single request per Node. If a new requestPointerCapture() call is made
+ * on the same node without stopping the previous pointer capture request, the previous request
+ * is automatically stopped.
+ *
+ * @param stateCallback a callback that will be called when pointer capture state changes.
+ * @param executor the executor the callback will be called on.
+ */
+ public void requestPointerCapture(
+ com.android.extensions.xr.function.Consumer<java.lang.Integer> stateCallback,
+ java.util.concurrent.Executor executor) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Disables previously-requested pointer capture on this node. The stateCallback callback will
+ * be called with POINTER_CAPTURE_STOPPED.
+ */
+ public void stopPointerCapture() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Subscribes to the transform of this node, relative to the OpenXR reference space used as
+ * world space for the shared scene. See {@code XrExtensions.getOpenXrWorldSpaceType()}. The
+ * provided matrix transforms a point in this node's local coordinate system into a point in
+ * world space coordinates. For example, {@code NodeTransform.getTransform()} * (0, 0, 0, 1) is
+ * the position of this node in world space. The first non-null transform will be returned
+ * immediately after the subscription set-up is complete. Note that the returned closeable must
+ * be closed by calling {@code close()} to prevent wasting system resources associated with the
+ * subscription.
+ *
+ * @param transformCallback a callback that will be called when this node's transform changes.
+ * @param executor the executor the callback will be called on.
+ * @return a Closeable that must be used to cancel the subscription by calling {@code close()}.
+ */
+ public java.io.Closeable subscribeToTransform(
+ com.android.extensions.xr.function.Consumer<
+ com.android.extensions.xr.node.NodeTransform>
+ transformCallback,
+ java.util.concurrent.Executor executor) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** A no-op override. */
+ public int describeContents() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Writes the Node to a Parcel. */
+ public void writeToParcel(android.os.Parcel out, int flags) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** toString() */
+ public java.lang.String toString() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** equals() */
+ public boolean equals(java.lang.Object object) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** hashCode() */
+ public int hashCode() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public static final int POINTER_CAPTURE_STATE_ACTIVE = 1; // 0x1
+
+ public static final int POINTER_CAPTURE_STATE_PAUSED = 0; // 0x0
+
+ public static final int POINTER_CAPTURE_STATE_STOPPED = 2; // 0x2
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/NodeTransaction.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/NodeTransaction.java
new file mode 100644
index 0000000..1983264
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/NodeTransaction.java
@@ -0,0 +1,510 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.node;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * An atomic set of changes to apply to a set of {@link com.android.extensions.xr.node.Node Node}s.
+ *
+ * <p>Note that {@link com.android.extensions.xr.node.Node Node} uses a right-hand coordinate
+ * system, i.e. +X points to the right, +Y up, and +Z points towards the camera.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class NodeTransaction implements java.io.Closeable {
+
+ NodeTransaction() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Sets a name for the node that is used to it in `adb dumpsys cpm` output log.
+ *
+ * <p>While the name does not have to be globally unique, it is recommended to set a unique name
+ * for each node for ease of debugging.
+ *
+ * @param node The node to be updated.
+ * @param name The debug name of the node.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction setName(
+ com.android.extensions.xr.node.Node node, java.lang.String name) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Sets the parent of this node to the given node.
+ *
+ * <p>This method detaches the node from its current branch and moves into the new parent's
+ * hierarchy (if any). If parent parameter is `null`, the node will be orphaned and removed from
+ * the rendering tree until it is reattached to another node that is in the root hierarchy.
+ *
+ * @param node The node to be updated.
+ * @param parent The new parent of the node or `null` if the node is to be removed from the
+ * rendering tree.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction setParent(
+ com.android.extensions.xr.node.Node node, com.android.extensions.xr.node.Node parent) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Sets the position of the node in the local coordinate space (parent space).
+ *
+ * @param node The node to be updated.
+ * @param x The 'x' distance in meters from parent's origin.
+ * @param y The 'y' distance in meters from parent's origin.
+ * @param z The 'z' distance in meters from parent's origin.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction setPosition(
+ com.android.extensions.xr.node.Node node, float x, float y, float z) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Rotates the node by the quaternion specified by x, y, z, and w components in the local
+ * coordinate space.
+ *
+ * @param node The node to be updated.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction setOrientation(
+ com.android.extensions.xr.node.Node node, float x, float y, float z, float w) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Scales the node along the x, y, and z axis in the local coordinate space.
+ *
+ * <p>For 2D panels, this method scales the panel in the world, increasing its visual size
+ * without changing the buffer size. It will not trigger a relayout and will not affect its
+ * enclosing view's layout configuration.
+ *
+ * @param node The node to be updated.
+ * @param sx The scaling factor along the x-axis.
+ * @param sy The scaling factor along the y-axis.
+ * @param sz The scaling factor along the z-axis.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction setScale(
+ com.android.extensions.xr.node.Node node, float sx, float sy, float sz) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Sets the opacity of the node's content to a value between [0..1].
+ *
+ * @param value The new opacity amount in range of [0..1].
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction setAlpha(
+ com.android.extensions.xr.node.Node node, float value) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Changes the visibility of the node and its content.
+ *
+ * @param isVisible Whether the node is visible.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction setVisibility(
+ com.android.extensions.xr.node.Node node, boolean isVisible) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Configures the node to host and control the given surface data.
+ *
+ * <p>Passing a 'null' for surfaceControl parameter will disassociate it from the node, so the
+ * same node can be used to host another surface or volume data.
+ *
+ * @param node The node to be updated.
+ * @param surfaceControl Handle to an on-screen surface managed by the system compositor, or
+ * 'null' to disassociate the currently hosted surface from the node.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction setSurfaceControl(
+ com.android.extensions.xr.node.Node node, android.view.SurfaceControl surfaceControl) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Configures the node to host and control the given surface data.
+ *
+ * <p>This method is similar to {@link
+ * #setSurfaceControl(com.android.extensions.xr.node.Node,android.view.SurfaceControl)} and is
+ * provided for convenience.
+ *
+ * @param node The node to be updated.
+ * @param surfacePackage The package that contains the {@link android.view.SurfaceControl
+ * SurfaceControl}, or 'null' to disassociate the currently hosted surface from the node.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction setSurfacePackage(
+ com.android.extensions.xr.node.Node node,
+ android.view.SurfaceControlViewHost.SurfacePackage surfacePackage) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Crops the 2D buffer of the Surface hosted by this node to match the given bounds in pixels.
+ *
+ * <p>This method only applies to nodes that host a {@link android.view.SurfaceControl
+ * SurfaceControl} set by {@link #setSurfaceControl}.
+ *
+ * @param surfaceControl The on-screen surface.
+ * @param widthPx The width of the surface in pixels.
+ * @param heightPx The height of the surface in pixels.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction setWindowBounds(
+ android.view.SurfaceControl surfaceControl, int widthPx, int heightPx) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Crops the 2D buffer of the Surface hosted by this node to match the given bounds in pixels.
+ *
+ * <p>This method is similar to {@link #setWindowBounds(android.view.SurfaceControl,int,int)}
+ * and is provided for convenience.
+ *
+ * @param surfacePackage The package that contains the {@link android.view.SurfaceControl
+ * SurfaceControl}.
+ * @param widthPx The width of the surface in pixels.
+ * @param heightPx The height of the surface in pixels.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction setWindowBounds(
+ android.view.SurfaceControlViewHost.SurfacePackage surfacePackage,
+ int widthPx,
+ int heightPx) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Curves the XY plane of the node around the y-axis and towards the positive z-axis.
+ *
+ * <p>This method essentially curves the x-axis of the node, moving and rotating its children to
+ * align with the new x-axis shape. It will also curve the children's x-axes in a similar
+ * manner.
+ *
+ * <p>If this node is hosting a 2D panel, setting a curvature will bend the panel along the Y
+ * axis, projecting it onto a cylinder defined by the given radius.
+ *
+ * <p>To remove the curvature, set the radius to 0.
+ *
+ * @param node The node to be updated.
+ * @param curvature A positive value equivalent to 1/radius, where 'radius' represents the
+ * radial distance of the polar coordinate system that is used to curve the x-axis. Setting
+ * this value to 0 will straighten the axis and remove its curvature.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ * @deprecated Use Split Engine to create a curved panel.
+ */
+ @Deprecated
+ public com.android.extensions.xr.node.NodeTransaction setCurvature(
+ com.android.extensions.xr.node.Node node, float curvature) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Sets the resolution of 2D surfaces under this node.
+ *
+ * <p>The sizes of 2D surfaces under this node will be set according to their 2D pixel
+ * dimensions and the pixelsPerMeter value. The pixelsPerMeter value is propagated to child
+ * nodes.
+ *
+ * @param node The node to be updated.
+ * @param pixelsPerMeter The number of pixels per meter to use when sizing 2D surfaces.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction setPixelResolution(
+ com.android.extensions.xr.node.Node node, float pixelsPerMeter) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Sets whether position is interpreted in meters or in pixels for each dimension.
+ *
+ * <p>The sizes of 2D surfaces under this node will be set according to their 2D pixel
+ * dimensions and the pixelsPerMeter value. The pixelsPerMeter value is propagated to child
+ * nodes.
+ *
+ * @param node The node to be updated.
+ * @param pixelPositionFlags Flags indicating which dimensins of the local position of the node
+ * should be interpreted as pixel values (as opposed to the default meters).
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction setPixelPositioning(
+ com.android.extensions.xr.node.Node node, int pixelPositionFlags) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Renders a previously loaded glTF model.
+ *
+ * <p>The token must belong to a previously loaded glTF model that is currently cached in the
+ * SpaceFlinger.
+ *
+ * @param node The node to be updated.
+ * @param gltfModelToken The token of a glTF model that was previously loaded.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ public com.android.extensions.xr.node.NodeTransaction setGltfModel(
+ com.android.extensions.xr.node.Node node,
+ com.android.extensions.xr.asset.GltfModelToken gltfModelToken) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Renders a previously loaded environment.
+ *
+ * <p>The token must belong to a previously loaded environment that is currently cached in the
+ * SpaceFlinger.
+ *
+ * @param node The node to be updated.
+ * @param environmentToken The token of an environment that was previously loaded.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ public com.android.extensions.xr.node.NodeTransaction setEnvironment(
+ com.android.extensions.xr.node.Node node,
+ com.android.extensions.xr.asset.EnvironmentToken environmentToken) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Renders a previously loaded Impress scene.
+ *
+ * <p>The token must belong to a previously loaded Impress scene that is currently cached in the
+ * SpaceFlinger.
+ *
+ * @param node The node to be updated.
+ * @param sceneToken The token of an Impress scene that was previously loaded.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ public com.android.extensions.xr.node.NodeTransaction setImpressScene(
+ com.android.extensions.xr.node.Node node,
+ com.android.extensions.xr.asset.SceneToken sceneToken) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Animates a previously loaded glTF model.
+ *
+ * @param node The node to be updated.
+ * @param gltfAnimationName The name of the glTF animation.
+ * @param gltfAnimationState The {@link com.android.extensions.xr.asset.GltfAnimation.State
+ * GltfAnimation.State} state of the glTF animation.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ * @deprecated JXR Core doesn't need this anymore as it does the same with Split Engine.
+ */
+ @Deprecated
+ public com.android.extensions.xr.node.NodeTransaction setGltfAnimation(
+ com.android.extensions.xr.node.Node node,
+ java.lang.String gltfAnimationName,
+ com.android.extensions.xr.asset.GltfAnimation.State gltfAnimationState) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Sets the transform of the node on a per-frame basis from a previously created anchor.
+ *
+ * <p>The client who created the anchor and provided the ID will always remain the owner of the
+ * anchor.
+ *
+ * <p>Modifying the transform of the node will only be applied if or when the anchor is no
+ * longer linked to the node, or if the anchor is no longer locatable.
+ *
+ * <p>A node can be unlinked from an anchor by setting the ID to null. Note that this does not
+ * destroy the actual anchor.
+ *
+ * @param node The node to be updated.
+ * @param anchorId The ID of a previously created anchor.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction setAnchorId(
+ com.android.extensions.xr.node.Node node, android.os.IBinder anchorId) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Sets a subspace to be used.
+ *
+ * @param node The node to be updated.
+ * @param subspace The previously created subspace to be associated with the node.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction setSubspace(
+ com.android.extensions.xr.node.Node node,
+ com.android.extensions.xr.subspace.Subspace subspace) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Updates the passthrough state.
+ *
+ * @param node The node to be updated.
+ * @param passthroughOpacity The opacity of the passthrough layer where 0.0 means no passthrough
+ * and 1.0 means full passthrough.
+ * @param passthroughMode The {@link com.android.extensions.xr.passthrough.PassthroughState.Mode
+ * PassthroughState.Mode} mode that the passthrough will use.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction setPassthroughState(
+ com.android.extensions.xr.node.Node node,
+ float passthroughOpacity,
+ int passthroughMode) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Enables reform UX for a node.
+ *
+ * @param node The node to be updated.
+ * @param options Configuration options for the reform UX.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction enableReform(
+ com.android.extensions.xr.node.Node node,
+ com.android.extensions.xr.node.ReformOptions options) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Updates the size of the reform UX.
+ *
+ * @param node The node to be updated.
+ * @param reformSize The new size in meters that should be used to lay out the reform UX.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction setReformSize(
+ com.android.extensions.xr.node.Node node,
+ com.android.extensions.xr.node.Vec3 reformSize) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Disables reform UX for a node.
+ *
+ * @param node The node to be updated.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction disableReform(
+ com.android.extensions.xr.node.Node node) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Sets the corner radius for 2D surfaces under this node.
+ *
+ * <p>The corner radius is propagated to child nodes.
+ *
+ * @param node The node to be updated.
+ * @param cornerRadius The corner radius for 2D surfaces under this node, in meters.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction setCornerRadius(
+ com.android.extensions.xr.node.Node node, float cornerRadius) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Removes the corner radius from this node.
+ *
+ * @param node The node to be updated.
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction removeCornerRadius(
+ com.android.extensions.xr.node.Node node) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Merges the given transaction into this one so that they can be submitted together to the
+ * system. All of the changes in the other transaction are moved into this one; the other
+ * transaction is left in an empty state.
+ *
+ * @return The reference to this {@link com.android.extensions.xr.node.NodeTransaction
+ * NodeTransaction} object that is currently being updated.
+ */
+ public com.android.extensions.xr.node.NodeTransaction merge(
+ com.android.extensions.xr.node.NodeTransaction other) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Submits the queued transactions to backend.
+ *
+ * <p>This method will clear the existing transaction state so the same transaction object can
+ * be used for the next set of updates.
+ */
+ public void apply() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Closes and releases the native transaction object without applying it.
+ *
+ * <p>Note that a closed transaction cannot be used again.
+ */
+ public void close() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public static final int POSITION_FROM_PARENT_TOP_LEFT = 64; // 0x40
+
+ public static final int X_POSITION_IN_PIXELS = 1; // 0x1
+
+ public static final int Y_POSITION_IN_PIXELS = 2; // 0x2
+
+ public static final int Z_POSITION_IN_PIXELS = 4; // 0x4
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/NodeTransform.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/NodeTransform.java
new file mode 100644
index 0000000..189dd51
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/NodeTransform.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.node;
+
+import androidx.annotation.RestrictTo;
+
+/** interface containing the Node transform */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class NodeTransform {
+
+ NodeTransform() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Get the transformation matrix associated with the node.
+ *
+ * <p>The provided matrix transforms a point in this node's local coordinate system into a point
+ * in world space coordinates. For example, {@code NodeTransform.getTransform()} * (0, 0, 0, 1)
+ * is the position of this node in world space. The first non-null transform will be returned
+ * immediately after the subscription set-up is complete.
+ *
+ * @return A transformation matrix {@link com.android.extensions.xr.node.Mat4f Mat4f} containing
+ * the current transformation matrix of this node.
+ */
+ public com.android.extensions.xr.node.Mat4f getTransform() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Get the timestamp at which the transformation matrix was recorded.
+ *
+ * <p>The time the record happened, in the android.os.SystemClock#uptimeNanos time base.
+ *
+ * @return A timestamp at which the transformation matrix was recorded.
+ */
+ public long getTimestamp() {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/Quatf.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/Quatf.java
new file mode 100644
index 0000000..9879687
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/Quatf.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.node;
+
+import androidx.annotation.RestrictTo;
+
+/** Quaternion. q = w + xi + yj + zk */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public final class Quatf {
+
+ public Quatf(float x, float y, float z, float w) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public final float w;
+
+ {
+ w = 0;
+ }
+
+ public final float x;
+
+ {
+ x = 0;
+ }
+
+ public final float y;
+
+ {
+ y = 0;
+ }
+
+ public final float z;
+
+ {
+ z = 0;
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/ReformEvent.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/ReformEvent.java
new file mode 100644
index 0000000..fc8a5f0
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/ReformEvent.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.node;
+
+import androidx.annotation.RestrictTo;
+
+/** A reform (move / resize) event. */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class ReformEvent {
+
+ ReformEvent() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Gets the event type. */
+ public int getType() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Gets the event state. */
+ public int getState() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** An identifier for this reform action. */
+ public int getId() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** The initial ray origin and direction, in task space. */
+ public com.android.extensions.xr.node.Vec3 getInitialRayOrigin() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** The initial ray direction, in task space. */
+ public com.android.extensions.xr.node.Vec3 getInitialRayDirection() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** The current ray origin and direction, in task space. */
+ public com.android.extensions.xr.node.Vec3 getCurrentRayOrigin() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** The current ray direction, in task space. */
+ public com.android.extensions.xr.node.Vec3 getCurrentRayDirection() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * For a move event, the proposed pose of the node, in task space (or relative to the parent
+ * node, if FLAG_POSE_RELATIVE_TO_PARENT was specified in the ReformOptions).
+ */
+ public com.android.extensions.xr.node.Vec3 getProposedPosition() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** For a move event, the proposed orientation of the node, in task space. */
+ public com.android.extensions.xr.node.Quatf getProposedOrientation() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Scale will change with distance if ReformOptions.FLAG_SCALE_WITH_DISTANCE is set. */
+ public com.android.extensions.xr.node.Vec3 getProposedScale() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * For a resize event, the proposed new size in meters. Note that in the initial implementation,
+ * the Z size may not be modified.
+ */
+ public com.android.extensions.xr.node.Vec3 getProposedSize() {
+ throw new RuntimeException("Stub!");
+ }
+
+ public static final int REFORM_STATE_END = 3; // 0x3
+
+ public static final int REFORM_STATE_ONGOING = 2; // 0x2
+
+ public static final int REFORM_STATE_START = 1; // 0x1
+
+ public static final int REFORM_STATE_UNKNOWN = 0; // 0x0
+
+ public static final int REFORM_TYPE_MOVE = 1; // 0x1
+
+ public static final int REFORM_TYPE_RESIZE = 2; // 0x2
+
+ public static final int REFORM_TYPE_UNKNOWN = 0; // 0x0
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/ReformOptions.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/ReformOptions.java
new file mode 100644
index 0000000..3c0242c
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/ReformOptions.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.node;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Configuration options for reform (move/resize) UX. To create a ReformOptions instance, call
+ * {@code XrExtensions.createReformOptions()}.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class ReformOptions {
+
+ ReformOptions() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Which reform actions are enabled. */
+ public int getEnabledReform() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** By default, only ALLOW_MOVE is enabled. */
+ public com.android.extensions.xr.node.ReformOptions setEnabledReform(int enabledReform) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Behaviour flags. */
+ public int getFlags() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** By default, the flags are set to 0. */
+ public com.android.extensions.xr.node.ReformOptions setFlags(int flags) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Current size of the content, in meters. This is the local size (does not include any scale
+ * factors)
+ */
+ public com.android.extensions.xr.node.Vec3 getCurrentSize() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** By default, the current size is set to (1, 1, 1). */
+ public com.android.extensions.xr.node.ReformOptions setCurrentSize(
+ com.android.extensions.xr.node.Vec3 currentSize) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Minimum size of the content, in meters. This is a local size. */
+ public com.android.extensions.xr.node.Vec3 getMinimumSize() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** By default, the minimum size is set to (1, 1, 1). */
+ public com.android.extensions.xr.node.ReformOptions setMinimumSize(
+ com.android.extensions.xr.node.Vec3 minimumSize) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Maximum size of the content, in meters. This is a local size. */
+ public com.android.extensions.xr.node.Vec3 getMaximumSize() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** By default, the maximum size is set to (1, 1, 1). */
+ public com.android.extensions.xr.node.ReformOptions setMaximumSize(
+ com.android.extensions.xr.node.Vec3 maximumSize) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** The aspect ratio of the content on resizing. <= 0.0f when there are no preferences. */
+ public float getFixedAspectRatio() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * The aspect ratio determined by taking the panel's width over its height. An aspect ratio
+ * value less than 0 will be ignored. A value <= 0.0f means there are no preferences.
+ */
+ public com.android.extensions.xr.node.ReformOptions setFixedAspectRatio(
+ float fixedAspectRatio) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Returns the current value of forceShowResizeOverlay. */
+ public boolean getForceShowResizeOverlay() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * If forceShowResizeOverlay is set to true, the resize overlay will always be show (until
+ * forceShowResizeOverlay is changed to false). This can be used by apps to implement their own
+ * resize affordances.
+ */
+ public com.android.extensions.xr.node.ReformOptions setForceShowResizeOverlay(
+ boolean forceShowResizeOverlay) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Returns the callback that will receive reform events. */
+ public com.android.extensions.xr.function.Consumer<com.android.extensions.xr.node.ReformEvent>
+ getEventCallback() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Sets the callback that will receive reform events. */
+ public com.android.extensions.xr.node.ReformOptions setEventCallback(
+ com.android.extensions.xr.function.Consumer<com.android.extensions.xr.node.ReformEvent>
+ callback) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Returns the executor that events will be handled on. */
+ public java.util.concurrent.Executor getEventExecutor() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Sets the executor that events will be handled on. */
+ public com.android.extensions.xr.node.ReformOptions setEventExecutor(
+ java.util.concurrent.Executor executor) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Returns the current value of scaleWithDistanceMode. */
+ public int getScaleWithDistanceMode() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * If scaleWithDistanceMode is set, and FLAG_SCALE_WITH_DISTANCE is also in use, the scale the
+ * system suggests (or automatically applies when FLAG_ALLOW_SYSTEM_MOVEMENT is also in use)
+ * follows scaleWithDistanceMode:
+ *
+ * <p>SCALE_WITH_DISTANCE_MODE_DEFAULT: The panel scales in the same way as home space mode.
+ * SCALE_WITH_DISTANCE_MODE_DMM: The panel scales in a way that the user-perceived panel size
+ * never changes.
+ *
+ * <p>When FLAG_SCALE_WITH_DISTANCE is not in use, scaleWithDistanceMode is ignored.
+ */
+ public com.android.extensions.xr.node.ReformOptions setScaleWithDistanceMode(
+ int scaleWithDistanceMode) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public static final int ALLOW_MOVE = 1; // 0x1
+
+ public static final int ALLOW_RESIZE = 2; // 0x2
+
+ public static final int FLAG_ALLOW_SYSTEM_MOVEMENT = 2; // 0x2
+
+ public static final int FLAG_POSE_RELATIVE_TO_PARENT = 4; // 0x4
+
+ public static final int FLAG_SCALE_WITH_DISTANCE = 1; // 0x1
+
+ public static final int SCALE_WITH_DISTANCE_MODE_DEFAULT = 3; // 0x3
+
+ public static final int SCALE_WITH_DISTANCE_MODE_DMM = 2; // 0x2
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/Vec3.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/Vec3.java
new file mode 100644
index 0000000..746d909
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/node/Vec3.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.node;
+
+import androidx.annotation.RestrictTo;
+
+/** 3D vector. */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public final class Vec3 {
+
+ public Vec3(float x, float y, float z) {
+ throw new RuntimeException("Stub!");
+ }
+
+ public final float x;
+
+ {
+ x = 0;
+ }
+
+ public final float y;
+
+ {
+ y = 0;
+ }
+
+ public final float z;
+
+ {
+ z = 0;
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/passthrough/PassthroughState.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/passthrough/PassthroughState.java
new file mode 100644
index 0000000..42f3901
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/passthrough/PassthroughState.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.passthrough;
+
+import androidx.annotation.RestrictTo;
+
+/** Allows to configure the passthrough when the application is in full-space mode. */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class PassthroughState {
+
+ PassthroughState() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Node maximizes the opacity of the final passthrough state. */
+ public static final int PASSTHROUGH_MODE_MAX = 1; // 0x1
+
+ /** Node minimizes the opacity of the final passthrough state. */
+ public static final int PASSTHROUGH_MODE_MIN = 2; // 0x2
+
+ /** Node does not contribute to the opacity of the final passthrough state. */
+ public static final int PASSTHROUGH_MODE_OFF = 0; // 0x0
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/ActivityPanel.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/ActivityPanel.java
new file mode 100644
index 0000000..c33ca4a
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/ActivityPanel.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.space;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Defines the panel in XR scene to support embedding activities within a host activity.
+ *
+ * <p>When the host activity is destroyed, all the activities in its embedded {@link
+ * com.android.extensions.xr.space.ActivityPanel ActivityPanel} will also be destroyed.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class ActivityPanel {
+
+ ActivityPanel() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Launches an activity into this panel.
+ *
+ * @param intent the {@link android.content.Intent Intent} to start.
+ * @param options additional options for how the Activity should be started.
+ */
+ public void launchActivity(android.content.Intent intent, android.os.Bundle options) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Moves an existing activity into this panel.
+ *
+ * @param activity the {@link android.app.Activity Activity} to move.
+ */
+ public void moveActivity(android.app.Activity activity) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Gets the node associated with this {@link com.android.extensions.xr.space.ActivityPanel
+ * ActivityPanel}.
+ *
+ * <p>The {@link com.android.extensions.xr.space.ActivityPanel ActivityPanel} can only be shown
+ * to the user after this node is attached to the host activity's scene.
+ *
+ * @see androidx.xr.extensions.XrExtensions#attachSpatialScene
+ */
+ public com.android.extensions.xr.node.Node getNode() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Updates the 2D window bounds of this {@link com.android.extensions.xr.space.ActivityPanel
+ * ActivityPanel}.
+ *
+ * <p>If the new bounds are smaller that the minimum dimensions of the activity embedded in this
+ * ActivityPanel, the ActivityPanel bounds will be reset to match the host Activity bounds.
+ *
+ * @param windowBounds the new 2D window bounds in the host container window coordinates.
+ */
+ public void setWindowBounds(android.graphics.Rect windowBounds) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Deletes the activity panel. All the activities in this {@link
+ * com.android.extensions.xr.space.ActivityPanel ActivityPanel} will also be destroyed.
+ */
+ public void delete() {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/ActivityPanelLaunchParameters.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/ActivityPanelLaunchParameters.java
new file mode 100644
index 0000000..cf58741
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/ActivityPanelLaunchParameters.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.space;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Defines the launch parameters when creating an {@link
+ * com.android.extensions.xr.space.ActivityPanel ActivityPanel}.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public final class ActivityPanelLaunchParameters {
+
+ /**
+ * Constructs an {@link com.android.extensions.xr.space.ActivityPanelLaunchParameters
+ * ActivityPanelLaunchParameters} with the given initial window bounds.
+ *
+ * @param windowBounds the initial 2D window bounds of the panel, which will be the bounds of
+ * the Activity launched into the {@link com.android.extensions.xr.space.ActivityPanel
+ * ActivityPanel}.
+ */
+ public ActivityPanelLaunchParameters(android.graphics.Rect windowBounds) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * @return the initial 2D window bounds.
+ */
+ public android.graphics.Rect getWindowBounds() {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/Bounds.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/Bounds.java
new file mode 100644
index 0000000..5562fc58d
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/Bounds.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.space;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Bounds values in meters.
+ *
+ * @see androidx.xr.extensions.XrExtensions#getSpatialState
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class Bounds {
+
+ public Bounds(float width, float height, float depth) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** The bounds width. */
+ public float getWidth() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** The bounds height. */
+ public float getHeight() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** The bounds depth. */
+ public float getDepth() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** equals() */
+ public boolean equals(java.lang.Object other) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** hashCode() */
+ public int hashCode() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** toString() */
+ public java.lang.String toString() {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/BoundsChangeEvent.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/BoundsChangeEvent.java
new file mode 100644
index 0000000..255c6198
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/BoundsChangeEvent.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.space;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Triggers when there is a bounds change. For example, resize the panel in home space, or
+ * enter/exit FSM.
+ *
+ * @deprecated Use SpatialState instead.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@Deprecated
+public final class BoundsChangeEvent implements com.android.extensions.xr.space.SpatialStateEvent {
+
+ @Deprecated
+ public BoundsChangeEvent(com.android.extensions.xr.space.Bounds bounds) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Width of the bounds in meters.
+ *
+ * @deprecated Use SpatialState instead.
+ */
+ @Deprecated
+ public float getWidth() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Height of the bounds in meters.
+ *
+ * @deprecated Use SpatialState instead.
+ */
+ @Deprecated
+ public float getHeight() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Depth of the bounds in meters.
+ *
+ * @deprecated Use SpatialState instead.
+ */
+ @Deprecated
+ public float getDepth() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Bounds in meters.
+ *
+ * @deprecated Use SpatialState instead.
+ */
+ @Deprecated
+ public com.android.extensions.xr.space.Bounds getBounds() {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/EnvironmentControlChangeEvent.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/EnvironmentControlChangeEvent.java
new file mode 100644
index 0000000..73ec74e
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/EnvironmentControlChangeEvent.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.space;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Triggers when the ability to control the environment changes.
+ *
+ * @deprecated Use SpatialState instead.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@Deprecated
+public final class EnvironmentControlChangeEvent
+ implements com.android.extensions.xr.space.SpatialStateEvent {
+
+ @Deprecated
+ public EnvironmentControlChangeEvent(boolean allowed) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Whether or not the receiver can control the environment.
+ *
+ * @deprecated Use SpatialState instead.
+ */
+ @Deprecated
+ public boolean getEnvironmentControlAllowed() {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/EnvironmentVisibilityChangeEvent.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/EnvironmentVisibilityChangeEvent.java
new file mode 100644
index 0000000..233f5c7
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/EnvironmentVisibilityChangeEvent.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.space;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * For all resumed top activities with this spatialstate callback set, this is called whenever the
+ * VR background changes. This is also called when an activity becomes top resumed.
+ *
+ * @deprecated Use SpatialState instead.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@Deprecated
+public final class EnvironmentVisibilityChangeEvent
+ implements com.android.extensions.xr.space.SpatialStateEvent {
+
+ @Deprecated
+ public EnvironmentVisibilityChangeEvent(int state) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Visibility state of the VR background
+ *
+ * @deprecated Use SpatialState instead.
+ */
+ @Deprecated
+ public int getEnvironmentState() {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/HitTestResult.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/HitTestResult.java
new file mode 100644
index 0000000..007a3ca
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/HitTestResult.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.space;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Hit test result.
+ *
+ * @see androidx.xr.extensions.XrExtensions#hitTest
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class HitTestResult {
+
+ HitTestResult() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Distance from the ray origin to the hit position. */
+ public float getDistance() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** The hit position in task coordinates. */
+ public com.android.extensions.xr.node.Vec3 getHitPosition() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Normal of the surface at the collision point, if known. */
+ public com.android.extensions.xr.node.Vec3 getSurfaceNormal() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** The type of surface that was hit. */
+ public int getSurfaceType() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Whether or not the virtual background environment is visible. */
+ public boolean getVirtualEnvironmentIsVisible() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** The ray has hit a 3D object */
+ public static final int SURFACE_3D_OBJECT = 2; // 0x2
+
+ /** The ray has hit a 2D panel */
+ public static final int SURFACE_PANEL = 1; // 0x1
+
+ /** The ray has hit something unknown or nothing at all */
+ public static final int SURFACE_UNKNOWN = 0; // 0x0
+
+ @SuppressWarnings({"unchecked", "deprecation", "all"})
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public static final class Builder {
+
+ public Builder(
+ float distance,
+ com.android.extensions.xr.node.Vec3 hitPosition,
+ boolean virtualEnvironmentIsVisible,
+ int surfaceType) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Sets the surface vector. */
+ public com.android.extensions.xr.space.HitTestResult.Builder setSurfaceNormal(
+ com.android.extensions.xr.node.Vec3 surfaceNormal) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Builds the HitTestResult. */
+ public com.android.extensions.xr.space.HitTestResult build() {
+ throw new RuntimeException("Stub!");
+ }
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/SpatialCapabilities.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/SpatialCapabilities.java
new file mode 100644
index 0000000..a9f25752
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/SpatialCapabilities.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.space;
+
+import androidx.annotation.RestrictTo;
+
+/** Represents a set of capabilities an activity has. */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SpatialCapabilities {
+
+ public SpatialCapabilities() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** Returns true if the capability is available. */
+ public boolean get(int capability) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** equals() */
+ public boolean equals(java.lang.Object other) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** hashCode() */
+ public int hashCode() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** toString() */
+ public java.lang.String toString() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** The activity can set its own environment. */
+ public static final int APP_ENVIRONMENTS_CAPABLE = 3; // 0x3
+
+ /** The activity can enable or disable passthrough. */
+ public static final int PASSTHROUGH_CONTROL_CAPABLE = 2; // 0x2
+
+ /**
+ * The activity can create 3D contents.
+ *
+ * <p>This capability allows neither spatial panel creation nor spatial activity embedding.
+ */
+ public static final int SPATIAL_3D_CONTENTS_CAPABLE = 1; // 0x1
+
+ /**
+ * The activity can launch another activity on a spatial panel to spatially embed it.
+ *
+ * <p>This capability allows neither spatial panel creation nor 3D content creation.
+ */
+ public static final int SPATIAL_ACTIVITY_EMBEDDING_CAPABLE = 5; // 0x5
+
+ /** The activity can use spatial audio. */
+ public static final int SPATIAL_AUDIO_CAPABLE = 4; // 0x4
+
+ /**
+ * The activity can spatialize itself by adding a spatial panel.
+ *
+ * <p>This capability allows neither 3D content creation nor spatial activity embedding.
+ */
+ public static final int SPATIAL_UI_CAPABLE = 0; // 0x0
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/SpatialCapabilityChangeEvent.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/SpatialCapabilityChangeEvent.java
new file mode 100644
index 0000000..be1b160
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/SpatialCapabilityChangeEvent.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.space;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Triggers when the spatial capability set has changed.
+ *
+ * @deprecated Use SpatialState instead.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@Deprecated
+public final class SpatialCapabilityChangeEvent
+ implements com.android.extensions.xr.space.SpatialStateEvent {
+
+ @Deprecated
+ public SpatialCapabilityChangeEvent(
+ com.android.extensions.xr.space.SpatialCapabilities capabilities) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Gets the enabled capabailities.
+ *
+ * @deprecated Use SpatialState instead.
+ */
+ @Deprecated
+ public com.android.extensions.xr.space.SpatialCapabilities getCurrentCapabilities() {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/SpatialState.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/SpatialState.java
new file mode 100644
index 0000000..bd2356f
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/SpatialState.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.space;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * An interface that represents an activity's spatial state.
+ *
+ * <p>An object of the class is effectively immutable. Once the object, which is a "snapshot" of the
+ * activity's spatial state, is returned to the client, each getters will always return the same
+ * value even if the activity's state later changes.
+ *
+ * @see androidx.xr.extensions.XrExtensions#registerSpatialStateCallback
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SpatialState {
+
+ SpatialState() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Gets spatial bounds of the activity. When in full space mode, (infinity, infinity, infinity)
+ * is returned.
+ *
+ * @see androidx.xr.extensions.space.Bounds
+ */
+ public com.android.extensions.xr.space.Bounds getBounds() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Gets spatial capabilities of the activity. Unlike other capabilities in Android, this may
+ * dynamically change based on the current mode the activity is in, whether the activity is the
+ * top one in its task, whether the task is the top visible one on the desktop, and so on.
+ *
+ * @see androidx.xr.extensions.space.SpatialCapabilities
+ */
+ public com.android.extensions.xr.space.SpatialCapabilities getSpatialCapabilities() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Gets the environment visibility of the activity.
+ *
+ * @see androidx.xr.extensions.environment.EnvironmentVisibilityState
+ */
+ public com.android.extensions.xr.environment.EnvironmentVisibilityState
+ getEnvironmentVisibility() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Gets the passthrough visibility of the activity.
+ *
+ * @see androidx.xr.extensions.environment.PassthroughVisibilityState
+ */
+ public com.android.extensions.xr.environment.PassthroughVisibilityState
+ getPassthroughVisibility() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * True if the scene node that is currently in use (if any) is the same as targetNode. When
+ * targetNode is null, this API returns true when no scene node is currently in use (i.e. the
+ * activity is not SPATIAL_UI_CAPABLE, the activity hasn't called attachSpatialScene API at all,
+ * or the activity hasn't called it again since the last detachSpatialScene API call.)
+ *
+ * @see androidx.xr.extensions.attachSpatialScene
+ */
+ public boolean isActiveSceneNode(com.android.extensions.xr.node.Node targetNode) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * True if the window leash node that is currently in use (if any) is the same as targetNode.
+ * When targetNode is null, this API returns true when no window leash node is currently in use
+ * (i.e. the activity is not SPATIAL_UI_CAPABLE, the activity hasn't called attachSpatialScene
+ * API at all, or the activity hasn't called it again since the last detachSpatialScene API
+ * call.)
+ *
+ * @see androidx.xr.extensions.attachSpatialScene
+ */
+ public boolean isActiveWindowLeashNode(com.android.extensions.xr.node.Node targetNode) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * True if the environment node that is currently in use (if any) is the same as targetNode.
+ * When targetNode is null, this API returns true when no environment node is currently in use
+ * (i.e. the activity is not APP_ENVIRONMENTS_CAPABLE, the activity hasn't called
+ * attachSpatialEnvironment API at all, or the activity hasn't called it again since the last
+ * detachSpatialEnvironment API call.)
+ *
+ * <p>Note that as a special case, when isEnvironmentInherited() is true, the API returns false
+ * for a null targetNode even if your activity hasn't called attachSpatialEnvironment yet.
+ *
+ * @see androidx.xr.extensions.attachSpatialEnvironment
+ * @see androidx.xr.extensions.setFullSpaceModeWithEnvironmentInherited
+ */
+ public boolean isActiveEnvironmentNode(com.android.extensions.xr.node.Node targetNode) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * True if an activity-provided environment node is currently in use, and the node is one
+ * inherited from a different activity.
+ *
+ * @see androidx.xr.extensions.attachSpatialEnvironment
+ * @see androidx.xr.extensions.setFullSpaceModeWithEnvironmentInherited
+ */
+ public boolean isEnvironmentInherited() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Gets the main window size. (0, 0) is returned when the activity is not SPATIAL_UI_CAPABLE.
+ *
+ * <p>When the activity is not SPATIAL_UI_CAPABLE, use android.content.res.Configuration to
+ * obtain the activity's size.
+ *
+ * @see androidx.xr.extensions.setMainWindowSize
+ * @see android.content.res.Configuration
+ */
+ public android.util.Size getMainWindowSize() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /**
+ * Gets the main window's aspect ratio preference. 0.0f is returned when there's no preference
+ * set via setPreferredAspectRatio API, or the activity is currently SPATIAL_UI_CAPABLE.
+ *
+ * <p>When SPATIAL_UI_CAPABLE, activities can set a preferred aspect ratio via ReformOptions,
+ * but that reform options setting won't be reflected to the value returned from this API.
+ *
+ * @see androidx.xr.extensions.setPreferredAspectRatio
+ * @see androidx.xr.extensions.node.ReformOptions
+ */
+ public float getPreferredAspectRatio() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** equals() */
+ public boolean equals(java.lang.Object other) {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** hashCode() */
+ public int hashCode() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** toString() */
+ public java.lang.String toString() {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/SpatialStateEvent.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/SpatialStateEvent.java
new file mode 100644
index 0000000..7f520fb
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/space/SpatialStateEvent.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.space;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Base class for spatial state change events.
+ *
+ * @see androidx.xr.extensions.XrExtensions#setSpatialStateCallback
+ * @deprecated Use SpatialState instead.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@Deprecated
+public interface SpatialStateEvent {}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/splitengine/SplitEngineBridge.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/splitengine/SplitEngineBridge.java
new file mode 100644
index 0000000..fcc89dd
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/splitengine/SplitEngineBridge.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.splitengine;
+
+import androidx.annotation.RestrictTo;
+
+/** Wrapper object around a native SplitEngineBridge. */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SplitEngineBridge {
+
+ SplitEngineBridge() {
+ throw new RuntimeException("Stub!");
+ }
+
+ /** A handle to the shared memory split engine bridge. */
+ public long getNativeHandle() {
+ throw new RuntimeException("Stub!");
+ }
+}
diff --git a/xr/xr-stubs/src/main/java/com/android/extensions/xr/subspace/Subspace.java b/xr/xr-stubs/src/main/java/com/android/extensions/xr/subspace/Subspace.java
new file mode 100644
index 0000000..e8bb053
--- /dev/null
+++ b/xr/xr-stubs/src/main/java/com/android/extensions/xr/subspace/Subspace.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 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
+ *
+ * http://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.android.extensions.xr.subspace;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Handle to a subspace in the system scene graph.
+ *
+ * <p>A subspace by itself does not have any visual representation. It merely defines a local space
+ * in its parent space. Once created, 3D content can be rendered in the hierarchy of that subspace.
+ *
+ * <p>Note that {@link com.android.extensions.xr.subspace.Subspace Subspace} uses a right-hand
+ * coordinate system, i.e. +X points to the right, +Y up, and +Z points towards the camera.
+ */
+@SuppressWarnings({"unchecked", "deprecation", "all"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class Subspace {
+
+ Subspace() {
+ throw new RuntimeException("Stub!");
+ }
+}