Merge "Merge SimpleCSP and FakeCaptureSequenceProcessor into a single class" into androidx-main
diff --git a/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml b/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml
index 65975e0..d8fe4cd 100644
--- a/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml
+++ b/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml
@@ -4,69 +4,6 @@
<issue
id="GradleProjectIsolation"
message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" project.properties["androidx.benchmark.test.maxagpversion"]?.let { str ->"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" it in project.properties && project.properties[it].toString().toBoolean()"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" it in project.properties && project.properties[it].toString().toBoolean()"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" project.properties.containsKey(PROP_SKIP_GENERATION)"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" project.properties.containsKey(PROP_FORCE_ONLY_CONNECTED_DEVICES)"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" !project.properties.containsKey(PROP_DONT_DISABLE_RULES)"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" !project.properties.containsKey(PROP_SEND_TARGET_PACKAGE_NAME)"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
errorLine1=" project.properties.filterKeys { k ->"
errorLine2=" ~~~~~~~~~~">
<location
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
index 239eff6..aa184ec 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
@@ -79,16 +79,16 @@
private val baselineProfileExtension = BaselineProfileProducerExtension.register(project)
private val configurationManager = ConfigurationManager(project)
private val shouldSkipGeneration by lazy {
- project.properties.containsKey(PROP_SKIP_GENERATION)
+ project.providers.gradleProperty(PROP_SKIP_GENERATION).isPresent
}
private val forceOnlyConnectedDevices: Boolean by lazy {
- project.properties.containsKey(PROP_FORCE_ONLY_CONNECTED_DEVICES)
+ project.providers.gradleProperty(PROP_FORCE_ONLY_CONNECTED_DEVICES).isPresent
}
private val addEnabledRulesInstrumentationArgument by lazy {
- !project.properties.containsKey(PROP_DONT_DISABLE_RULES)
+ !project.providers.gradleProperty(PROP_DONT_DISABLE_RULES).isPresent
}
private val addTargetPackageNameInstrumentationArgument by lazy {
- !project.properties.containsKey(PROP_SEND_TARGET_PACKAGE_NAME)
+ !project.providers.gradleProperty(PROP_SEND_TARGET_PACKAGE_NAME).isPresent
}
// This maps all the extended build types to the original ones. Note that release does not
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
index cbd7fa7..29f7c75 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
@@ -51,8 +51,9 @@
// Properties that can be specified by cmd line using -P<property_name> when invoking gradle.
val testMaxAgpVersion by lazy {
- project.properties["androidx.benchmark.test.maxagpversion"]?.let { str ->
- val parts = str.toString().split(".").map { it.toInt() }
+ project.providers.gradleProperty("androidx.benchmark.test.maxagpversion").orNull?.let { str
+ ->
+ val parts = str.split(".").map { it.toInt() }
return@lazy AndroidPluginVersion(parts[0], parts[1], parts[2])
} ?: return@lazy null
}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml b/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml
index 9ce75c2..42b07bb 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml
+++ b/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.6.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.6.0-beta01)" variant="all" version="8.6.0-beta01">
+<issues format="6" by="lint 8.7.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.0-alpha02)" variant="all" version="8.7.0-alpha02">
<issue
id="GradleProjectIsolation"
@@ -11,6 +11,15 @@
</issue>
<issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.projectDir, // frameworks/support"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt"/>
+ </issue>
+
+ <issue
id="WithTypeWithoutConfigureEach"
message="Avoid passing a closure to withType, use withType().configureEach instead"
errorLine1=" project.plugins.withType(KotlinMultiplatformPluginWrapper::class.java) {"
diff --git a/benchmark/benchmark-junit4/proguard-rules.pro b/benchmark/benchmark-junit4/proguard-rules.pro
index 544c831..621d897 100644
--- a/benchmark/benchmark-junit4/proguard-rules.pro
+++ b/benchmark/benchmark-junit4/proguard-rules.pro
@@ -18,6 +18,7 @@
## needed for listeners instantiated by reflection (e.g. InstrumentationResultsRunListener)
-keepclasseswithmembers class * extends androidx.test.internal.runner.listener.InstrumentationRunListener { *; }
+-keepclasseswithmembers class * extends org.junit.runner.notification.RunListener { *; }
## Needed due to b/328649293 - shouldn't be needed since they're ref'd by manifest
## May need to leave these in place long term to account for old gradle versions
diff --git a/benchmark/gradle-plugin/lint-baseline.xml b/benchmark/gradle-plugin/lint-baseline.xml
index 4363bf5..8c91fdc 100644
--- a/benchmark/gradle-plugin/lint-baseline.xml
+++ b/benchmark/gradle-plugin/lint-baseline.xml
@@ -3,27 +3,36 @@
<issue
id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" if (!project.findProperty(ADDITIONAL_TEST_OUTPUT_KEY).toString().toBoolean()) {"
- errorLine2=" ~~~~~~~~~~~~">
+ message="Avoid using method getRootProject"
+ errorLine1=" if (!project.rootProject.tasks.exists("lockClocks")) {"
+ errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
</issue>
<issue
id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" project.findProperty("androidx.benchmark.lockClocks.cores")?.toString() ?: """
- errorLine2=" ~~~~~~~~~~~~">
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks.register("lockClocks", LockClocksTask::class.java).configure {"
+ errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
</issue>
<issue
id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" if (!project.properties[ADDITIONAL_TEST_OUTPUT_KEY].toString().toBoolean()) {"
- errorLine2=" ~~~~~~~~~~">
+ message="Avoid using method getRootProject"
+ errorLine1=" if (!project.rootProject.tasks.exists("unlockClocks")) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks"
+ errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
</issue>
diff --git a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
index a7bd8e3..8b2eb89 100644
--- a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
+++ b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
@@ -101,14 +101,19 @@
extension.buildTypes.named(testBuildType).configure { it.isDefault = true }
if (
- !project.rootProject.hasProperty("android.injected.invoked.from.ide") &&
+ !project.providers.gradleProperty("android.injected.invoked.from.ide").isPresent &&
!testInstrumentationArgs.containsKey("androidx.benchmark.output.enable")
) {
// NOTE: This argument is checked by ResultWriter to enable CI reports.
defaultConfig.testInstrumentationRunnerArguments["androidx.benchmark.output.enable"] =
"true"
- if (!project.findProperty(ADDITIONAL_TEST_OUTPUT_KEY).toString().toBoolean()) {
+ if (
+ !project.providers
+ .gradleProperty(ADDITIONAL_TEST_OUTPUT_KEY)
+ .getOrElse("false")
+ .toBoolean()
+ ) {
defaultConfig.testInstrumentationRunnerArguments["no-isolated-storage"] = "1"
}
}
@@ -119,7 +124,9 @@
project.rootProject.tasks.register("lockClocks", LockClocksTask::class.java).configure {
it.adbPath.set(adbPathProvider)
it.coresArg.set(
- project.findProperty("androidx.benchmark.lockClocks.cores")?.toString() ?: ""
+ project.providers
+ .gradleProperty("androidx.benchmark.lockClocks.cores")
+ .orElse("")
)
}
}
@@ -159,7 +166,12 @@
project.layout.buildDirectory.dir(
"outputs/connected_android_test_additional_output"
)
- if (!project.properties[ADDITIONAL_TEST_OUTPUT_KEY].toString().toBoolean()) {
+ if (
+ !project.providers
+ .gradleProperty(ADDITIONAL_TEST_OUTPUT_KEY)
+ .getOrElse("false")
+ .toBoolean()
+ ) {
// Only enable pulling benchmark data through this plugin on older versions of
// AGP that do not yet enable this flag.
project.tasks
diff --git a/buildSrc-tests/lint-baseline.xml b/buildSrc-tests/lint-baseline.xml
index 235650f..656e12c 100644
--- a/buildSrc-tests/lint-baseline.xml
+++ b/buildSrc-tests/lint-baseline.xml
@@ -138,6 +138,69 @@
<issue
id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val extensions = project.rootProject.extensions"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/dependencyTracker/AffectedModuleDetector.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val compilerProject = project.rootProject.resolveProject(":compose")"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks.named(zipComposeMetricsTaskName).configure { zipTask ->"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks.named(zipComposeReportsTaskName).configure { zipTask ->"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" return project.rootProject.layout.buildDirectory"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" return project.rootProject.layout.buildDirectory"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" return File(rootProject.projectDir, "../../external").canonicalFile"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/AndroidXConfig.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
message="Use providers.gradleProperty instead of getProperties"
errorLine1=" for (propertyName in project.properties.keys) {"
errorLine2=" ~~~~~~~~~~">
@@ -147,11 +210,146 @@
<issue
id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" if (properties.containsKey("android.injected.invoked.from.ide")) {"
- errorLine2=" ~~~~~~~~~~">
+ message="Avoid using method getRootProject"
+ errorLine1=" AndroidXPlaygroundRootImplPlugin.projectOrArtifact(rootProject, this)"
+ errorLine2=" ~~~~~~~~~~~">
<location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXRootImplPlugin.kt"/>
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks.named("createModuleInfo").configure {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method findProject"
+ errorLine1=" allProjectsExist || findProject(otherGradlePath) != null"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.rootDir == project.getSupportRootFolder()"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.extensions.findByType<NodeJsRootExtension>()?.version = getVersionByName("node")"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.extensions.findByType(YarnRootExtension::class.java)?.let {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks.register("createYarnRcFile", CreateYarnRcFileTask::class.java) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" it.yarnrcFile.set(rootProject.layout.buildDirectory.file("js/.yarnrc"))"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks.withType<KotlinNpmInstallTask>().configureEach {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method findProject"
+ errorLine1=" val requested = rootProject.findProject(path)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXPlaygroundRootImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks.named(NAME).configure { it.dependsOn(task) }"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXPlaygroundRootImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.layout.buildDirectory.dir("test-xml-configs")"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/BuildServerConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.layout.buildDirectory.dir("privacysandbox-files")"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/BuildServerConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val actualRootProject = if (project.isRoot) project else project.rootProject"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/BuildServerConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks.named(CREATE_AGGREGATE_BUILD_INFO_FILES_TASK).configure {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/buildInfo/CreateAggregateLibraryBuildInfoFileTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" get() = this == rootProject"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/gradle/Extensions.kt"/>
</issue>
<issue
@@ -174,6 +372,15 @@
<issue
id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks.named(GLOBAL_TASK_NAME).configure { task ->"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/FilteredAnchorTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
message="Use providers.gradleProperty instead of getProperties"
errorLine1=" task.pathPrefix = properties[PROP_PATH_PREFIX] as String"
errorLine2=" ~~~~~~~~~~">
@@ -191,6 +398,213 @@
</issue>
<issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" it.parameters.workingDir.set(rootProject.layout.projectDirectory)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/gitclient/GitClient.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getParent"
+ errorLine1=" ${project.parent}."
+ errorLine2=" ~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/java/JavaCompileInputs.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val rootBaseDir = if (compilerDaemonDisabled) projectDir else rootProject.projectDir"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/KonanPrebuiltsSetup.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method findProject"
+ errorLine1=" return project.rootProject.findProject(path)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" return project.rootProject.findProject(path)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method findProject"
+ errorLine1=" project.rootProject.findProject(":lint:lint-gradle")?.let {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.findProject(":lint:lint-gradle")?.let {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val tasksByOutput = project.rootProject.findAllTasksByOutput()"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/ListTaskOutputsTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method findProject"
+ errorLine1=" project.findProject(projectPath)?.plugins?.let { plugins ->"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/MavenUploadHelper.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method 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="Avoid using method getRootProject"
+ errorLine1=" regenerate(project.rootProject, groupId, artifactId, artifactVersion, location)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/metalava/RegenerateOldApisTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" regenerate(project.rootProject, groupId, artifactId, version, location)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/metalava/RegenerateOldApisTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.maybeRegister("
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/Release.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.getRepositoryDirectory()"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/Release.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" return project.rootProject.maybeRegister("
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/Release.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val localPropsFile = rootProject.projectDir.resolve("local.properties")"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/SdkHelper.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.rootDir.toRelativeString(project.projectDir)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/SdkResourceGenerator.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" (project.rootProject.extensions.extraProperties).let { it.get("supportRootFolder") as File }"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/studio/StudioTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks.named<ModuleInfoGenerator>("createModuleInfo").configure {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getParent"
+ errorLine1=" val parentProject = project.parent!!"
+ errorLine2=" ~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks.findByName(FINALIZE_TEST_CONFIGS_WITH_APKS_TASK)!!.dependsOn(task)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks.named<ModuleInfoGenerator>("createModuleInfo").configure {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+ </issue>
+
+ <issue
id="InternalAgpApiUsage"
message="Avoid using internal Android Gradle Plugin APIs"
errorLine1="import com.android.build.gradle.internal.lint.AndroidLintAnalysisTask"
@@ -382,8 +796,8 @@
<issue
id="WithPluginClasspathUsage"
message="Avoid usage of GradleRunner#withPluginClasspath, which is broken. Instead use something like https://github.com/autonomousapps/dependency-analysis-gradle-plugin/tree/main/testkit#gradle-testkit-support-plugin"
- errorLine1=" .withPluginClasspath()"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ errorLine1=" .withPluginClasspath()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
file="src/test/java/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt"/>
</issue>
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
index e777875..ba8881f 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -61,7 +61,7 @@
// If we're running inside Studio, validate the Android Gradle Plugin version.
val expectedAgpVersion = System.getenv("EXPECTED_AGP_VERSION")
- if (properties.containsKey("android.injected.invoked.from.ide")) {
+ if (providers.gradleProperty("android.injected.invoked.from.ide").isPresent) {
if (expectedAgpVersion != ANDROID_GRADLE_PLUGIN_VERSION) {
throw GradleException(
"""
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt
index f632090..73aca82 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt
@@ -62,7 +62,10 @@
* match the requested path prefix and task name.
*/
internal fun Project.addFilterableTasks(vararg taskProviders: TaskProvider<*>?) {
- if (hasProperty(PROP_PATH_PREFIX) && hasProperty(PROP_TASK_NAME)) {
+ if (
+ providers.gradleProperty(PROP_PATH_PREFIX).isPresent &&
+ providers.gradleProperty(PROP_TASK_NAME).isPresent
+ ) {
val pathPrefixes = (properties[PROP_PATH_PREFIX] as String).split(",")
if (pathPrefixes.any { pathPrefix -> relativePathForFiltering().startsWith(pathPrefix) }) {
val taskName = properties[PROP_TASK_NAME] as String
@@ -84,7 +87,10 @@
* -Pandroidx.taskName=checkApi -Pandroidx.pathPrefix=core/core/
*/
internal fun Project.maybeRegisterFilterableTask() {
- if (hasProperty(PROP_TASK_NAME) && hasProperty(PROP_PATH_PREFIX)) {
+ if (
+ providers.gradleProperty(PROP_TASK_NAME).isPresent &&
+ providers.gradleProperty(PROP_PATH_PREFIX).isPresent
+ ) {
tasks.register(GLOBAL_TASK_NAME, FilteredAnchorTask::class.java) { task ->
task.pathPrefix = properties[PROP_PATH_PREFIX] as String
task.taskName = properties[PROP_TASK_NAME] as String
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
index b6e55c4..eace746 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
@@ -30,7 +30,6 @@
import androidx.camera.camera2.pipe.integration.impl.StillCaptureRequestControl
import androidx.camera.camera2.pipe.integration.impl.TorchControl
import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
-import androidx.camera.camera2.pipe.integration.impl.UseCaseThreads
import androidx.camera.camera2.pipe.integration.impl.ZoomControl
import androidx.camera.camera2.pipe.integration.interop.Camera2CameraControl
import androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions
@@ -49,8 +48,8 @@
import androidx.camera.core.impl.utils.futures.Futures
import com.google.common.util.concurrent.ListenableFuture
import javax.inject.Inject
+import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.async
/**
* Adapt the [CameraControlInternal] interface to [CameraPipe].
@@ -71,7 +70,6 @@
private val focusMeteringControl: FocusMeteringControl,
private val stillCaptureRequestControl: StillCaptureRequestControl,
private val torchControl: TorchControl,
- private val threads: UseCaseThreads,
private val zoomControl: ZoomControl,
private val zslControl: ZslControl,
public val camera2cameraControl: Camera2CameraControl,
@@ -112,11 +110,10 @@
override fun cancelFocusAndMetering(): ListenableFuture<Void> {
return Futures.nonCancellationPropagating(
- threads.sequentialScope
- .async {
- focusMeteringControl.cancelFocusAndMeteringAsync().join()
+ CompletableDeferred<Void?>()
+ .also {
// Convert to null once the task is done, ignore the results.
- return@async null
+ focusMeteringControl.cancelFocusAndMeteringAsync().propagateTo(it) { null }
}
.asListenableFuture()
)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt
index 76d7012..5353105 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt
@@ -74,21 +74,48 @@
return CallbackToFutureAdapter.getFuture(resolver)
}
+/**
+ * Propagates the result of this to `destination` parameter when this deferred is completed.
+ *
+ * Cancelling the destination is no-op returned from this function does not cancel the `Deferred`
+ * returned by `block`.
+ */
public fun <T> Deferred<T>.propagateTo(destination: CompletableDeferred<T>) {
- invokeOnCompletion { propagateOnceTo(destination, it) }
+ invokeOnCompletion { propagateCompletion(destination, it) }
}
-@OptIn(ExperimentalCoroutinesApi::class)
-public fun <T> Deferred<T>.propagateOnceTo(
- destination: CompletableDeferred<T>,
- throwable: Throwable?,
+/**
+ * Propagates the result of this to `destination` parameter when this deferred is completed.
+ *
+ * Cancelling the destination is no-op returned from this function does not cancel the `Deferred`
+ * returned by `block`.
+ *
+ * @param destination The destination [CompletableDeferred] to which result is propagated to.
+ * @param transform Transformation function to convert the result during propagation.
+ */
+public fun <T, R> Deferred<T>.propagateTo(
+ destination: CompletableDeferred<R>,
+ transform: (T) -> R,
) {
- if (throwable != null) {
- if (throwable is CancellationException) {
- destination.cancel(throwable)
- } else {
- destination.completeExceptionally(throwable)
- }
+ invokeOnCompletion { propagateCompletion(destination, it, transform) }
+}
+
+/**
+ * Propagates the result of this to `destination` parameter immediately.
+ *
+ * This function assumes that [Deferred.invokeOnCompletion] has already been invoked.
+ *
+ * @param destination The destination `Deferred` to which result is propagated to.
+ * @param completionCause The `Throwable` cause of completion that was passed in
+ * `Deferred.invokeOnCompletion`.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+public fun <T> Deferred<T>.propagateCompletion(
+ destination: CompletableDeferred<T>,
+ completionCause: Throwable?,
+) {
+ if (completionCause != null) {
+ destination.completeFailing(completionCause)
} else {
// Ignore exceptions - This should never throw in this situation.
destination.complete(getCompleted())
@@ -96,6 +123,46 @@
}
/**
+ * Propagates the result of this to `destination` parameter immediately.
+ *
+ * This function assumes that [Deferred.invokeOnCompletion] has already been invoked.
+ *
+ * @param destination The destination `Deferred` to which result is propagated to.
+ * @param completionCause The `Throwable` cause of completion that was passed in
+ * `Deferred.invokeOnCompletion`.
+ * @param transform Transformation function to convert the result during propagation.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+public fun <T, R> Deferred<T>.propagateCompletion(
+ destination: CompletableDeferred<R>,
+ completionCause: Throwable?,
+ transform: (T) -> R,
+) {
+ if (completionCause != null) {
+ destination.completeFailing(completionCause)
+ } else {
+ // Ignore exceptions - This should never throw in this situation.
+ destination.complete(transform(getCompleted()))
+ }
+}
+
+/**
+ * Completes this `Deferred` as failure based on the provided `cause`.
+ *
+ * @param cause If it's an instance of [CancellationException], [Deferred.cancel] is invoked for
+ * this, otherwise, [CompletableDeferred.completeExceptionally] is invoked.
+ */
+public fun <T> CompletableDeferred<T>.completeFailing(
+ cause: Throwable,
+) {
+ if (cause is CancellationException) {
+ cancel(cause)
+ } else {
+ completeExceptionally(cause)
+ }
+}
+
+/**
* Waits for [Deferred.await] to be completed until the given timeout.
*
* @return true if `Deferred.await` had completed, false otherwise.
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
index 82da040..2080cb8 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
@@ -45,6 +45,7 @@
import androidx.camera.core.impl.UseCaseConfigFactory
import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.internal.TargetConfig.OPTION_TARGET_NAME
import kotlin.math.min
private val DEFAULT_PREVIEW_SIZE = Size(0, 0)
@@ -203,6 +204,7 @@
OPTION_SESSION_CONFIG_UNPACKER,
CameraUseCaseAdapter.DefaultSessionOptionsUnpacker
)
+ insertOption(OPTION_TARGET_NAME, "MeteringRepeating")
insertOption(OPTION_CAPTURE_TYPE, CaptureType.METERING_REPEATING)
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt
index 7ffef5d..94f03ea 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt
@@ -19,7 +19,7 @@
import androidx.annotation.GuardedBy
import androidx.camera.camera2.pipe.core.Log.debug
import androidx.camera.camera2.pipe.integration.adapter.asListenableFuture
-import androidx.camera.camera2.pipe.integration.adapter.propagateOnceTo
+import androidx.camera.camera2.pipe.integration.adapter.propagateCompletion
import androidx.camera.camera2.pipe.integration.config.CameraScope
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
@@ -197,7 +197,7 @@
}
}
} else {
- propagateOnceTo(submittedRequest.result, cause)
+ propagateCompletion(submittedRequest.result, cause)
}
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index 54adf25..cedc7c4 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -17,6 +17,7 @@
package androidx.camera.camera2.pipe.integration.impl
import android.content.Context
+import android.graphics.ImageFormat
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW
import android.hardware.camera2.CaptureRequest
@@ -55,23 +56,30 @@
import androidx.camera.camera2.pipe.integration.config.UseCaseCameraComponent
import androidx.camera.camera2.pipe.integration.config.UseCaseCameraConfig
import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
+import androidx.camera.camera2.pipe.integration.internal.DynamicRangeResolver
import androidx.camera.camera2.pipe.integration.interop.Camera2CameraControl
import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
import androidx.camera.core.DynamicRange
+import androidx.camera.core.ImageCapture
import androidx.camera.core.MirrorMode
+import androidx.camera.core.Preview
import androidx.camera.core.UseCase
+import androidx.camera.core.impl.AttachedSurfaceInfo
import androidx.camera.core.impl.CameraControlInternal
import androidx.camera.core.impl.CameraInfoInternal
import androidx.camera.core.impl.CameraInternal
import androidx.camera.core.impl.CameraMode
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.DeferrableSurface
-import androidx.camera.core.impl.PreviewConfig
+import androidx.camera.core.impl.MutableOptionsBundle
import androidx.camera.core.impl.SessionConfig
import androidx.camera.core.impl.SessionConfig.OutputConfig.SURFACE_GROUP_ID_NONE
import androidx.camera.core.impl.SessionConfig.ValidatingBuilder
import androidx.camera.core.impl.SessionProcessor
+import androidx.camera.core.impl.SurfaceConfig
import androidx.camera.core.impl.stabilization.StabilizationMode
+import androidx.camera.core.streamsharing.StreamSharing
+import androidx.camera.core.streamsharing.StreamSharingConfig
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.Deferred
@@ -171,6 +179,8 @@
)
}
+ private val dynamicRangeResolver = DynamicRangeResolver(cameraProperties.metadata)
+
@Volatile private var _activeComponent: UseCaseCameraComponent? = null
public val camera: UseCaseCamera?
get() = _activeComponent?.getUseCaseCamera()
@@ -602,7 +612,7 @@
return activeSurfaces > 0 &&
with(attachedUseCases.withoutMetering()) {
(onlyVideoCapture() || requireMeteringRepeating()) &&
- supportMeteringCombination()
+ isMeteringCombinationSupported()
}
}
return false
@@ -624,7 +634,7 @@
return activeSurfaces == 0 ||
with(attachedUseCases.withoutMetering()) {
!(onlyVideoCapture() || requireMeteringRepeating()) ||
- !supportMeteringCombination()
+ !isMeteringCombinationSupported()
}
}
return false
@@ -664,46 +674,133 @@
}
}
- private fun Collection<UseCase>.supportMeteringCombination(): Boolean {
- val useCases = this.toMutableList().apply { add(meteringRepeating) }
+ private fun Collection<UseCase>.isMeteringCombinationSupported(): Boolean {
if (meteringRepeating.attachedSurfaceResolution == null) {
meteringRepeating.setupSession()
}
- return isCombinationSupported(useCases).also {
- Log.debug { "Combination of $useCases is supported: $it" }
+
+ val attachedSurfaceInfoList = getAttachedSurfaceInfoList()
+
+ if (attachedSurfaceInfoList.isEmpty()) {
+ return false
}
+
+ val sessionSurfacesConfigs = getSessionSurfacesConfigs()
+
+ return supportedSurfaceCombination
+ .checkSupported(
+ SupportedSurfaceCombination.FeatureSettings(
+ CameraMode.DEFAULT,
+ getRequiredMaxBitDepth(attachedSurfaceInfoList),
+ isPreviewStabilizationOn(),
+ isUltraHdrOn()
+ ),
+ mutableListOf<SurfaceConfig>().apply {
+ addAll(sessionSurfacesConfigs)
+ add(createMeteringRepeatingSurfaceConfig())
+ }
+ )
+ .also {
+ Log.debug {
+ "Combination of $sessionSurfacesConfigs + $meteringRepeating is supported: $it"
+ }
+ }
}
- private fun isCombinationSupported(currentUseCases: Collection<UseCase>): Boolean {
- val surfaceConfigs =
- currentUseCases.map { useCase ->
- // TODO: Test with correct Camera Mode when concurrent mode / ultra high resolution
- // is
- // implemented.
- supportedSurfaceCombination.transformSurfaceConfig(
- CameraMode.DEFAULT,
- useCase.imageFormat,
- useCase.attachedSurfaceResolution!!
+ private fun getRequiredMaxBitDepth(attachedSurfaceInfoList: List<AttachedSurfaceInfo>): Int {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ dynamicRangeResolver
+ .resolveAndValidateDynamicRanges(
+ attachedSurfaceInfoList,
+ listOf(meteringRepeating.currentConfig),
+ listOf(0)
+ )
+ .forEach { (_, u) ->
+ if (u.bitDepth == DynamicRange.BIT_DEPTH_10_BIT) {
+ return DynamicRange.BIT_DEPTH_10_BIT
+ }
+ }
+ }
+
+ return DynamicRange.BIT_DEPTH_8_BIT
+ }
+
+ private fun Collection<UseCase>.getAttachedSurfaceInfoList(): List<AttachedSurfaceInfo> =
+ mutableListOf<AttachedSurfaceInfo>().apply {
+ [email protected] { useCase ->
+ val surfaceResolution = useCase.attachedSurfaceResolution
+ val streamSpec = useCase.attachedStreamSpec
+
+ // When collecting the info, the UseCases might be unbound to make these info
+ // become null.
+ if (surfaceResolution == null || streamSpec == null) {
+ Log.warn { "Invalid surface resolution or stream spec is found." }
+ clear()
+ return@apply
+ }
+
+ val surfaceConfig =
+ supportedSurfaceCombination.transformSurfaceConfig(
+ // TODO: Test with correct Camera Mode when concurrent mode / ultra high
+ // resolution is implemented.
+ CameraMode.DEFAULT,
+ useCase.currentConfig.inputFormat,
+ surfaceResolution
+ )
+ add(
+ AttachedSurfaceInfo.create(
+ surfaceConfig,
+ useCase.currentConfig.inputFormat,
+ surfaceResolution,
+ streamSpec.dynamicRange,
+ useCase.getCaptureTypes(),
+ streamSpec.implementationOptions ?: MutableOptionsBundle.create(),
+ useCase.currentConfig.getTargetFrameRate(null)
+ )
)
}
+ }
- var isPreviewStabilizationOn = false
- for (useCase in currentUseCases) {
- if (useCase.currentConfig is PreviewConfig) {
- isPreviewStabilizationOn =
- useCase.currentConfig.previewStabilizationMode == StabilizationMode.ON
+ private fun UseCase.getCaptureTypes() =
+ if (this is StreamSharing) {
+ (currentConfig as StreamSharingConfig).captureTypes
+ } else {
+ listOf(currentConfig.captureType)
+ }
+
+ private fun Collection<UseCase>.isPreviewStabilizationOn() =
+ filterIsInstance<Preview>().firstOrNull()?.currentConfig?.previewStabilizationMode ==
+ StabilizationMode.ON
+
+ private fun Collection<UseCase>.isUltraHdrOn() =
+ filterIsInstance<ImageCapture>().firstOrNull()?.currentConfig?.inputFormat ==
+ ImageFormat.JPEG_R
+
+ private fun Collection<UseCase>.getSessionSurfacesConfigs(): List<SurfaceConfig> =
+ mutableListOf<SurfaceConfig>().apply {
+ [email protected] { useCase ->
+ useCase.sessionConfig.surfaces.forEach { deferrableSurface ->
+ add(
+ supportedSurfaceCombination.transformSurfaceConfig(
+ // TODO: Test with correct Camera Mode when concurrent mode / ultra high
+ // resolution is implemented.
+ CameraMode.DEFAULT,
+ useCase.currentConfig.inputFormat,
+ deferrableSurface.prescribedSize
+ )
+ )
+ }
}
}
- return supportedSurfaceCombination.checkSupported(
- SupportedSurfaceCombination.FeatureSettings(
- CameraMode.DEFAULT,
- DynamicRange.BIT_DEPTH_8_BIT,
- isPreviewStabilizationOn
- ),
- surfaceConfigs
+ private fun createMeteringRepeatingSurfaceConfig() =
+ supportedSurfaceCombination.transformSurfaceConfig(
+ // TODO: Test with correct Camera Mode when concurrent mode / ultra high resolution is
+ // implemented.
+ CameraMode.DEFAULT,
+ meteringRepeating.imageFormat,
+ meteringRepeating.attachedSurfaceResolution!!
)
- }
private fun Collection<UseCase>.surfaceCount(): Int =
ValidatingBuilder().let { validatingBuilder ->
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapterTest.kt
index 9b27a11..28b47b6 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapterTest.kt
@@ -46,6 +46,23 @@
}
@Test
+ fun propagateTransformedCompleteResult(): Unit = runBlocking {
+ // Arrange.
+ val resultValue = 123
+ val resultValueTransformed = resultValue.toString()
+
+ val sourceDeferred = CompletableDeferred<Int>()
+ val resultDeferred = CompletableDeferred<String>()
+ sourceDeferred.propagateTo(resultDeferred) { res -> res.toString() }
+
+ // Act.
+ sourceDeferred.complete(resultValue)
+
+ // Assert.
+ assertThat(resultDeferred.await()).isEqualTo(resultValueTransformed)
+ }
+
+ @Test
fun propagateCancelResult() {
// Arrange.
val sourceDeferred = CompletableDeferred<Unit>()
@@ -59,6 +76,20 @@
assertThat(resultDeferred.isCancelled).isTrue()
}
+ @Test
+ fun propagateCancelResult_whenTransformFunctionIsUsed() {
+ // Arrange.
+ val sourceDeferred = CompletableDeferred<Unit>()
+ val resultDeferred = CompletableDeferred<Unit>()
+ sourceDeferred.propagateTo(resultDeferred) { res -> res.toString() }
+
+ // Act.
+ sourceDeferred.cancel()
+
+ // Assert.
+ assertThat(resultDeferred.isCancelled).isTrue()
+ }
+
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun propagateExceptionResult() {
@@ -74,4 +105,20 @@
// Assert.
assertThat(resultDeferred.getCompletionExceptionOrNull()).isSameInstanceAs(testThrowable)
}
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun propagateExceptionResult_whenTransformFunctionIsUsed() {
+ // Arrange.
+ val sourceDeferred = CompletableDeferred<Unit>()
+ val resultDeferred = CompletableDeferred<Unit>()
+ sourceDeferred.propagateTo(resultDeferred) { res -> res.toString() }
+ val testThrowable = Throwable()
+
+ // Act.
+ sourceDeferred.completeExceptionally(testThrowable)
+
+ // Assert.
+ assertThat(resultDeferred.getCompletionExceptionOrNull()).isSameInstanceAs(testThrowable)
+ }
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
index 37d015b..89e2eb6 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
@@ -228,6 +228,67 @@
}
@Test
+ fun meteringRepeatingEnabled_whenPreviewEnabledWithNoSurfaceProvider() = runTest {
+ // Arrange
+ initializeUseCaseThreads(this)
+ val useCaseManager = createUseCaseManager()
+ val preview = createPreview(/* withSurfaceProvider= */ false)
+ val imageCapture = createImageCapture()
+ useCaseManager.attach(listOf(preview, imageCapture))
+
+ // Act
+ useCaseManager.activate(preview)
+ useCaseManager.activate(imageCapture)
+
+ // Assert
+ val enabledUseCaseClasses =
+ useCaseManager.getRunningUseCasesForTest().map { it::class.java }
+ assertThat(enabledUseCaseClasses)
+ .containsExactly(
+ Preview::class.java,
+ ImageCapture::class.java,
+ MeteringRepeating::class.java
+ )
+ }
+
+ @Test
+ fun meteringRepeatingNotEnabled_whenImageAnalysisAndPreviewWithNoSurfaceProvider() = runTest {
+ // Arrange
+ initializeUseCaseThreads(this)
+ val useCaseManager = createUseCaseManager()
+ val preview = createPreview(/* withSurfaceProvider= */ false)
+ val imageAnalysis =
+ ImageAnalysis.Builder().build().apply {
+ setAnalyzer(useCaseThreads.backgroundExecutor) { image -> image.close() }
+ }
+ useCaseManager.attach(listOf(preview, imageAnalysis))
+
+ // Act
+ useCaseManager.activate(preview)
+ useCaseManager.activate(imageAnalysis)
+
+ // Assert
+ val enabledUseCases = useCaseManager.getRunningUseCasesForTest()
+ assertThat(enabledUseCases).containsExactly(preview, imageAnalysis)
+ }
+
+ @Test
+ fun meteringRepeatingNotEnabled_whenOnlyPreviewWithNoSurfaceProvider() = runTest {
+ // Arrange
+ initializeUseCaseThreads(this)
+ val useCaseManager = createUseCaseManager()
+ val preview = createPreview(/* withSurfaceProvider= */ false)
+ useCaseManager.attach(listOf(preview))
+
+ // Act
+ useCaseManager.activate(preview)
+
+ // Assert
+ val enabledUseCases = useCaseManager.getRunningUseCasesForTest()
+ assertThat(enabledUseCases).containsExactly(preview)
+ }
+
+ @Test
fun meteringRepeatingEnabled_whenOnlyImageCaptureEnabled() = runTest {
// Arrange
initializeUseCaseThreads(this)
@@ -736,16 +797,18 @@
useCaseList.add(it)
}
- private fun createPreview(): Preview =
+ private fun createPreview(withSurfaceProvider: Boolean = true): Preview =
Preview.Builder()
.setCaptureOptionUnpacker(CameraUseCaseAdapter.DefaultCaptureOptionsUnpacker.INSTANCE)
.setSessionOptionUnpacker(CameraUseCaseAdapter.DefaultSessionOptionsUnpacker)
.build()
.apply {
- setSurfaceProvider(
- CameraXExecutors.mainThreadExecutor(),
- SurfaceTextureProvider.createSurfaceTextureProvider()
- )
+ if (withSurfaceProvider) {
+ setSurfaceProvider(
+ CameraXExecutors.mainThreadExecutor(),
+ SurfaceTextureProvider.createSurfaceTextureProvider()
+ )
+ }
}
.also {
it.simulateActivation()
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt
index 9ed683c..5d5ee33 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt
@@ -40,7 +40,7 @@
}
public class CameraAvailable(public val cameraId: CameraId) : CameraStatus() {
- override fun toString(): String = "CameraAvailable(camera=$cameraId"
+ override fun toString(): String = "CameraAvailable(camera=$cameraId)"
}
}
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
index 16a26fd..efe9f68 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
@@ -176,7 +176,7 @@
ControllerState.ERROR ->
if (
cameraStatus is CameraStatus.CameraAvailable &&
- lastCameraError == CameraError.ERROR_CAMERA_DEVICE
+ lastCameraError != CameraError.ERROR_GRAPH_CONFIG
) {
shouldRestart = true
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt
index 8039abf..f7532e5 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt
@@ -55,9 +55,19 @@
} catch (e: Exception) {
Log.warn { "Unexpected error: " + e.message }
when (e) {
+ is CameraAccessException -> {
+ cameraErrorListener.onCameraError(
+ cameraId,
+ CameraError.from(e),
+ // CameraAccessException indicates the task failed because the camera is
+ // unavailable, such as when the camera is in use or disconnected. Such errors
+ // can be recovered when the camera becomes available.
+ willAttemptRetry = true,
+ )
+ return null
+ }
is IllegalArgumentException,
is IllegalStateException,
- is CameraAccessException,
is SecurityException,
is UnsupportedOperationException,
is NullPointerException -> {
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
index 263b7a5..b596010 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
@@ -124,7 +124,7 @@
FakeCameraControl fakeCameraControl =
getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
- fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
// Notify the cancel after the capture request has been successfully submitted
fakeCameraControl.notifyAllRequestsOnCaptureCancelled();
});
@@ -154,7 +154,7 @@
ImageCapture.OnImageCapturedCallback.class);
FakeCameraControl fakeCameraControl =
getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
- fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
// Notify the failure after the capture request has been successfully submitted
fakeCameraControl.notifyAllRequestsOnCaptureFailed();
});
@@ -302,7 +302,7 @@
getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
// Simulates the case that the capture request failed after running in 300 ms.
- fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
CameraXExecutors.mainThreadExecutor().schedule(() -> {
fakeCameraControl.notifyAllRequestsOnCaptureFailed();
}, 300, TimeUnit.MILLISECONDS);
@@ -395,7 +395,7 @@
getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
FakeCameraControl.OnNewCaptureRequestListener mockCaptureRequestListener =
mock(FakeCameraControl.OnNewCaptureRequestListener.class);
- fakeCameraControl.setOnNewCaptureRequestListener(mockCaptureRequestListener);
+ fakeCameraControl.addOnNewCaptureRequestListener(mockCaptureRequestListener);
// Act.
mInstrumentation.runOnMainSync(
@@ -463,7 +463,7 @@
FakeCameraControl fakeCameraControl =
getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
CountDownLatch latch = new CountDownLatch(1);
- fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
latch.countDown();
});
@@ -492,7 +492,7 @@
private void addExtraFailureNotificationsForRetry(FakeCameraControl cameraControl,
int retryCount) {
if (retryCount > 0) {
- cameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ cameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
addExtraFailureNotificationsForRetry(cameraControl, retryCount - 1);
cameraControl.notifyAllRequestsOnCaptureFailed();
});
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.kt b/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.kt
index a0557a5..94fa6ae 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.kt
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.kt
@@ -18,7 +18,6 @@
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope
import androidx.lifecycle.LifecycleOwner
-import com.google.common.util.concurrent.ListenableFuture
/**
* A [CameraProvider] provides basic access to a set of cameras such as querying for camera
@@ -92,12 +91,4 @@
public fun getCameraInfo(cameraSelector: CameraSelector): CameraInfo {
throw UnsupportedOperationException("The camera provider is not implemented properly.")
}
-
- /**
- * Shuts down the camera provider.
- *
- * @return A [ListenableFuture] representing the shutdown status. Cancellation of this future is
- * a no-op.
- */
- @RestrictTo(Scope.LIBRARY_GROUP) public fun shutdownAsync(): ListenableFuture<Void>
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 20b2ecd..0873c4b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -1312,7 +1312,8 @@
if (mTakePictureManager == null) {
// mTakePictureManager is reused when the Surface is reset.
- mTakePictureManager = new TakePictureManager(mImageCaptureControl);
+ mTakePictureManager = getCurrentConfig().getTakePictureManagerProvider().newInstance(
+ mImageCaptureControl);
}
mTakePictureManager.setImagePipeline(mImagePipeline);
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 b81133b..62245fd 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
@@ -49,10 +49,11 @@
public final class ImageProcessingUtil {
private static final String TAG = "ImageProcessingUtil";
+ public static final String JNI_LIB_NAME = "image_processing_util_jni";
private static int sImageCount = 0;
static {
- System.loadLibrary("image_processing_util_jni");
+ System.loadLibrary(JNI_LIB_NAME);
}
enum Result {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java
index d4ab73e..ceec2f8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java
@@ -24,6 +24,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.processing.Operation;
import androidx.camera.core.processing.Packet;
@@ -37,7 +38,8 @@
*
* <p>The {@link Bitmap} will be recycled and should not be used after the processing.
*/
-class Bitmap2JpegBytes implements Operation<Bitmap2JpegBytes.In, Packet<byte[]>> {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class Bitmap2JpegBytes implements Operation<Bitmap2JpegBytes.In, Packet<byte[]>> {
@NonNull
@Override
@@ -79,16 +81,16 @@
* Input of {@link Bitmap2JpegBytes} processor.
*/
@AutoValue
- abstract static class In {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public abstract static class In {
abstract Packet<Bitmap> getPacket();
abstract int getJpegQuality();
@NonNull
- static In of(@NonNull Packet<Bitmap> imagePacket, int jpegQuality) {
+ public static In of(@NonNull Packet<Bitmap> imagePacket, int jpegQuality) {
return new AutoValue_Bitmap2JpegBytes_In(imagePacket, jpegQuality);
}
}
}
-
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
index 4dccd13..cb378f1 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
@@ -27,6 +27,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.impl.utils.Exif;
@@ -48,7 +49,9 @@
/**
* Saves JPEG bytes to disk.
*/
-class JpegBytes2Disk implements Operation<JpegBytes2Disk.In, ImageCapture.OutputFileResults> {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class JpegBytes2Disk implements
+ Operation<JpegBytes2Disk.In, ImageCapture.OutputFileResults> {
private static final String TEMP_FILE_PREFIX = "CameraX";
private static final String TEMP_FILE_SUFFIX = ".tmp";
@@ -287,7 +290,8 @@
* Input packet.
*/
@AutoValue
- abstract static class In {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public abstract static class In {
@NonNull
abstract Packet<byte[]> getPacket();
@@ -296,7 +300,7 @@
abstract ImageCapture.OutputFileOptions getOutputFileOptions();
@NonNull
- static In of(@NonNull Packet<byte[]> jpegBytes,
+ public static In of(@NonNull Packet<byte[]> jpegBytes,
@NonNull ImageCapture.OutputFileOptions outputFileOptions) {
return new AutoValue_JpegBytes2Disk_In(jpegBytes, outputFileOptions);
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
index fdf33ed..5b06259 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
@@ -16,36 +16,15 @@
package androidx.camera.core.imagecapture;
-import static androidx.camera.core.ImageCapture.ERROR_CAMERA_CLOSED;
-import static androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED;
-import static androidx.camera.core.impl.utils.Threads.checkMainThread;
-import static androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor;
-import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
-import static androidx.core.util.Preconditions.checkState;
-
-import static java.util.Objects.requireNonNull;
-
-import android.util.Log;
-
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
-import androidx.camera.core.ImageProxy;
-import androidx.camera.core.Logger;
-import androidx.camera.core.impl.utils.futures.FutureCallback;
-import androidx.camera.core.impl.utils.futures.Futures;
-import androidx.core.util.Pair;
import com.google.auto.value.AutoValue;
-import com.google.common.util.concurrent.ListenableFuture;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Deque;
import java.util.List;
/**
@@ -62,46 +41,13 @@
*
* <p>The thread safety is guaranteed by using the main thread.
*/
-public class TakePictureManager implements OnImageCloseListener, TakePictureRequest.RetryControl {
-
- private static final String TAG = "TakePictureManager";
-
- // Queue of new requests that have not been sent to the pipeline/camera.
- @VisibleForTesting
- final Deque<TakePictureRequest> mNewRequests = new ArrayDeque<>();
- final ImageCaptureControl mImageCaptureControl;
- ImagePipeline mImagePipeline;
-
- // The current request being processed by the camera. Only one request can be processed by
- // the camera at the same time. Null if the camera is idle.
- @Nullable
- private RequestWithCallback mCapturingRequest;
- // The current requests that have not received a result or an error.
- private final List<RequestWithCallback> mIncompleteRequests;
-
- // Once paused, the class waits until the class is resumed to handle new requests.
- boolean mPaused = false;
-
- /**
- * @param imageCaptureControl for controlling {@link ImageCapture}
- */
- @MainThread
- public TakePictureManager(@NonNull ImageCaptureControl imageCaptureControl) {
- checkMainThread();
- mImageCaptureControl = imageCaptureControl;
- mIncompleteRequests = new ArrayList<>();
- }
-
+public interface TakePictureManager {
/**
* Sets the {@link ImagePipeline} for building capture requests and post-processing camera
* output.
*/
@MainThread
- public void setImagePipeline(@NonNull ImagePipeline imagePipeline) {
- checkMainThread();
- mImagePipeline = imagePipeline;
- mImagePipeline.setOnImageCloseListener(this);
- }
+ void setImagePipeline(@NonNull ImagePipeline imagePipeline);
/**
* Adds requests to the queue.
@@ -109,201 +55,52 @@
* <p>The requests in the queue will be executed based on the order being added.
*/
@MainThread
- public void offerRequest(@NonNull TakePictureRequest takePictureRequest) {
- checkMainThread();
- mNewRequests.offer(takePictureRequest);
- issueNextRequest();
- }
-
- @MainThread
- @Override
- public void retryRequest(@NonNull TakePictureRequest request) {
- checkMainThread();
- Logger.d(TAG, "Add a new request for retrying.");
- // Insert the request to the front of the queue.
- mNewRequests.addFirst(request);
- // Try to issue the newly added request in case condition allows.
- issueNextRequest();
- }
+ void offerRequest(@NonNull TakePictureRequest takePictureRequest);
/**
* Pauses sending request to camera.
*/
@MainThread
- public void pause() {
- checkMainThread();
- mPaused = true;
-
- // Always retry because the camera may not send an error callback during the reset.
- if (mCapturingRequest != null) {
- mCapturingRequest.abortSilentlyAndRetry();
- }
- }
+ void pause();
/**
* Resumes sending request to camera.
*/
@MainThread
- public void resume() {
- checkMainThread();
- mPaused = false;
- issueNextRequest();
- }
+ void resume();
/**
* Clears the requests queue.
*/
@MainThread
- public void abortRequests() {
- checkMainThread();
- ImageCaptureException exception =
- new ImageCaptureException(ERROR_CAMERA_CLOSED, "Camera is closed.", null);
-
- // Clear pending request first so aborting in-flight request won't trigger another capture.
- for (TakePictureRequest request : mNewRequests) {
- request.onError(exception);
- }
- mNewRequests.clear();
-
- // Abort the in-flight request after clearing the pending requests.
- // Snapshot to avoid concurrent modification with the removal in getCompleteFuture().
- List<RequestWithCallback> requestsSnapshot = new ArrayList<>(mIncompleteRequests);
- for (RequestWithCallback request : requestsSnapshot) {
- // TODO: optimize the performance by not processing aborted requests.
- request.abortAndSendErrorToApp(exception);
- }
- }
+ void abortRequests();
/**
- * Issues the next request if conditions allow.
+ * Returns whether any capture request is being processed currently.
*/
- @MainThread
- void issueNextRequest() {
- checkMainThread();
- Log.d(TAG, "Issue the next TakePictureRequest.");
- if (hasCapturingRequest()) {
- Log.d(TAG, "There is already a request in-flight.");
- return;
- }
- if (mPaused) {
- Log.d(TAG, "The class is paused.");
- return;
- }
- if (mImagePipeline.getCapacity() == 0) {
- Log.d(TAG, "Too many acquire images. Close image to be able to process next.");
- return;
- }
- TakePictureRequest request = mNewRequests.poll();
- if (request == null) {
- Log.d(TAG, "No new request.");
- return;
- }
-
- RequestWithCallback requestWithCallback = new RequestWithCallback(request, this);
- trackCurrentRequests(requestWithCallback);
-
- // Send requests.
- Pair<CameraRequest, ProcessingRequest> requests =
- mImagePipeline.createRequests(request, requestWithCallback,
- requestWithCallback.getCaptureFuture());
- CameraRequest cameraRequest = requireNonNull(requests.first);
- ProcessingRequest processingRequest = requireNonNull(requests.second);
- mImagePipeline.submitProcessingRequest(processingRequest);
- ListenableFuture<Void> captureRequestFuture = submitCameraRequest(cameraRequest);
- requestWithCallback.setCaptureRequestFuture(captureRequestFuture);
- }
-
- /**
- * Waits for the request to finish before issuing the next.
- */
- private void trackCurrentRequests(@NonNull RequestWithCallback requestWithCallback) {
- checkState(!hasCapturingRequest());
- mCapturingRequest = requestWithCallback;
-
- // Waits for the capture to finish before issuing the next.
- mCapturingRequest.getCaptureFuture().addListener(() -> {
- mCapturingRequest = null;
- issueNextRequest();
- }, directExecutor());
-
- // Track all incomplete requests so we can abort them when UseCase is detached.
- mIncompleteRequests.add(requestWithCallback);
- requestWithCallback.getCompleteFuture().addListener(() -> {
- mIncompleteRequests.remove(requestWithCallback);
- }, directExecutor());
- }
-
- /**
- * Submit a request to camera and post-processing pipeline.
- *
- * <p>Flash is locked/unlocked during the flight of a {@link CameraRequest}.
- */
- @MainThread
- private ListenableFuture<Void> submitCameraRequest(
- @NonNull CameraRequest cameraRequest) {
- checkMainThread();
- mImageCaptureControl.lockFlashMode();
- ListenableFuture<Void> captureRequestFuture =
- mImageCaptureControl.submitStillCaptureRequests(cameraRequest.getCaptureConfigs());
- Futures.addCallback(captureRequestFuture, new FutureCallback<Void>() {
- @Override
- public void onSuccess(@Nullable Void result) {
- mImageCaptureControl.unlockFlashMode();
- }
-
- @Override
- public void onFailure(@NonNull Throwable throwable) {
- if (cameraRequest.isAborted()) {
- // When the pipeline is recreated, the in-flight request is aborted and
- // retried. On legacy devices, the camera may return CancellationException
- // for the aborted request which causes the retried request to fail. Return
- // early if the request has been aborted.
- return;
- } else {
- int requestId = cameraRequest.getCaptureConfigs().get(0).getId();
- if (throwable instanceof ImageCaptureException) {
- mImagePipeline.notifyCaptureError(
- CaptureError.of(requestId, (ImageCaptureException) throwable));
- } else {
- mImagePipeline.notifyCaptureError(
- CaptureError.of(requestId, new ImageCaptureException(
- ERROR_CAPTURE_FAILED,
- "Failed to submit capture request",
- throwable)));
- }
- }
- mImageCaptureControl.unlockFlashMode();
- }
- }, mainThreadExecutor());
- return captureRequestFuture;
- }
-
@VisibleForTesting
- boolean hasCapturingRequest() {
- return mCapturingRequest != null;
- }
+ boolean hasCapturingRequest();
+ /**
+ * Returns the capture request being processed currently.
+ */
@VisibleForTesting
@Nullable
- public RequestWithCallback getCapturingRequest() {
- return mCapturingRequest;
- }
+ RequestWithCallback getCapturingRequest();
+ /**
+ * Returns the requests that have not received a result or an error yet.
+ */
+ @NonNull
@VisibleForTesting
- List<RequestWithCallback> getIncompleteRequests() {
- return mIncompleteRequests;
- }
+ List<RequestWithCallback> getIncompleteRequests();
+ /**
+ * Returns the {@link ImagePipeline} instance used under the hood.
+ */
@VisibleForTesting
@NonNull
- public ImagePipeline getImagePipeline() {
- return mImagePipeline;
- }
-
- @Override
- public void onImageClose(@NonNull ImageProxy image) {
- mainThreadExecutor().execute(this::issueNextRequest);
- }
+ ImagePipeline getImagePipeline();
@AutoValue
abstract static class CaptureError {
@@ -318,4 +115,18 @@
}
}
+ /**
+ * Interface for deferring creation of a {@link TakePictureManager}.
+ */
+ interface Provider {
+ /**
+ * Creates a new, initialized instance of a {@link TakePictureManager}.
+ *
+ * @param imageCaptureControl Used by TakePictureManager to control an
+ * {@link ImageCapture} instance.
+ * @return The {@code TakePictureManager} instance.
+ */
+ @NonNull
+ TakePictureManager newInstance(@NonNull ImageCaptureControl imageCaptureControl);
+ }
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManagerImpl.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManagerImpl.java
new file mode 100644
index 0000000..e356a50
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManagerImpl.java
@@ -0,0 +1,317 @@
+/*
+ * 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.core.imagecapture;
+
+import static androidx.camera.core.ImageCapture.ERROR_CAMERA_CLOSED;
+import static androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED;
+import static androidx.camera.core.impl.utils.Threads.checkMainThread;
+import static androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor;
+import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
+import static androidx.core.util.Preconditions.checkState;
+
+import static java.util.Objects.requireNonNull;
+
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.utils.futures.FutureCallback;
+import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.core.util.Pair;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+
+/**
+ * Manages {@link ImageCapture#takePicture} calls.
+ *
+ * <p>In coming requests are added to a queue and later sent to camera one at a time. Only one
+ * in-flight request is allowed at a time. The next request cannot be sent until the current one
+ * is completed by camera. However, it allows multiple concurrent requests for post-processing,
+ * as {@link ImagePipeline} supports parallel processing.
+ *
+ * <p>This class selectively propagates callbacks from camera and {@link ImagePipeline} to the
+ * app. e.g. it may choose to retry the request before sending the {@link ImageCaptureException}
+ * to the app.
+ *
+ * <p>The thread safety is guaranteed by using the main thread.
+ */
+public class TakePictureManagerImpl implements TakePictureManager, OnImageCloseListener,
+ TakePictureRequest.RetryControl {
+
+ private static final String TAG = "TakePictureManagerImpl";
+
+ // Queue of new requests that have not been sent to the pipeline/camera.
+ @VisibleForTesting
+ final Deque<TakePictureRequest> mNewRequests = new ArrayDeque<>();
+ final ImageCaptureControl mImageCaptureControl;
+ ImagePipeline mImagePipeline;
+
+ // The current request being processed by the camera. Only one request can be processed by
+ // the camera at the same time. Null if the camera is idle.
+ @Nullable
+ private RequestWithCallback mCapturingRequest;
+ // The current requests that have not received a result or an error.
+ private final List<RequestWithCallback> mIncompleteRequests;
+
+ // Once paused, the class waits until the class is resumed to handle new requests.
+ boolean mPaused = false;
+
+ /**
+ * @param imageCaptureControl for controlling {@link ImageCapture}
+ */
+ @MainThread
+ public TakePictureManagerImpl(@NonNull ImageCaptureControl imageCaptureControl) {
+ checkMainThread();
+ mImageCaptureControl = imageCaptureControl;
+ mIncompleteRequests = new ArrayList<>();
+ }
+
+ /**
+ * Sets the {@link ImagePipeline} for building capture requests and post-processing camera
+ * output.
+ */
+ @MainThread
+ @Override
+ public void setImagePipeline(@NonNull ImagePipeline imagePipeline) {
+ checkMainThread();
+ mImagePipeline = imagePipeline;
+ mImagePipeline.setOnImageCloseListener(this);
+ }
+
+ /**
+ * Adds requests to the queue.
+ *
+ * <p>The requests in the queue will be executed based on the order being added.
+ */
+ @MainThread
+ @Override
+ public void offerRequest(@NonNull TakePictureRequest takePictureRequest) {
+ checkMainThread();
+ mNewRequests.offer(takePictureRequest);
+ issueNextRequest();
+ }
+
+ @MainThread
+ @Override
+ public void retryRequest(@NonNull TakePictureRequest request) {
+ checkMainThread();
+ Logger.d(TAG, "Add a new request for retrying.");
+ // Insert the request to the front of the queue.
+ mNewRequests.addFirst(request);
+ // Try to issue the newly added request in case condition allows.
+ issueNextRequest();
+ }
+
+ /**
+ * Pauses sending request to camera.
+ */
+ @MainThread
+ @Override
+ public void pause() {
+ checkMainThread();
+ mPaused = true;
+
+ // Always retry because the camera may not send an error callback during the reset.
+ if (mCapturingRequest != null) {
+ mCapturingRequest.abortSilentlyAndRetry();
+ }
+ }
+
+ /**
+ * Resumes sending request to camera.
+ */
+ @MainThread
+ @Override
+ public void resume() {
+ checkMainThread();
+ mPaused = false;
+ issueNextRequest();
+ }
+
+ /**
+ * Clears the requests queue.
+ */
+ @MainThread
+ @Override
+ public void abortRequests() {
+ checkMainThread();
+ ImageCaptureException exception =
+ new ImageCaptureException(ERROR_CAMERA_CLOSED, "Camera is closed.", null);
+
+ // Clear pending request first so aborting in-flight request won't trigger another capture.
+ for (TakePictureRequest request : mNewRequests) {
+ request.onError(exception);
+ }
+ mNewRequests.clear();
+
+ // Abort the in-flight request after clearing the pending requests.
+ // Snapshot to avoid concurrent modification with the removal in getCompleteFuture().
+ List<RequestWithCallback> requestsSnapshot = new ArrayList<>(mIncompleteRequests);
+ for (RequestWithCallback request : requestsSnapshot) {
+ // TODO: optimize the performance by not processing aborted requests.
+ request.abortAndSendErrorToApp(exception);
+ }
+ }
+
+ /**
+ * Issues the next request if conditions allow.
+ */
+ @MainThread
+ void issueNextRequest() {
+ checkMainThread();
+ Log.d(TAG, "Issue the next TakePictureRequest.");
+ if (hasCapturingRequest()) {
+ Log.d(TAG, "There is already a request in-flight.");
+ return;
+ }
+ if (mPaused) {
+ Log.d(TAG, "The class is paused.");
+ return;
+ }
+ if (mImagePipeline.getCapacity() == 0) {
+ Log.d(TAG, "Too many acquire images. Close image to be able to process next.");
+ return;
+ }
+ TakePictureRequest request = mNewRequests.poll();
+ if (request == null) {
+ Log.d(TAG, "No new request.");
+ return;
+ }
+
+ RequestWithCallback requestWithCallback = new RequestWithCallback(request, this);
+ trackCurrentRequests(requestWithCallback);
+
+ // Send requests.
+ Pair<CameraRequest, ProcessingRequest> requests =
+ mImagePipeline.createRequests(request, requestWithCallback,
+ requestWithCallback.getCaptureFuture());
+ CameraRequest cameraRequest = requireNonNull(requests.first);
+ ProcessingRequest processingRequest = requireNonNull(requests.second);
+ mImagePipeline.submitProcessingRequest(processingRequest);
+ ListenableFuture<Void> captureRequestFuture = submitCameraRequest(cameraRequest);
+ requestWithCallback.setCaptureRequestFuture(captureRequestFuture);
+ }
+
+ /**
+ * Waits for the request to finish before issuing the next.
+ */
+ private void trackCurrentRequests(@NonNull RequestWithCallback requestWithCallback) {
+ checkState(!hasCapturingRequest());
+ mCapturingRequest = requestWithCallback;
+
+ // Waits for the capture to finish before issuing the next.
+ mCapturingRequest.getCaptureFuture().addListener(() -> {
+ mCapturingRequest = null;
+ issueNextRequest();
+ }, directExecutor());
+
+ // Track all incomplete requests so we can abort them when UseCase is detached.
+ mIncompleteRequests.add(requestWithCallback);
+ requestWithCallback.getCompleteFuture().addListener(() -> {
+ mIncompleteRequests.remove(requestWithCallback);
+ }, directExecutor());
+ }
+
+ /**
+ * Submit a request to camera and post-processing pipeline.
+ *
+ * <p>Flash is locked/unlocked during the flight of a {@link CameraRequest}.
+ */
+ @MainThread
+ private ListenableFuture<Void> submitCameraRequest(
+ @NonNull CameraRequest cameraRequest) {
+ checkMainThread();
+ mImageCaptureControl.lockFlashMode();
+ ListenableFuture<Void> captureRequestFuture =
+ mImageCaptureControl.submitStillCaptureRequests(cameraRequest.getCaptureConfigs());
+ Futures.addCallback(captureRequestFuture, new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(@Nullable Void result) {
+ mImageCaptureControl.unlockFlashMode();
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ if (cameraRequest.isAborted()) {
+ // When the pipeline is recreated, the in-flight request is aborted and
+ // retried. On legacy devices, the camera may return CancellationException
+ // for the aborted request which causes the retried request to fail. Return
+ // early if the request has been aborted.
+ return;
+ } else {
+ int requestId = cameraRequest.getCaptureConfigs().get(0).getId();
+ if (throwable instanceof ImageCaptureException) {
+ mImagePipeline.notifyCaptureError(
+ CaptureError.of(requestId, (ImageCaptureException) throwable));
+ } else {
+ mImagePipeline.notifyCaptureError(
+ CaptureError.of(requestId, new ImageCaptureException(
+ ERROR_CAPTURE_FAILED,
+ "Failed to submit capture request",
+ throwable)));
+ }
+ }
+ mImageCaptureControl.unlockFlashMode();
+ }
+ }, mainThreadExecutor());
+ return captureRequestFuture;
+ }
+
+ @VisibleForTesting
+ @Override
+ public boolean hasCapturingRequest() {
+ return mCapturingRequest != null;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ @Override
+ public RequestWithCallback getCapturingRequest() {
+ return mCapturingRequest;
+ }
+
+ @NonNull
+ @VisibleForTesting
+ @Override
+ public List<RequestWithCallback> getIncompleteRequests() {
+ return mIncompleteRequests;
+ }
+
+ @VisibleForTesting
+ @NonNull
+ @Override
+ public ImagePipeline getImagePipeline() {
+ return mImagePipeline;
+ }
+
+ @Override
+ public void onImageClose(@NonNull ImageProxy image) {
+ mainThreadExecutor().execute(this::issueNextRequest);
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
index 4ef3f3b..d49c06b9 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
@@ -82,14 +82,14 @@
* Gets the app provided options for on-disk capture.
*/
@Nullable
- abstract ImageCapture.OutputFileOptions getOutputFileOptions();
+ public abstract ImageCapture.OutputFileOptions getOutputFileOptions();
/**
* A snapshot of {@link ImageCapture#getViewPortCropRect()} when
* {@link ImageCapture#takePicture} is called.
*/
@NonNull
- abstract Rect getCropRect();
+ public abstract Rect getCropRect();
/**
* A snapshot of {@link ImageCapture#getSensorToBufferTransformMatrix()} when
@@ -102,14 +102,14 @@
* A snapshot of rotation degrees when {@link ImageCapture#takePicture} is called.
*/
@ImageOutputConfig.RotationValue
- abstract int getRotationDegrees();
+ public abstract int getRotationDegrees();
/**
* A snapshot of {@link ImageCaptureConfig#getJpegQuality()} when
* {@link ImageCapture#takePicture} is called.
*/
@IntRange(from = 1, to = 100)
- abstract int getJpegQuality();
+ public abstract int getJpegQuality();
/**
* Gets the capture mode of the request.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
index 347a25c..9d6f159 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
@@ -21,10 +21,16 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.ExtendableBuilder;
+import androidx.camera.core.ImageCapture;
import androidx.camera.core.UseCase;
+import androidx.camera.core.imagecapture.ImageCaptureControl;
+import androidx.camera.core.imagecapture.TakePictureManager;
+import androidx.camera.core.imagecapture.TakePictureManagerImpl;
import androidx.camera.core.impl.stabilization.StabilizationMode;
import androidx.camera.core.internal.TargetConfig;
+import java.util.Objects;
+
/**
* Configuration containing options for use cases.
*
@@ -108,6 +114,10 @@
Option<Integer> OPTION_VIDEO_STABILIZATION_MODE =
Option.create("camerax.core.useCase.videoStabilizationMode", int.class);
+ Option<TakePictureManager.Provider> OPTION_TAKE_PICTURE_MANAGER_PROVIDER =
+ Option.create("camerax.core.useCase.takePictureManagerProvider",
+ TakePictureManager.Provider.class);
+
// *********************************************************************************************
/**
@@ -329,6 +339,22 @@
}
/**
+ * @return The {@link TakePictureManager} implementation for {@link ImageCapture} use case.
+ */
+ @NonNull
+ default TakePictureManager.Provider getTakePictureManagerProvider() {
+ return Objects.requireNonNull(retrieveOption(OPTION_TAKE_PICTURE_MANAGER_PROVIDER,
+ new TakePictureManager.Provider() {
+ @NonNull
+ @Override
+ public TakePictureManager newInstance(
+ @NonNull ImageCaptureControl imageCaptureControl) {
+ return new TakePictureManagerImpl(imageCaptureControl);
+ }
+ }));
+ }
+
+ /**
* Builder for a {@link UseCase}.
*
* @param <T> The type of the object which will be built by {@link #build()}.
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt
index be859db..45ba186 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt
@@ -28,7 +28,6 @@
import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
-import java.io.File
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.MainScope
@@ -36,7 +35,9 @@
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
+import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows
@@ -47,9 +48,14 @@
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class ImageCaptureExtTest {
+ @get:Rule
+ val temporaryFolder =
+ TemporaryFolder(ApplicationProvider.getApplicationContext<Context>().cacheDir)
+
private val context = ApplicationProvider.getApplicationContext<Context>()
- private val fakeOutputFileOptions =
- ImageCapture.OutputFileOptions.Builder(File("fake_path")).build()
+ private val fakeOutputFileOptions by lazy {
+ ImageCapture.OutputFileOptions.Builder(temporaryFolder.newFile("fake_path")).build()
+ }
private lateinit var cameraProvider: ProcessCameraProvider
private lateinit var imageCapture: ImageCapture
@@ -88,12 +94,11 @@
fun takePicture_inMemory_canGetImage(): Unit = runTest {
// Arrange
val imageProxy = FakeImageProxy(FakeImageInfo())
+ val fakeTakePictureManager = FakeAppConfig.getTakePictureManager()!!
+ fakeTakePictureManager.enqueueImageProxy(imageProxy)
// Arrange & Act.
val takePictureAsync = MainScope().async { imageCapture.takePicture() }
- Shadows.shadowOf(Looper.getMainLooper()).idle()
- val imageCaptureCallback = imageCapture.getTakePictureRequest()?.inMemoryCallback
- imageCaptureCallback?.onCaptureSuccess(imageProxy)
// Assert.
Shadows.shadowOf(Looper.getMainLooper()).idle()
@@ -135,6 +140,7 @@
var callbackCalled = false
val progress = 100
var resultProgress = 0
+ FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
// Act.
val takePictureAsync =
@@ -163,6 +169,7 @@
var callbackCalled = false
val bitmap = Bitmap.createBitmap(800, 600, Bitmap.Config.ARGB_8888)
lateinit var resultBitmap: Bitmap
+ FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
// Act.
val takePictureAsync =
@@ -189,15 +196,14 @@
fun takePicture_onDisk_canGetResult(): Unit = runTest {
// Arrange
val outputFileResults = ImageCapture.OutputFileResults(null)
+ val fakeTakePictureManager = FakeAppConfig.getTakePictureManager()!!
+ fakeTakePictureManager.enqueueOutputFileResults(outputFileResults)
// Arrange & Act.
val takePictureAsync =
MainScope().async {
imageCapture.takePicture(outputFileOptions = fakeOutputFileOptions)
}
- Shadows.shadowOf(Looper.getMainLooper()).idle()
- val imageCaptureCallback = imageCapture.getTakePictureRequest()?.onDiskCallback
- imageCaptureCallback?.onImageSaved(outputFileResults)
// Assert.
Shadows.shadowOf(Looper.getMainLooper()).idle()
@@ -245,6 +251,7 @@
var callbackCalled = false
val progress = 100
var resultProgress = 0
+ FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
// Act.
val takePictureAsync =
@@ -274,6 +281,7 @@
var callbackCalled = false
val bitmap = Bitmap.createBitmap(800, 600, Bitmap.Config.ARGB_8888)
lateinit var resultBitmap: Bitmap
+ FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
// Act.
val takePictureAsync =
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt
index 7b775e8..87e05b0 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt
@@ -112,7 +112,7 @@
return fileOptions
}
- internal override fun getCropRect(): Rect {
+ override fun getCropRect(): Rect {
return Rect(0, 0, 640, 480)
}
@@ -120,11 +120,11 @@
return Matrix()
}
- internal override fun getRotationDegrees(): Int {
+ override fun getRotationDegrees(): Int {
return ROTATION_DEGREES
}
- internal override fun getJpegQuality(): Int {
+ override fun getJpegQuality(): Int {
return JPEG_QUALITY
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
index db131ac..4f02ad7 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
@@ -50,7 +50,7 @@
private val imagePipeline = FakeImagePipeline()
private val imageCaptureControl = FakeImageCaptureControl()
private val takePictureManager =
- TakePictureManager(imageCaptureControl).also { it.imagePipeline = imagePipeline }
+ TakePictureManagerImpl(imageCaptureControl).also { it.imagePipeline = imagePipeline }
private val exception = ImageCaptureException(ImageCapture.ERROR_UNKNOWN, "", null)
@After
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProviderImpl.kt b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProviderImpl.kt
index bb2d4f4..667a60f 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProviderImpl.kt
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProviderImpl.kt
@@ -143,7 +143,7 @@
}
}
- override fun shutdownAsync(): ListenableFuture<Void> {
+ internal fun shutdownAsync(): ListenableFuture<Void> {
Threads.runOnMainSync {
unbindAll()
lifecycleCameraRepository.clear()
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.kt b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.kt
index 9814e90..e40f3ed 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.kt
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.kt
@@ -114,9 +114,8 @@
return lifecycleCameraProvider.getCameraInfo(cameraSelector)
}
- // TODO: Remove the annotation when LifecycleCameraProvider is ready to be public.
@VisibleForTesting
- override fun shutdownAsync(): ListenableFuture<Void> {
+ public fun shutdownAsync(): ListenableFuture<Void> {
return lifecycleCameraProvider.shutdownAsync()
}
diff --git a/camera/camera-testing/api/current.txt b/camera/camera-testing/api/current.txt
index fbf779a..161b797 100644
--- a/camera/camera-testing/api/current.txt
+++ b/camera/camera-testing/api/current.txt
@@ -42,10 +42,12 @@
ctor public FakeCameraControl(androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
ctor public FakeCameraControl(java.util.concurrent.Executor, androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
method public void addInteropConfig(androidx.camera.core.impl.Config);
+ method public void addOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method public void addOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
method public void addZslConfig(androidx.camera.core.impl.SessionConfig.Builder);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelFocusAndMetering();
method public void clearInteropConfig();
- method public void clearNewCaptureRequestListener();
+ method @Deprecated public void clearNewCaptureRequestListener();
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
method public int getExposureCompensationIndex();
method public int getFlashMode();
@@ -62,11 +64,14 @@
method public void notifyAllRequestsOnCaptureCancelled();
method public void notifyAllRequestsOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
method public void notifyAllRequestsOnCaptureFailed();
+ method public void notifyLastRequestOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
+ method public void removeOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method public void removeOnNewCaptureRequestListeners(java.util.List<androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener!>);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
method public void setFlashMode(int);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(float);
- method public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
- method public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method @Deprecated public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method @Deprecated public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
method public void setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash?);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
method public void setZslDisabledByUserCaseConfig(boolean);
diff --git a/camera/camera-testing/api/restricted_current.txt b/camera/camera-testing/api/restricted_current.txt
index fbf779a..161b797 100644
--- a/camera/camera-testing/api/restricted_current.txt
+++ b/camera/camera-testing/api/restricted_current.txt
@@ -42,10 +42,12 @@
ctor public FakeCameraControl(androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
ctor public FakeCameraControl(java.util.concurrent.Executor, androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
method public void addInteropConfig(androidx.camera.core.impl.Config);
+ method public void addOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method public void addOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
method public void addZslConfig(androidx.camera.core.impl.SessionConfig.Builder);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelFocusAndMetering();
method public void clearInteropConfig();
- method public void clearNewCaptureRequestListener();
+ method @Deprecated public void clearNewCaptureRequestListener();
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
method public int getExposureCompensationIndex();
method public int getFlashMode();
@@ -62,11 +64,14 @@
method public void notifyAllRequestsOnCaptureCancelled();
method public void notifyAllRequestsOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
method public void notifyAllRequestsOnCaptureFailed();
+ method public void notifyLastRequestOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
+ method public void removeOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method public void removeOnNewCaptureRequestListeners(java.util.List<androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener!>);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
method public void setFlashMode(int);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(float);
- method public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
- method public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method @Deprecated public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method @Deprecated public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
method public void setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash?);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
method public void setZslDisabledByUserCaseConfig(boolean);
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
index 41958c4..c7f41a5 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
@@ -28,6 +28,10 @@
import androidx.camera.testing.impl.fakes.FakeCameraDeviceSurfaceManager;
import androidx.camera.testing.impl.fakes.FakeCameraFactory;
import androidx.camera.testing.impl.fakes.FakeUseCaseConfigFactory;
+import androidx.camera.testing.impl.wrappers.TakePictureManagerWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
/**
* Convenience class for generating a fake {@link CameraXConfig}.
@@ -47,6 +51,9 @@
@Nullable
private static FakeCamera sFrontCamera = null;
+ @Nullable
+ private static FakeUseCaseConfigFactory sFakeUseCaseConfigFactory;
+
/** Generates a fake {@link CameraXConfig}. */
@NonNull
public static CameraXConfig create() {
@@ -59,28 +66,26 @@
*/
@NonNull
public static CameraXConfig create(@Nullable CameraSelector availableCamerasSelector) {
+ FakeCameraFactory cameraFactory = createCameraFactory(availableCamerasSelector);
+
final CameraFactory.Provider cameraFactoryProvider =
- (ignored1, ignored2, ignored3, ignore4) -> {
- final FakeCameraFactory cameraFactory = new FakeCameraFactory(
- availableCamerasSelector);
- cameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK,
- DEFAULT_BACK_CAMERA_ID,
- FakeAppConfig::getBackCamera);
- cameraFactory.insertCamera(CameraSelector.LENS_FACING_FRONT,
- DEFAULT_FRONT_CAMERA_ID,
- FakeAppConfig::getFrontCamera);
- final CameraCoordinator cameraCoordinator = new FakeCameraCoordinator();
- cameraFactory.setCameraCoordinator(cameraCoordinator);
- return cameraFactory;
- };
+ (ignored1, ignored2, ignored3, ignore4) -> cameraFactory;
final CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
(ignored1, ignored2, ignored3) -> new FakeCameraDeviceSurfaceManager();
+ List<FakeCamera> fakeCameras = new ArrayList<>();
+ for (String cameraId : cameraFactory.getAvailableCameraIds()) {
+ fakeCameras.add((FakeCamera) cameraFactory.getCamera(cameraId));
+ }
+
+ sFakeUseCaseConfigFactory = new FakeUseCaseConfigFactory(fakeCameras);
+
final CameraXConfig.Builder appConfigBuilder = new CameraXConfig.Builder()
.setCameraFactoryProvider(cameraFactoryProvider)
.setDeviceSurfaceManagerProvider(surfaceManagerProvider)
- .setUseCaseConfigFactoryProvider(ignored -> new FakeUseCaseConfigFactory());
+ .setUseCaseConfigFactoryProvider(
+ ignored -> sFakeUseCaseConfigFactory);
if (availableCamerasSelector != null) {
appConfigBuilder.setAvailableCamerasLimiter(availableCamerasSelector);
@@ -89,6 +94,21 @@
return appConfigBuilder.build();
}
+ private static FakeCameraFactory createCameraFactory(
+ @Nullable CameraSelector availableCamerasSelector) {
+ FakeCameraFactory cameraFactory = new FakeCameraFactory(availableCamerasSelector);
+ cameraFactory.insertCamera(
+ CameraSelector.LENS_FACING_BACK,
+ DEFAULT_BACK_CAMERA_ID,
+ FakeAppConfig::getBackCamera);
+ cameraFactory.insertCamera(CameraSelector.LENS_FACING_FRONT,
+ DEFAULT_FRONT_CAMERA_ID,
+ FakeAppConfig::getFrontCamera);
+ final CameraCoordinator cameraCoordinator = new FakeCameraCoordinator();
+ cameraFactory.setCameraCoordinator(cameraCoordinator);
+ return cameraFactory;
+ }
+
/**
* Returns the default fake back camera that is used internally by CameraX.
*/
@@ -126,4 +146,20 @@
return create();
}
}
+
+ /**
+ * Returns the {@link TakePictureManagerWrapper} being used for image capture.
+ *
+ * <p> Note that this may be null if {@link androidx.camera.core.ImageCapture} is still not set
+ * up and bound to a camera.
+ */
+ @Nullable
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static TakePictureManagerWrapper getTakePictureManager() {
+ if (sFakeUseCaseConfigFactory == null) {
+ return null;
+ }
+ return sFakeUseCaseConfigFactory.getTakePictureManager();
+ }
+
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
index a3b256b..b4d661b 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
@@ -21,6 +21,7 @@
import android.graphics.Rect;
+import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.FocusMeteringAction;
@@ -46,6 +47,8 @@
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
@@ -76,13 +79,18 @@
* <p> {@link CameraXExecutors#directExecutor} via default, unless some other executor is set
* via {@link #FakeCameraControl(Executor, CameraControlInternal.ControlUpdateCallback)}.
*/
- @NonNull private final Executor mExecutor;
+ @NonNull
+ private final Executor mExecutor;
private final ControlUpdateCallback mControlUpdateCallback;
private final SessionConfig.Builder mSessionConfigBuilder = new SessionConfig.Builder();
@ImageCapture.FlashMode
private int mFlashMode = FLASH_MODE_OFF;
private final ArrayList<CaptureConfig> mSubmittedCaptureRequests = new ArrayList<>();
+ @Deprecated
private Pair<Executor, OnNewCaptureRequestListener> mOnNewCaptureRequestListener;
+ @GuardedBy("mOnNewCaptureRequestListeners")
+ private final List<Pair<Executor, OnNewCaptureRequestListener>> mOnNewCaptureRequestListeners =
+ new ArrayList<>();
private MutableOptionsBundle mInteropConfig = MutableOptionsBundle.create();
private final ArrayList<CallbackToFutureAdapter.Completer<Void>> mSubmittedCompleterList =
new ArrayList<>();
@@ -127,7 +135,8 @@
* Constructs an instance of {@link FakeCameraControl} with the
* provided {@link ControlUpdateCallback}.
*
- * @param executor {@link Executor} used to invoke the {@code controlUpdateCallback}.
+ * @param executor {@link Executor} used to invoke the {@code
+ * controlUpdateCallback}.
* @param controlUpdateCallback {@link ControlUpdateCallback} to notify events.
*/
public FakeCameraControl(@NonNull Executor executor,
@@ -180,6 +189,38 @@
}
/**
+ * Notifies last submitted request using {@link CameraCaptureCallback#onCaptureCompleted},
+ * which is invoked in the thread denoted by {@link #mExecutor}.
+ *
+ * @param result The {@link CameraCaptureResult} which is notified to all the callbacks.
+ */
+ public void notifyLastRequestOnCaptureCompleted(@NonNull CameraCaptureResult result) {
+ if (mSubmittedCaptureRequests.isEmpty() || mSubmittedCompleterList.isEmpty()) {
+ Logger.e(TAG,
+ "notifyLastRequestOnCaptureCompleted: returning early since either "
+ + "mSubmittedCaptureRequests or mSubmittedCompleterList is empty, "
+ + "mSubmittedCaptureRequests = "
+ + mSubmittedCaptureRequests + ", mSubmittedCompleterList"
+ + mSubmittedCompleterList);
+ return;
+ }
+
+ CaptureConfig captureConfig = mSubmittedCaptureRequests.get(
+ mSubmittedCaptureRequests.size() - 1);
+ for (CameraCaptureCallback cameraCaptureCallback :
+ captureConfig.getCameraCaptureCallbacks()) {
+ mExecutor.execute(() -> cameraCaptureCallback.onCaptureCompleted(
+ captureConfig.getId(), result));
+ }
+ mSubmittedCaptureRequests.remove(captureConfig);
+
+ CallbackToFutureAdapter.Completer<Void> completer = mSubmittedCompleterList.get(
+ mSubmittedCompleterList.size() - 1);
+ completer.set(null);
+ mSubmittedCompleterList.remove(completer);
+ }
+
+ /**
* Notifies all submitted requests using {@link CameraCaptureCallback#onCaptureCompleted},
* which is invoked in the thread denoted by {@link #mExecutor}.
*
@@ -288,6 +329,7 @@
public ListenableFuture<List<Void>> submitStillCaptureRequests(
@NonNull List<CaptureConfig> captureConfigs,
int captureMode, int flashType) {
+ Logger.d(TAG, "submitStillCaptureRequests: captureConfigs = " + captureConfigs);
mSubmittedCaptureRequests.addAll(captureConfigs);
mExecutor.execute(
() -> mControlUpdateCallback.onCameraControlCaptureRequests(captureConfigs));
@@ -299,12 +341,16 @@
}));
}
- if (mOnNewCaptureRequestListener != null) {
- Executor executor = Objects.requireNonNull(mOnNewCaptureRequestListener.first);
- OnNewCaptureRequestListener listener =
- Objects.requireNonNull(mOnNewCaptureRequestListener.second);
+ synchronized (mOnNewCaptureRequestListeners) {
+ Logger.d(TAG, "submitStillCaptureRequests: mOnNewCaptureRequestListeners = "
+ + mOnNewCaptureRequestListeners);
- executor.execute(() -> listener.onNewCaptureRequests(captureConfigs));
+ for (Pair<Executor, FakeCameraControl.OnNewCaptureRequestListener> listenerPair :
+ mOnNewCaptureRequestListeners) {
+ Executor executor = Objects.requireNonNull(listenerPair.first);
+ OnNewCaptureRequestListener listener = Objects.requireNonNull(listenerPair.second);
+ executor.execute(() -> listener.onNewCaptureRequests(captureConfigs));
+ }
}
return Futures.allAsList(fakeFutures);
}
@@ -348,6 +394,58 @@
}
/**
+ * Adds a listener to be notified when there are new capture requests submitted.
+ *
+ * <p> Note that the listener will be executed on the calling thread directly using
+ * {@link CameraXExecutors#directExecutor}. To specify the execution thread, use
+ * {@link #setOnNewCaptureRequestListener(Executor, OnNewCaptureRequestListener)}.
+ *
+ * @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
+ * {@link CaptureConfig} parameters when new capture requests are submitted.
+ */
+ public void addOnNewCaptureRequestListener(@NonNull OnNewCaptureRequestListener listener) {
+ addOnNewCaptureRequestListener(CameraXExecutors.directExecutor(), listener);
+ }
+
+ /**
+ * Adds a listener to be notified when there are new capture requests submitted.
+ *
+ * @param executor {@link Executor} used to notify the {@code listener}.
+ * @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
+ * {@link CaptureConfig} parameters when new capture requests are submitted.
+ */
+ public void addOnNewCaptureRequestListener(@NonNull Executor executor,
+ @NonNull OnNewCaptureRequestListener listener) {
+ synchronized (mOnNewCaptureRequestListeners) {
+ mOnNewCaptureRequestListeners.add(new Pair<>(executor, listener));
+ }
+ }
+
+ /**
+ * Removes a listener set via {@link #addOnNewCaptureRequestListener}.
+ */
+ public void removeOnNewCaptureRequestListener(@NonNull OnNewCaptureRequestListener listener) {
+ removeOnNewCaptureRequestListeners(Collections.singletonList(listener));
+ }
+
+ /**
+ * Removes a listener set via {@link #addOnNewCaptureRequestListener}.
+ */
+ public void removeOnNewCaptureRequestListeners(
+ @NonNull List<OnNewCaptureRequestListener> listeners) {
+ synchronized (mOnNewCaptureRequestListeners) {
+ Iterator<Pair<Executor, OnNewCaptureRequestListener>> iterator =
+ mOnNewCaptureRequestListeners.iterator();
+ while (iterator.hasNext()) {
+ Pair<Executor, OnNewCaptureRequestListener> element = iterator.next();
+ if (listeners.contains(element.second)) {
+ iterator.remove();
+ }
+ }
+ }
+ }
+
+ /**
* Sets a listener to be notified when there are new capture requests submitted.
*
* <p> Note that the listener will be executed on the calling thread directly using
@@ -355,8 +453,10 @@
* {@link #setOnNewCaptureRequestListener(Executor, OnNewCaptureRequestListener)}.
*
* @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
- * {@link CaptureConfig} parameters when new capture requests are submitted.
+ * {@link CaptureConfig} parameters when new capture requests are submitted.
+ * @deprecated Use {@link #addOnNewCaptureRequestListener(OnNewCaptureRequestListener)} instead.
*/
+ @Deprecated // TODO: b/359458110 - Remove all usages
public void setOnNewCaptureRequestListener(@NonNull OnNewCaptureRequestListener listener) {
setOnNewCaptureRequestListener(CameraXExecutors.directExecutor(), listener);
}
@@ -366,17 +466,31 @@
*
* @param executor {@link Executor} used to notify the {@code listener}.
* @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
- * {@link CaptureConfig} parameters when new capture requests are submitted.
+ * {@link CaptureConfig} parameters when new capture requests are submitted.
+ * @deprecated Use
+ * {@link #addOnNewCaptureRequestListener(Executor, OnNewCaptureRequestListener)}
+ * instead.
*/
+ @Deprecated // TODO: b/359458110 - Remove all usages
public void setOnNewCaptureRequestListener(@NonNull Executor executor,
@NonNull OnNewCaptureRequestListener listener) {
mOnNewCaptureRequestListener = new Pair<>(executor, listener);
+ addOnNewCaptureRequestListener(executor, listener);
}
/**
* Clears any listener set via {@link #setOnNewCaptureRequestListener}.
+ *
+ * @deprecated Use {@link #removeOnNewCaptureRequestListener(OnNewCaptureRequestListener)}
+ * instead.
*/
+ @Deprecated // TODO: b/359458110 - Remove all usages
public void clearNewCaptureRequestListener() {
+ if (mOnNewCaptureRequestListener == null) {
+ return;
+ }
+ removeOnNewCaptureRequestListener(
+ Objects.requireNonNull(mOnNewCaptureRequestListener.second));
mOnNewCaptureRequestListener = null;
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt
index e960662..015bff9 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt
@@ -16,8 +16,11 @@
package androidx.camera.testing.impl
+import android.graphics.Bitmap
import android.graphics.Rect
+import android.util.Size
import android.view.Surface
+import androidx.camera.core.Logger
import androidx.camera.core.impl.DeferrableSurface
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.core.impl.utils.futures.FutureCallback
@@ -36,7 +39,7 @@
private const val TAG = "CaptureSimulation"
/** Simulates a capture frame being drawn on all of the provided surfaces. */
-public suspend fun List<DeferrableSurface>.simulateCaptureFrame(): Unit = forEach {
+internal suspend fun List<DeferrableSurface>.simulateCaptureFrame(): Unit = forEach {
it.simulateCaptureFrame()
}
@@ -45,7 +48,7 @@
*
* @throws IllegalStateException If [DeferrableSurface.getSurface] provides a null surface.
*/
-public suspend fun DeferrableSurface.simulateCaptureFrame() {
+internal suspend fun DeferrableSurface.simulateCaptureFrame() {
val deferred = CompletableDeferred<Unit>()
Futures.addCallback(
@@ -53,6 +56,7 @@
object : FutureCallback<Surface?> {
override fun onSuccess(surface: Surface?) {
if (surface == null) {
+ Logger.w(TAG, "simulateCaptureFrame: surface obtained from $this is null!")
deferred.completeExceptionally(
IllegalStateException(
"Null surface obtained from ${this@simulateCaptureFrame}"
@@ -60,10 +64,9 @@
)
return
}
- val canvas =
- surface.lockCanvas(Rect(0, 0, prescribedSize.width, prescribedSize.height))
// TODO: Draw something on the canvas (e.g. fake image bitmap or alternating color).
- surface.unlockCanvasAndPost(canvas)
+ surface.simulateCaptureFrame(prescribedSize)
+
deferred.complete(Unit)
}
@@ -77,6 +80,20 @@
deferred.await()
}
+/**
+ * Simulates a capture frame being drawn on a [Surface].
+ *
+ * @param canvasSize The canvas size for drawing.
+ * @param bitmap A bitmap to draw as the capture frame, if not null.
+ */
+internal fun Surface.simulateCaptureFrame(canvasSize: Size, bitmap: Bitmap? = null) {
+ val canvas = lockCanvas(Rect(0, 0, canvasSize.width, canvasSize.height))
+ if (bitmap != null) {
+ canvas.drawBitmap(bitmap, null, Rect(0, 0, canvasSize.width, canvasSize.height), null)
+ }
+ unlockCanvasAndPost(canvas)
+}
+
// The following methods are adapters for Java invocations.
/**
@@ -88,7 +105,7 @@
* @return A [ListenableFuture] representing when the operation has been completed.
*/
@JvmOverloads
-public fun List<DeferrableSurface>.simulateCaptureFrameAsync(
+internal fun List<DeferrableSurface>.simulateCaptureFrameAsync(
executor: Executor = Dispatchers.Default.asExecutor()
): ListenableFuture<Void> {
val scope = CoroutineScope(SupervisorJob() + executor.asCoroutineDispatcher())
@@ -104,7 +121,7 @@
* @return A [ListenableFuture] representing when the operation has been completed.
*/
@JvmOverloads
-public fun DeferrableSurface.simulateCaptureFrameAsync(
+internal fun DeferrableSurface.simulateCaptureFrameAsync(
executor: Executor = Dispatchers.Default.asExecutor()
): ListenableFuture<Void> {
val scope = CoroutineScope(SupervisorJob() + executor.asCoroutineDispatcher())
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java
index 90c4bce..5acfa2bb 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java
@@ -16,6 +16,7 @@
package androidx.camera.testing.impl.fakes;
+import android.graphics.Bitmap;
import android.graphics.Rect;
import android.media.Image;
@@ -47,6 +48,8 @@
@NonNull
private ImageInfo mImageInfo;
private Image mImage;
+ @Nullable
+ private Bitmap mBitmap;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Object mReleaseLock = new Object();
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@@ -60,6 +63,11 @@
mImageInfo = imageInfo;
}
+ public FakeImageProxy(@NonNull ImageInfo imageInfo, @NonNull Bitmap bitmap) {
+ mImageInfo = imageInfo;
+ mBitmap = bitmap;
+ }
+
@Override
public void close() {
synchronized (mReleaseLock) {
@@ -196,4 +204,13 @@
return mReleaseFuture;
}
}
+
+ @NonNull
+ @Override
+ public Bitmap toBitmap() {
+ if (mBitmap != null) {
+ return mBitmap;
+ }
+ return ImageProxy.super.toBitmap();
+ }
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java
index f364c4e..9ccd9bd 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java
@@ -19,6 +19,7 @@
import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_TAKE_PICTURE_MANAGER_PROVIDER;
import android.annotation.SuppressLint;
import android.hardware.camera2.CameraDevice;
@@ -30,21 +31,47 @@
import androidx.camera.core.ExperimentalZeroShutterLag;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCapture.CaptureMode;
+import androidx.camera.core.imagecapture.ImageCaptureControl;
+import androidx.camera.core.imagecapture.TakePictureManager;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.MutableOptionsBundle;
import androidx.camera.core.impl.OptionsBundle;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.testing.fakes.FakeCamera;
+import androidx.camera.testing.impl.wrappers.TakePictureManagerWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
/**
* A fake implementation of {@link UseCaseConfigFactory}.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final class FakeUseCaseConfigFactory implements UseCaseConfigFactory {
-
@Nullable
private CaptureType mLastRequestedCaptureType;
+ @Nullable
+ private TakePictureManagerWrapper mTakePictureManager;
+
+ @NonNull
+ private final List<FakeCamera> mFakeCameras = new ArrayList<>();
+
+ /**
+ * Creates a {@link FakeUseCaseConfigFactory} instance.
+ */
+ public FakeUseCaseConfigFactory() {
+ }
+
+ /**
+ * Creates a {@link FakeUseCaseConfigFactory} instance with the available {@link FakeCamera}
+ * instances.
+ */
+ public FakeUseCaseConfigFactory(@NonNull List<FakeCamera> fakeCameras) {
+ mFakeCameras.addAll(fakeCameras);
+ }
+
/**
* Returns the configuration for the given capture type, or <code>null</code> if the
* configuration cannot be produced.
@@ -66,6 +93,20 @@
mutableConfig.insertOption(OPTION_SESSION_CONFIG_UNPACKER,
new FakeSessionConfigOptionUnpacker());
+ if (captureType == CaptureType.IMAGE_CAPTURE) {
+ mutableConfig.insertOption(OPTION_TAKE_PICTURE_MANAGER_PROVIDER,
+ new TakePictureManager.Provider() {
+ @NonNull
+ @Override
+ public TakePictureManager newInstance(
+ @NonNull ImageCaptureControl imageCaptureControl) {
+ mTakePictureManager = new TakePictureManagerWrapper(
+ imageCaptureControl, mFakeCameras);
+ return mTakePictureManager;
+ }
+ });
+ }
+
return OptionsBundle.from(mutableConfig);
}
@@ -97,4 +138,12 @@
return CameraDevice.TEMPLATE_PREVIEW;
}
}
+
+ /**
+ * Returns the last provided {@link TakePictureManagerWrapper} instance.
+ */
+ @Nullable
+ public TakePictureManagerWrapper getTakePictureManager() {
+ return mTakePictureManager;
+ }
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/wrappers/TakePictureManagerWrapper.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/wrappers/TakePictureManagerWrapper.kt
new file mode 100644
index 0000000..0de7dba
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/wrappers/TakePictureManagerWrapper.kt
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.testing.impl.wrappers
+
+import android.graphics.Bitmap
+import android.graphics.Matrix
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCapture.OutputFileOptions
+import androidx.camera.core.ImageCapture.OutputFileResults
+import androidx.camera.core.ImageProcessingUtil
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.Logger
+import androidx.camera.core.imagecapture.Bitmap2JpegBytes
+import androidx.camera.core.imagecapture.ImageCaptureControl
+import androidx.camera.core.imagecapture.ImagePipeline
+import androidx.camera.core.imagecapture.JpegBytes2Disk
+import androidx.camera.core.imagecapture.JpegBytes2Image
+import androidx.camera.core.imagecapture.RequestWithCallback
+import androidx.camera.core.imagecapture.TakePictureManager
+import androidx.camera.core.imagecapture.TakePictureManagerImpl
+import androidx.camera.core.imagecapture.TakePictureRequest
+import androidx.camera.core.processing.Packet
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraControl
+import androidx.camera.testing.impl.ExifUtil
+import androidx.camera.testing.impl.TestImageUtil
+import androidx.camera.testing.impl.fakes.FakeCameraCaptureResult
+import androidx.camera.testing.impl.fakes.FakeImageInfo
+import androidx.camera.testing.impl.fakes.FakeImageProxy
+
+/**
+ * A [TakePictureManager] implementation wrapped around the real implementation
+ * [TakePictureManagerImpl].
+ *
+ * It is used for fake cameras and provides fake image capture results when required from a camera.
+ */
+public class TakePictureManagerWrapper(
+ imageCaptureControl: ImageCaptureControl,
+ private val fakeCameras: List<FakeCamera>
+) : TakePictureManager {
+ // Try to keep the fake as close to real as possible
+ private val managerDelegate = TakePictureManagerImpl(imageCaptureControl)
+
+ private val bitmap2JpegBytes = Bitmap2JpegBytes()
+ private val jpegBytes2Disk = JpegBytes2Disk()
+ private val jpegBytes2Image = JpegBytes2Image()
+
+ private val imageProxyQueue = ArrayDeque<ImageProxy>()
+ private val outputFileResultsQueue = ArrayDeque<ImageCapture.OutputFileResults>()
+
+ /** Whether to disable auto capture completion. */
+ public var disableAutoComplete: Boolean = false
+
+ override fun setImagePipeline(imagePipeline: ImagePipeline) {
+ managerDelegate.imagePipeline = imagePipeline
+ }
+
+ override fun offerRequest(takePictureRequest: TakePictureRequest) {
+ val listeners = mutableListOf<FakeCameraControl.OnNewCaptureRequestListener>()
+
+ fakeCameras.forEach { camera ->
+ if (camera.cameraControlInternal is FakeCameraControl) {
+ (camera.cameraControlInternal as FakeCameraControl).apply {
+ val listener =
+ FakeCameraControl.OnNewCaptureRequestListener {
+ if (!disableAutoComplete) {
+ completeCapturingRequest(this)
+ }
+ }
+ listeners.add(listener)
+ addOnNewCaptureRequestListener(listener)
+ }
+ } else {
+ Logger.w(
+ TAG,
+ "Ignoring ${camera.cameraControlInternal} as it's not FakeCameraControl!"
+ )
+ }
+ }
+
+ managerDelegate.offerRequest(takePictureRequest)
+
+ fakeCameras.forEach { camera ->
+ if (camera.cameraControlInternal is FakeCameraControl) {
+ (camera.cameraControlInternal as FakeCameraControl)
+ .removeOnNewCaptureRequestListeners(listeners)
+ } else {
+ Logger.w(
+ TAG,
+ "Ignoring ${camera.cameraControlInternal} as it's not FakeCameraControl!"
+ )
+ }
+ }
+ }
+
+ override fun pause() {
+ managerDelegate.pause()
+ }
+
+ override fun resume() {
+ managerDelegate.resume()
+ }
+
+ override fun abortRequests() {
+ managerDelegate.abortRequests()
+ }
+
+ @VisibleForTesting
+ override fun hasCapturingRequest(): Boolean = managerDelegate.hasCapturingRequest()
+
+ @VisibleForTesting
+ override fun getCapturingRequest(): RequestWithCallback? = managerDelegate.capturingRequest
+
+ @VisibleForTesting
+ override fun getIncompleteRequests(): List<RequestWithCallback> =
+ managerDelegate.incompleteRequests
+
+ @VisibleForTesting
+ override fun getImagePipeline(): ImagePipeline = managerDelegate.imagePipeline
+
+ @VisibleForTesting
+ public fun completeCapturingRequest(fakeCameraControl: FakeCameraControl) {
+ Log.d(
+ TAG,
+ "completeCapturingRequest: capturingRequest = ${managerDelegate.capturingRequest}"
+ )
+ managerDelegate.capturingRequest?.apply {
+ onCaptureStarted()
+
+ // This ensures the future from CameraControlInternal#submitStillCaptureRequests() is
+ // completed and not garbage collected later
+ // TODO - notify all the new requests, not only the last one
+ fakeCameraControl.notifyLastRequestOnCaptureCompleted(FakeCameraCaptureResult())
+
+ onImageCaptured()
+
+ takePictureRequest.also { req ->
+ val outputFileOptions = req.outputFileOptions // enables smartcast for null check
+ if (req.onDiskCallback != null && outputFileOptions != null) {
+ if (outputFileResultsQueue.isEmpty()) {
+ onFinalResult(createOutputFileResults(req, outputFileOptions))
+ } else {
+ onFinalResult(outputFileResultsQueue.first())
+ outputFileResultsQueue.removeFirst()
+ }
+ } else {
+ if (imageProxyQueue.isEmpty()) {
+ onFinalResult(createImageProxy(req))
+ } else {
+ onFinalResult(imageProxyQueue.first())
+ imageProxyQueue.removeFirst()
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Enqueues an [ImageProxy] to be used as result for the next image capture with
+ * [ImageCapture.OnImageCapturedCallback].
+ *
+ * Note that the provided [ImageProxy] is consumed by next image capture and is not available
+ * for following captures. If no result is available during a capture, CameraX will create a
+ * fake image by itself and provide result based on that.
+ */
+ public fun enqueueImageProxy(imageProxy: ImageProxy) {
+ imageProxyQueue.add(imageProxy)
+ }
+
+ /**
+ * Enqueues an [OutputFileResults] to be used as result for the next image capture with
+ * [ImageCapture.OnImageSavedCallback].
+ *
+ * Note that the provided [OutputFileResults] is consumed by next image capture and is not
+ * available for following captures. If no result is available during a capture, CameraX will
+ * create a fake image by itself and provide result based on that.
+ */
+ public fun enqueueOutputFileResults(outputFileResults: ImageCapture.OutputFileResults) {
+ outputFileResultsQueue.add(outputFileResults)
+ }
+
+ private fun createOutputFileResults(
+ takePictureRequest: TakePictureRequest,
+ outputFileOptions: OutputFileOptions
+ ): ImageCapture.OutputFileResults {
+ // TODO - Take a bitmap as input and use that directly
+ val bytesPacket =
+ takePictureRequest.convertBitmapToBytes(
+ TestImageUtil.createBitmap(
+ takePictureRequest.cropRect.width(),
+ takePictureRequest.cropRect.height()
+ )
+ )
+ return jpegBytes2Disk.apply(JpegBytes2Disk.In.of(bytesPacket, outputFileOptions))
+ }
+
+ private fun createImageProxy(
+ takePictureRequest: TakePictureRequest,
+ ): ImageProxy {
+ // TODO - Take a bitmap as input and use that directly
+ val bitmap =
+ TestImageUtil.createBitmap(
+ takePictureRequest.cropRect.width(),
+ takePictureRequest.cropRect.height()
+ )
+ if (canLoadImageProcessingUtilJniLib()) {
+ val bytesPacket =
+ takePictureRequest.convertBitmapToBytes(
+ TestImageUtil.createBitmap(
+ takePictureRequest.cropRect.width(),
+ takePictureRequest.cropRect.height()
+ )
+ )
+ return jpegBytes2Image.apply(bytesPacket).data
+ } else {
+ return bitmap.toFakeImageProxy()
+ }
+ }
+
+ private fun Bitmap.toFakeImageProxy(): ImageProxy {
+ return FakeImageProxy(FakeImageInfo(), this)
+ }
+
+ private fun TakePictureRequest.convertBitmapToBytes(bitmap: Bitmap): Packet<ByteArray> {
+ val inputPacket =
+ Packet.of(
+ bitmap,
+ ExifUtil.createExif(
+ TestImageUtil.createJpegBytes(cropRect.width(), cropRect.height())
+ ),
+ cropRect,
+ rotationDegrees,
+ Matrix(),
+ FakeCameraCaptureResult()
+ )
+
+ return bitmap2JpegBytes.apply(Bitmap2JpegBytes.In.of(inputPacket, jpegQuality))
+ }
+
+ private fun canLoadImageProcessingUtilJniLib(): Boolean {
+ try {
+ System.loadLibrary(ImageProcessingUtil.JNI_LIB_NAME)
+ return true
+ } catch (e: UnsatisfiedLinkError) {
+ Logger.d(TAG, "canLoadImageProcessingUtilJniLib", e)
+ return false
+ }
+ }
+
+ private companion object {
+ private const val TAG = "TakePictureManagerWrap"
+ }
+}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java
index a61ada7..d7264b0 100644
--- a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java
@@ -173,6 +173,42 @@
}
@Test
+ public void notifiesLastRequestOnCaptureCompleted() {
+ CameraCaptureResult captureResult = new FakeCameraCaptureResult();
+
+ CountDownLatch latch = new CountDownLatch(1);
+ List<CameraCaptureResult> resultList = new ArrayList<>();
+ CaptureConfig captureConfig1 = createCaptureConfig(new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(int captureConfigId,
+ @NonNull CameraCaptureResult cameraCaptureResult) {
+ resultList.add(cameraCaptureResult);
+ }
+ }, new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(int captureConfigId,
+ @NonNull CameraCaptureResult cameraCaptureResult) {
+ resultList.add(cameraCaptureResult);
+ }
+ });
+ CaptureConfig captureConfig2 = createCaptureConfig(new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(int captureConfigId,
+ @NonNull CameraCaptureResult cameraCaptureResult) {
+ resultList.add(cameraCaptureResult);
+ latch.countDown();
+ }
+ });
+
+ mCameraControl.submitStillCaptureRequests(Arrays.asList(captureConfig1, captureConfig2),
+ ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY, ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
+ mCameraControl.notifyLastRequestOnCaptureCompleted(captureResult);
+
+ awaitLatch(latch);
+ assertThat(resultList).containsExactlyElementsIn(Collections.singletonList(captureResult));
+ }
+
+ @Test
public void canUpdateFlashModeToOff() {
mCameraControl.setFlashMode(ImageCapture.FLASH_MODE_OFF);
assertThat(mCameraControl.getFlashMode()).isEqualTo(ImageCapture.FLASH_MODE_OFF);
@@ -319,7 +355,7 @@
List<CaptureConfig> notifiedCaptureConfigs = new ArrayList<>();
CountDownLatch latch = new CountDownLatch(1);
- mCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ mCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
notifiedCaptureConfigs.addAll(captureConfigs);
latch.countDown();
});
@@ -335,7 +371,7 @@
AtomicReference<Thread> listenerThread = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
- mCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ mCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
listenerThread.set(Thread.currentThread());
latch.countDown();
});
@@ -350,7 +386,7 @@
AtomicReference<Thread> listenerThread = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
- mCameraControl.setOnNewCaptureRequestListener(CameraXExecutors.mainThreadExecutor(),
+ mCameraControl.addOnNewCaptureRequestListener(CameraXExecutors.mainThreadExecutor(),
captureConfigs -> {
listenerThread.set(Thread.currentThread());
latch.countDown();
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ResolutionSelectorDeviceTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ResolutionSelectorDeviceTest.kt
new file mode 100644
index 0000000..9e9562c
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ResolutionSelectorDeviceTest.kt
@@ -0,0 +1,692 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.integration.core
+
+import android.Manifest
+import android.content.Context
+import android.graphics.ImageFormat
+import android.graphics.Point
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES
+import android.util.Log
+import android.util.Range
+import android.util.Rational
+import android.util.Size
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.internal.DisplayInfoManager
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.AspectRatio.RATIO_16_9
+import androidx.camera.core.AspectRatio.RATIO_4_3
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.ImageFormatConstants
+import androidx.camera.core.impl.ImageOutputConfig
+import androidx.camera.core.impl.Quirk
+import androidx.camera.core.impl.RestrictedCameraControl
+import androidx.camera.core.impl.utils.AspectRatioUtil
+import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9
+import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3
+import androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio
+import androidx.camera.core.impl.utils.CompareSizesByArea
+import androidx.camera.core.internal.utils.SizeUtil
+import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1080P
+import androidx.camera.core.resolutionselector.AspectRatioStrategy
+import androidx.camera.core.resolutionselector.AspectRatioStrategy.FALLBACK_RULE_AUTO
+import androidx.camera.core.resolutionselector.ResolutionFilter
+import androidx.camera.core.resolutionselector.ResolutionSelector
+import androidx.camera.core.resolutionselector.ResolutionSelector.PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
+import androidx.camera.core.resolutionselector.ResolutionSelector.PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE
+import androidx.camera.core.resolutionselector.ResolutionStrategy
+import androidx.camera.core.resolutionselector.ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.impl.CameraPipeConfigTestRule
+import androidx.camera.testing.impl.CameraUtil
+import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * ResolutionSelector related test on the real device.
+ *
+ * Make the ResolutionSelectorDeviceTest focus on the generic ResolutionSelector selection results
+ * for all the normal devices. Skips the tests when the devices have any of the quirks that might
+ * affect the selected resolution.
+ */
+@LargeTest
+@RunWith(Parameterized::class)
+@SdkSuppress(minSdkVersion = 21)
+class ResolutionSelectorDeviceTest(
+ private val implName: String,
+ private var cameraSelector: CameraSelector,
+ private val cameraConfig: CameraXConfig,
+) {
+ @get:Rule
+ val cameraPipeConfigTestRule =
+ CameraPipeConfigTestRule(
+ active = implName.contains(CameraPipeConfig::class.simpleName!!),
+ )
+
+ @get:Rule
+ val cameraRule =
+ CameraUtil.grantCameraPermissionAndPreTestAndPostTest(
+ CameraUtil.PreTestCameraIdList(cameraConfig)
+ )
+
+ @get:Rule
+ val permissionRule: GrantPermissionRule =
+ GrantPermissionRule.grant(Manifest.permission.RECORD_AUDIO)
+
+ private val useCaseFormatMap =
+ mapOf(
+ Pair(Preview::class.java, ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE),
+ Pair(ImageCapture::class.java, ImageFormat.JPEG),
+ Pair(ImageAnalysis::class.java, ImageFormat.YUV_420_888)
+ )
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data() =
+ listOf(
+ arrayOf(
+ "back+" + Camera2Config::class.simpleName,
+ CameraSelector.DEFAULT_BACK_CAMERA,
+ Camera2Config.defaultConfig(),
+ ),
+ arrayOf(
+ "front+" + Camera2Config::class.simpleName,
+ CameraSelector.DEFAULT_FRONT_CAMERA,
+ Camera2Config.defaultConfig(),
+ ),
+ arrayOf(
+ "back+" + CameraPipeConfig::class.simpleName,
+ CameraSelector.DEFAULT_BACK_CAMERA,
+ CameraPipeConfig.defaultConfig(),
+ ),
+ arrayOf(
+ "front+" + CameraPipeConfig::class.simpleName,
+ CameraSelector.DEFAULT_FRONT_CAMERA,
+ CameraPipeConfig.defaultConfig(),
+ ),
+ )
+ }
+
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val context: Context = ApplicationProvider.getApplicationContext()
+ private lateinit var cameraProvider: ProcessCameraProvider
+ private lateinit var lifecycleOwner: FakeLifecycleOwner
+ private lateinit var camera: Camera
+ private lateinit var cameraInfoInternal: CameraInfoInternal
+
+ @Before
+ fun initializeCameraX() {
+ assumeTrue(CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!))
+ ProcessCameraProvider.configureInstance(cameraConfig)
+ cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
+
+ instrumentation.runOnMainSync {
+ lifecycleOwner = FakeLifecycleOwner()
+ lifecycleOwner.startAndResume()
+
+ camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector)
+ cameraInfoInternal = camera.cameraInfo as CameraInfoInternal
+ }
+
+ assumeNotAspectRatioQuirkDevice()
+ assumeNotOutputSizeQuirkDevice()
+ }
+
+ @After
+ fun shutdownCameraX() {
+ if (::cameraProvider.isInitialized) {
+ cameraProvider.shutdownAsync()[10, TimeUnit.SECONDS]
+ }
+ }
+
+ @Test
+ fun canSelect4x3ResolutionForPreviewImageCaptureAndImageAnalysis() {
+ canSelectTargetAspectRatioResolutionForPreviewImageCaptureAndImageAnalysis(RATIO_4_3)
+ }
+
+ @Test
+ fun canSelect16x9ResolutionForPreviewImageCaptureAndImageAnalysis() {
+ canSelectTargetAspectRatioResolutionForPreviewImageCaptureAndImageAnalysis(RATIO_16_9)
+ }
+
+ private fun canSelectTargetAspectRatioResolutionForPreviewImageCaptureAndImageAnalysis(
+ targetAspectRatio: Int
+ ) {
+ val preview = createUseCaseWithResolutionSelector(Preview::class.java, targetAspectRatio)
+ val imageCapture =
+ createUseCaseWithResolutionSelector(ImageCapture::class.java, targetAspectRatio)
+ val imageAnalysis =
+ createUseCaseWithResolutionSelector(ImageAnalysis::class.java, targetAspectRatio)
+ instrumentation.runOnMainSync {
+ cameraProvider.bindToLifecycle(
+ lifecycleOwner,
+ cameraSelector,
+ preview,
+ imageCapture,
+ imageAnalysis
+ )
+ }
+ assertThat(isResolutionAspectRatioBestMatched(preview, targetAspectRatio)).isTrue()
+ assertThat(isResolutionAspectRatioBestMatched(imageCapture, targetAspectRatio)).isTrue()
+ assertThat(isResolutionAspectRatioBestMatched(imageAnalysis, targetAspectRatio)).isTrue()
+ }
+
+ private fun isResolutionAspectRatioBestMatched(
+ useCase: UseCase,
+ targetAspectRatio: Int
+ ): Boolean {
+ val isMatched =
+ hasMatchingAspectRatio(
+ useCase.attachedSurfaceResolution!!,
+ aspectRatioToRational(targetAspectRatio)
+ )
+
+ if (isMatched) {
+ return true
+ }
+
+ // PRIV/PREVIEW + YUV/PREVIEW + JPEG/MAXIMUM will be used to select resolutions for the
+ // combination of Preview + ImageAnalysis + ImageCapture
+ val closestAspectRatioSizes =
+ if (useCase is Preview || useCase is ImageAnalysis) {
+ getClosestAspectRatioSizesUnderPreviewSize(targetAspectRatio, useCase.javaClass)
+ } else {
+ getClosestAspectRatioSizes(targetAspectRatio, useCase.javaClass)
+ }
+
+ Log.d(
+ "ResolutionSelectorDeviceTest",
+ "The selected resolution (${useCase.attachedSurfaceResolution!!}) does not exactly" +
+ " match the target aspect ratio. It is selected from the closest aspect ratio" +
+ " sizes: $closestAspectRatioSizes"
+ )
+
+ return closestAspectRatioSizes.contains(useCase.attachedSurfaceResolution!!)
+ }
+
+ @Test
+ fun canSelect4x3ResolutionForPreviewByResolutionStrategy() =
+ canSelectResolutionByResolutionStrategy(Preview::class.java, RATIO_4_3)
+
+ @Test
+ fun canSelect16x9ResolutionForPreviewByResolutionStrategy() =
+ canSelectResolutionByResolutionStrategy(Preview::class.java, RATIO_16_9)
+
+ @Test
+ fun canSelect4x3ResolutionForImageCaptureByResolutionStrategy() =
+ canSelectResolutionByResolutionStrategy(ImageCapture::class.java, RATIO_4_3)
+
+ @Test
+ fun canSelect16x9ResolutionForImageCaptureByResolutionStrategy() =
+ canSelectResolutionByResolutionStrategy(ImageCapture::class.java, RATIO_16_9)
+
+ @Test
+ fun canSelect4x3ResolutionForImageAnalysisByResolutionStrategy() =
+ canSelectResolutionByResolutionStrategy(ImageAnalysis::class.java, RATIO_4_3)
+
+ @Test
+ fun canSelect16x9ResolutionForImageAnalysisByResolutionStrategy() =
+ canSelectResolutionByResolutionStrategy(ImageAnalysis::class.java, RATIO_16_9)
+
+ private fun <T : UseCase> canSelectResolutionByResolutionStrategy(
+ useCaseClass: Class<T>,
+ ratio: Int
+ ) {
+ // Filters the output sizes matching the target aspect ratio
+ cameraInfoInternal
+ .getSupportedResolutions(useCaseFormatMap[useCaseClass]!!)
+ .filter { size -> hasMatchingAspectRatio(size, aspectRatioToRational(ratio)) }
+ .let {
+ // Picks the item in the middle of the list to run the test
+ it.elementAtOrNull(it.size / 2)?.let { boundSize ->
+ {
+ val useCase =
+ createUseCaseWithResolutionSelector(
+ useCaseClass,
+ aspectRatio = ratio,
+ aspectRatioStrategyFallbackRule = FALLBACK_RULE_AUTO,
+ boundSize = boundSize,
+ resolutionStrategyFallbackRule =
+ FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+ )
+ instrumentation.runOnMainSync {
+ cameraProvider.unbindAll()
+ cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCase)
+ }
+ assertThat(useCase.attachedSurfaceResolution).isEqualTo(boundSize)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun canSelectAnyResolutionForPreviewByResolutionFilter() =
+ canSelectAnyResolutionByResolutionFilter(
+ Preview::class.java,
+ // For Preview, need to override resolution strategy so that the output sizes larger
+ // than PREVIEW size can be selected.
+ cameraInfoInternal
+ .getSupportedResolutions(useCaseFormatMap[Preview::class.java]!!)
+ .maxWithOrNull(CompareSizesByArea())
+ )
+
+ @Test
+ fun canSelectAnyHighResolutionForPreviewByResolutionFilter() =
+ canSelectAnyHighResolutionByResolutionFilter(
+ Preview::class.java,
+ // For Preview, need to override resolution strategy so that the output sizes larger
+ // than PREVIEW size can be selected.
+ cameraInfoInternal
+ .getSupportedHighResolutions(useCaseFormatMap[Preview::class.java]!!)
+ .maxWithOrNull(CompareSizesByArea())
+ )
+
+ @Test
+ fun canSelectAnyResolutionForImageCaptureByResolutionFilter() =
+ canSelectAnyResolutionByResolutionFilter(ImageCapture::class.java)
+
+ @Test
+ fun canSelectAnyHighResolutionForImageCaptureByResolutionFilter() =
+ canSelectAnyHighResolutionByResolutionFilter(ImageCapture::class.java)
+
+ @Test
+ fun canSelectAnyResolutionForImageAnalysisByResolutionFilter() =
+ canSelectAnyResolutionByResolutionFilter(ImageAnalysis::class.java)
+
+ @Test
+ fun canSelectAnyHighResolutionForImageAnalysisByResolutionFilter() =
+ canSelectAnyHighResolutionByResolutionFilter(ImageAnalysis::class.java)
+
+ private fun <T : UseCase> canSelectAnyResolutionByResolutionFilter(
+ useCaseClass: Class<T>,
+ boundSize: Size? = null,
+ resolutionStrategyFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+ ) =
+ canSelectAnyResolutionByResolutionFilter(
+ useCaseClass,
+ cameraInfoInternal.getSupportedResolutions(useCaseFormatMap[useCaseClass]!!),
+ boundSize,
+ resolutionStrategyFallbackRule
+ )
+
+ private fun <T : UseCase> canSelectAnyHighResolutionByResolutionFilter(
+ useCaseClass: Class<T>,
+ boundSize: Size? = null,
+ resolutionStrategyFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+ ) =
+ canSelectAnyResolutionByResolutionFilter(
+ useCaseClass,
+ cameraInfoInternal.getSupportedHighResolutions(useCaseFormatMap[useCaseClass]!!),
+ boundSize,
+ resolutionStrategyFallbackRule,
+ PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE
+ )
+
+ private fun <T : UseCase> canSelectAnyResolutionByResolutionFilter(
+ useCaseClass: Class<T>,
+ outputSizes: List<Size>,
+ boundSize: Size? = null,
+ resolutionStrategyFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+ allowedResolutionMode: Int = PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
+ ) {
+ outputSizes.forEach { targetResolution ->
+ val useCase =
+ createUseCaseWithResolutionSelector(
+ useCaseClass,
+ boundSize = boundSize,
+ resolutionStrategyFallbackRule = resolutionStrategyFallbackRule,
+ resolutionFilter = { _, _ -> mutableListOf(targetResolution) },
+ allowedResolutionMode = allowedResolutionMode
+ )
+ instrumentation.runOnMainSync {
+ cameraProvider.unbindAll()
+ cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCase)
+ }
+ assertThat(useCase.attachedSurfaceResolution).isEqualTo(targetResolution)
+ }
+ }
+
+ @Test
+ fun canSelectResolutionForSixtyFpsPreview() {
+ assumeTrue(isSixtyFpsSupported())
+
+ val preview = Preview.Builder().setTargetFrameRate(Range.create(60, 60)).build()
+ val imageCapture = ImageCapture.Builder().build()
+
+ instrumentation.runOnMainSync {
+ cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageCapture)
+ }
+
+ assertThat(getMaxFrameRate(preview.attachedSurfaceResolution!!)).isEqualTo(60)
+ }
+
+ private fun isSixtyFpsSupported() =
+ CameraUtil.getCameraCharacteristics(cameraSelector.lensFacing!!)
+ ?.get(CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)
+ ?.any { range -> range.upper == 60 } ?: false
+
+ private fun getMaxFrameRate(size: Size) =
+ (1_000_000_000.0 /
+ CameraUtil.getCameraCharacteristics(cameraSelector.lensFacing!!)!!.get(
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
+ )!!
+ .getOutputMinFrameDuration(SurfaceTexture::class.java, size) + 0.5)
+ .toInt()
+
+ private fun <T : UseCase> createUseCaseWithResolutionSelector(
+ useCaseClass: Class<T>,
+ aspectRatio: Int? = null,
+ aspectRatioStrategyFallbackRule: Int = FALLBACK_RULE_AUTO,
+ boundSize: Size? = null,
+ resolutionStrategyFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+ resolutionFilter: ResolutionFilter? = null,
+ allowedResolutionMode: Int = PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
+ ): UseCase {
+ val builder =
+ when (useCaseClass) {
+ Preview::class.java -> Preview.Builder()
+ ImageCapture::class.java -> ImageCapture.Builder()
+ ImageAnalysis::class.java -> ImageAnalysis.Builder()
+ else -> throw IllegalArgumentException("Unsupported class type!!")
+ }
+
+ (builder as ImageOutputConfig.Builder<*>).setResolutionSelector(
+ createResolutionSelector(
+ aspectRatio,
+ aspectRatioStrategyFallbackRule,
+ boundSize,
+ resolutionStrategyFallbackRule,
+ resolutionFilter,
+ allowedResolutionMode
+ )
+ )
+
+ return builder.build()
+ }
+
+ private fun createResolutionSelector(
+ aspectRatio: Int? = null,
+ aspectRatioFallbackRule: Int = FALLBACK_RULE_AUTO,
+ boundSize: Size? = null,
+ resolutionFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+ resolutionFilter: ResolutionFilter? = null,
+ allowedResolutionMode: Int = PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
+ ) =
+ ResolutionSelector.Builder()
+ .apply {
+ aspectRatio?.let {
+ setAspectRatioStrategy(
+ AspectRatioStrategy(aspectRatio, aspectRatioFallbackRule)
+ )
+ }
+ boundSize?.let {
+ setResolutionStrategy(ResolutionStrategy(boundSize, resolutionFallbackRule))
+ }
+ resolutionFilter?.let { setResolutionFilter(resolutionFilter) }
+ setAllowedResolutionMode(allowedResolutionMode)
+ }
+ .build()
+
+ private fun aspectRatioToRational(ratio: Int) =
+ if (ratio == RATIO_16_9) {
+ ASPECT_RATIO_16_9
+ } else {
+ ASPECT_RATIO_4_3
+ }
+
+ private fun <T : UseCase> getClosestAspectRatioSizesUnderPreviewSize(
+ targetAspectRatio: Int,
+ useCaseClass: Class<T>
+ ): List<Size> {
+ val outputSizes =
+ cameraInfoInternal.getSupportedResolutions(useCaseFormatMap[useCaseClass]!!)
+ return outputSizes
+ .getSmallerThanOrEqualToPreviewScaleSizeSublist()
+ .getClosestAspectRatioSublist(targetAspectRatio)
+ }
+
+ private fun <T : UseCase> getClosestAspectRatioSizes(
+ targetAspectRatio: Int,
+ useCaseClass: Class<T>
+ ): List<Size> {
+ val outputSizes =
+ cameraInfoInternal.getSupportedResolutions(useCaseFormatMap[useCaseClass]!!)
+ return outputSizes.getClosestAspectRatioSublist(targetAspectRatio)
+ }
+
+ private fun List<Size>.getSmallerThanOrEqualToPreviewScaleSizeSublist() = filter { size ->
+ SizeUtil.getArea(size) <= SizeUtil.getArea(getPreviewScaleSize())
+ }
+
+ @Suppress("DEPRECATION")
+ private fun getPreviewScaleSize(): Size {
+ val point = Point()
+ DisplayInfoManager.getInstance(context).getMaxSizeDisplay(false).getRealSize(point)
+ val displaySize = Size(point.x, point.y)
+ return if (SizeUtil.isSmallerByArea(RESOLUTION_1080P, displaySize)) {
+ RESOLUTION_1080P
+ } else {
+ displaySize
+ }
+ }
+
+ private fun List<Size>.getClosestAspectRatioSublist(targetAspectRatio: Int): List<Size> {
+ val sensorRect = (camera.cameraControl as RestrictedCameraControl).sensorRect
+ val aspectRatios = getResolutionListGroupingAspectRatioKeys(this)
+ val sortedAspectRatios =
+ aspectRatios.sortedWith(
+ AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+ aspectRatioToRational(targetAspectRatio),
+ Rational(sensorRect.width(), sensorRect.height())
+ )
+ )
+ val groupedRatioToSizesMap = groupSizesByAspectRatio(this)
+
+ for (ratio in sortedAspectRatios) {
+ groupedRatioToSizesMap[ratio]?.let {
+ if (it.isNotEmpty()) {
+ return it
+ }
+ }
+ }
+
+ fail("There should have one non-empty size list returned.")
+ }
+
+ /**
+ * Returns the grouping aspect ratio keys of the input resolution list.
+ *
+ * Some sizes might be mod16 case. When grouping, those sizes will be grouped into an existing
+ * aspect ratio group if the aspect ratio can match by the mod16 rule.
+ */
+ private fun getResolutionListGroupingAspectRatioKeys(
+ resolutionCandidateList: List<Size>
+ ): List<Rational> {
+ val aspectRatios = mutableListOf<Rational>()
+
+ // Adds the default 4:3 and 16:9 items first to avoid their mod16 sizes to create
+ // additional items.
+ aspectRatios.add(ASPECT_RATIO_4_3)
+ aspectRatios.add(ASPECT_RATIO_16_9)
+
+ // Tries to find the aspect ratio which the target size belongs to.
+ for (size in resolutionCandidateList) {
+ val newRatio = Rational(size.width, size.height)
+ val aspectRatioFound = aspectRatios.contains(newRatio)
+
+ // The checking size might be a mod16 size which can be mapped to an existing aspect
+ // ratio group.
+ if (!aspectRatioFound) {
+ var hasMatchingAspectRatio = false
+ for (aspectRatio in aspectRatios) {
+ if (hasMatchingAspectRatio(size, aspectRatio)) {
+ hasMatchingAspectRatio = true
+ break
+ }
+ }
+ if (!hasMatchingAspectRatio) {
+ aspectRatios.add(newRatio)
+ }
+ }
+ }
+
+ return aspectRatios
+ }
+
+ /** Groups the input sizes into an aspect ratio to size list map. */
+ private fun groupSizesByAspectRatio(sizes: List<Size>): Map<Rational, MutableList<Size>> {
+ val aspectRatioSizeListMap = mutableMapOf<Rational, MutableList<Size>>()
+ val aspectRatioKeys = getResolutionListGroupingAspectRatioKeys(sizes)
+
+ for (aspectRatio in aspectRatioKeys) {
+ aspectRatioSizeListMap[aspectRatio] = mutableListOf()
+ }
+
+ for (outputSize in sizes) {
+ for (key in aspectRatioSizeListMap.keys) {
+ // Put the size into all groups that is matched in mod16 condition since a size
+ // may match multiple aspect ratio in mod16 algorithm.
+ if (hasMatchingAspectRatio(outputSize, key)) {
+ aspectRatioSizeListMap[key]!!.add(outputSize)
+ }
+ }
+ }
+
+ return aspectRatioSizeListMap
+ }
+
+ // Skips the tests when the devices have any of the quirks that might affect the selected
+ // resolution.
+ private fun assumeNotAspectRatioQuirkDevice() {
+ assumeFalse(hasAspectRatioLegacyApi21Quirk())
+ assumeFalse(hasNexus4AndroidLTargetAspectRatioQuirk())
+ assumeFalse(hasExtraCroppingQuirk())
+ }
+
+ // Checks whether it is the device for AspectRatioLegacyApi21Quirk
+ private fun hasAspectRatioLegacyApi21Quirk(): Boolean {
+ val quirks = cameraInfoInternal.cameraQuirks
+
+ return if (implName == CameraPipeConfig::class.simpleName) {
+ quirks.contains(
+ androidx.camera.camera2.pipe.integration.compat.quirk
+ .AspectRatioLegacyApi21Quirk::class
+ .java
+ )
+ } else {
+ quirks.contains(
+ androidx.camera.camera2.internal.compat.quirk.AspectRatioLegacyApi21Quirk::class
+ .java
+ )
+ }
+ }
+
+ // Checks whether it is the device for Nexus4AndroidLTargetAspectRatioQuirk
+ private fun hasNexus4AndroidLTargetAspectRatioQuirk() =
+ if (implName == CameraPipeConfig::class.simpleName) {
+ hasDeviceQuirk(
+ androidx.camera.camera2.pipe.integration.compat.quirk
+ .Nexus4AndroidLTargetAspectRatioQuirk::class
+ .java
+ )
+ } else {
+ hasDeviceQuirk(
+ androidx.camera.camera2.internal.compat.quirk
+ .Nexus4AndroidLTargetAspectRatioQuirk::class
+ .java
+ )
+ }
+
+ // Checks whether it is the device for ExtraCroppingQuirk
+ private fun hasExtraCroppingQuirk() =
+ if (implName == CameraPipeConfig::class.simpleName) {
+ hasDeviceQuirk(
+ androidx.camera.camera2.pipe.integration.compat.quirk.ExtraCroppingQuirk::class.java
+ )
+ } else {
+ hasDeviceQuirk(
+ androidx.camera.camera2.internal.compat.quirk.ExtraCroppingQuirk::class.java
+ )
+ }
+
+ // Skips the tests when the devices have any of the quirks that might affect the selected
+ // resolution.
+ private fun assumeNotOutputSizeQuirkDevice() {
+ assumeFalse(hasExcludedSupportedSizesQuirk())
+ assumeFalse(hasExtraSupportedOutputSizeQuirk())
+ }
+
+ private fun hasExcludedSupportedSizesQuirk() =
+ if (implName == CameraPipeConfig::class.simpleName) {
+ hasDeviceQuirk(
+ androidx.camera.camera2.pipe.integration.compat.quirk
+ .ExcludedSupportedSizesQuirk::class
+ .java
+ )
+ } else {
+ hasDeviceQuirk(
+ androidx.camera.camera2.internal.compat.quirk.ExcludedSupportedSizesQuirk::class
+ .java
+ )
+ }
+
+ private fun hasExtraSupportedOutputSizeQuirk() =
+ if (implName == CameraPipeConfig::class.simpleName) {
+ hasDeviceQuirk(
+ androidx.camera.camera2.pipe.integration.compat.quirk
+ .ExtraSupportedOutputSizeQuirk::class
+ .java
+ )
+ } else {
+ hasDeviceQuirk(
+ androidx.camera.camera2.internal.compat.quirk.ExtraSupportedOutputSizeQuirk::class
+ .java
+ )
+ }
+
+ private fun <T : Quirk?> hasDeviceQuirk(quirkClass: Class<T>) =
+ if (implName == CameraPipeConfig::class.simpleName) {
+ androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks.get(quirkClass)
+ } else {
+ androidx.camera.camera2.internal.compat.quirk.DeviceQuirks.get(quirkClass)
+ } != null
+}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
index af52c44..ef4fe9e 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
@@ -42,7 +42,6 @@
import kotlinx.coroutines.withContext
import org.junit.After
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
@@ -87,7 +86,7 @@
@Test
fun canSubmitTakePictureRequest(): Unit = runBlocking {
val countDownLatch = CountDownLatch(1)
- cameraControl.setOnNewCaptureRequestListener { countDownLatch.countDown() }
+ cameraControl.addOnNewCaptureRequestListener { countDownLatch.countDown() }
imageCapture.takePicture(CameraXExecutors.directExecutor(), FakeOnImageCapturedCallback())
@@ -96,10 +95,9 @@
// Duplicate to ImageCaptureTest on core-test-app JVM tests, any change here may need to be
// reflected there too
- @Ignore("b/318314454")
@Test
fun canCreateBitmapFromTakenImage_whenImageCapturedCallbackIsUsed(): Unit = runBlocking {
- val callback = FakeOnImageCapturedCallback()
+ val callback = FakeOnImageCapturedCallback(closeImageOnSuccess = false)
imageCapture.takePicture(CameraXExecutors.directExecutor(), callback)
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
callback.results.first().image.toBitmap()
@@ -107,7 +105,6 @@
// Duplicate to ImageCaptureTest on core-test-app JVM tests, any change here may need to be
// reflected there too
- @Ignore("b/318314454")
@Test
fun canFindImage_whenFileStorageAndImageSavedCallbackIsUsed(): Unit = runBlocking {
val saveLocation = temporaryFolder.newFile()
@@ -126,7 +123,6 @@
// Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
// need to be reflected there too
- @Ignore("b/318314454")
@Test
fun canFindImage_whenMediaStoreAndImageSavedCallbackIsUsed(): Unit = runBlocking {
val initialCount = getMediaStoreCameraXImageCount()
diff --git a/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 459187c..15cbefe 100644
--- a/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -44,7 +44,6 @@
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.rules.TemporaryFolder
@@ -92,7 +91,7 @@
@Test
fun canSubmitTakePictureRequest(): Unit = runTest {
val countDownLatch = CountDownLatch(1)
- cameraControl.setOnNewCaptureRequestListener { countDownLatch.countDown() }
+ cameraControl.addOnNewCaptureRequestListener { countDownLatch.countDown() }
imageCapture.takePicture(CameraXExecutors.directExecutor(), FakeOnImageCapturedCallback())
@@ -101,7 +100,6 @@
// Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
// need to be reflected there too
- @Ignore("b/318314454")
@Test
fun canCreateBitmapFromTakenImage_whenImageCapturedCallbackIsUsed(): Unit = runTest {
val callback = FakeOnImageCapturedCallback()
@@ -112,7 +110,6 @@
// Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
// need to be reflected there too
- @Ignore("b/318314454")
@Test
fun canFindImage_whenFileStorageAndImageSavedCallbackIsUsed(): Unit = runTest {
val saveLocation = temporaryFolder.newFile()
@@ -131,7 +128,6 @@
// Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
// need to be reflected there too
- @Ignore("b/318314454")
@Test
fun canFindFakeImageUri_whenMediaStoreAndImageSavedCallbackIsUsed(): Unit = runBlocking {
val callback = FakeOnImageSavedCallback()
diff --git a/car/app/app/api/1.7.0-beta02.txt b/car/app/app/api/1.7.0-beta02.txt
index 4458c04..6939bce 100644
--- a/car/app/app/api/1.7.0-beta02.txt
+++ b/car/app/app/api/1.7.0-beta02.txt
@@ -900,9 +900,20 @@
field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+ field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
}
+ public class MediaIntentExtras {
+ field public static final String ACTION_MEDIA_TEMPLATE_V2 = "androidx.car.app.mediaextensions.action.MEDIA_TEMPLATE_V2";
+ field public static final String EXTRA_KEY_MEDIA_COMPONENT = "android.car.intent.extra.MEDIA_COMPONENT";
+ field public static final String EXTRA_KEY_MEDIA_ID = "androidx.car.app.mediaextensions.extra.KEY_MEDIA_ID";
+ field public static final String EXTRA_KEY_SEARCH_ACTION = "androidx.car.app.mediaextensions.extra.KEY_SEARCH_ACTION";
+ field public static final String EXTRA_KEY_SEARCH_QUERY = "android.car.media.extra.SEARCH_QUERY";
+ field public static final int EXTRA_VALUE_NO_SEARCH_ACTION = 0; // 0x0
+ field public static final int EXTRA_VALUE_PLAY_FIRST_ITEM_FROM_SEARCH = 1; // 0x1
+ }
+
public final class MetadataExtras {
field public static final String KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI";
field public static final String KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI";
diff --git a/car/app/app/api/current.ignore b/car/app/app/api/current.ignore
new file mode 100644
index 0000000..e1d8307
--- /dev/null
+++ b/car/app/app/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+AddedField: androidx.car.app.mediaextensions.MediaBrowserExtras#KEY_ROOT_HINT_MEDIA_HOST_VERSION:
+ Added field androidx.car.app.mediaextensions.MediaBrowserExtras.KEY_ROOT_HINT_MEDIA_HOST_VERSION
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 4458c04..6939bce 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -900,9 +900,20 @@
field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+ field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
}
+ public class MediaIntentExtras {
+ field public static final String ACTION_MEDIA_TEMPLATE_V2 = "androidx.car.app.mediaextensions.action.MEDIA_TEMPLATE_V2";
+ field public static final String EXTRA_KEY_MEDIA_COMPONENT = "android.car.intent.extra.MEDIA_COMPONENT";
+ field public static final String EXTRA_KEY_MEDIA_ID = "androidx.car.app.mediaextensions.extra.KEY_MEDIA_ID";
+ field public static final String EXTRA_KEY_SEARCH_ACTION = "androidx.car.app.mediaextensions.extra.KEY_SEARCH_ACTION";
+ field public static final String EXTRA_KEY_SEARCH_QUERY = "android.car.media.extra.SEARCH_QUERY";
+ field public static final int EXTRA_VALUE_NO_SEARCH_ACTION = 0; // 0x0
+ field public static final int EXTRA_VALUE_PLAY_FIRST_ITEM_FROM_SEARCH = 1; // 0x1
+ }
+
public final class MetadataExtras {
field public static final String KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI";
field public static final String KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI";
diff --git a/car/app/app/api/restricted_1.7.0-beta02.txt b/car/app/app/api/restricted_1.7.0-beta02.txt
index 4458c04..6939bce 100644
--- a/car/app/app/api/restricted_1.7.0-beta02.txt
+++ b/car/app/app/api/restricted_1.7.0-beta02.txt
@@ -900,9 +900,20 @@
field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+ field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
}
+ public class MediaIntentExtras {
+ field public static final String ACTION_MEDIA_TEMPLATE_V2 = "androidx.car.app.mediaextensions.action.MEDIA_TEMPLATE_V2";
+ field public static final String EXTRA_KEY_MEDIA_COMPONENT = "android.car.intent.extra.MEDIA_COMPONENT";
+ field public static final String EXTRA_KEY_MEDIA_ID = "androidx.car.app.mediaextensions.extra.KEY_MEDIA_ID";
+ field public static final String EXTRA_KEY_SEARCH_ACTION = "androidx.car.app.mediaextensions.extra.KEY_SEARCH_ACTION";
+ field public static final String EXTRA_KEY_SEARCH_QUERY = "android.car.media.extra.SEARCH_QUERY";
+ field public static final int EXTRA_VALUE_NO_SEARCH_ACTION = 0; // 0x0
+ field public static final int EXTRA_VALUE_PLAY_FIRST_ITEM_FROM_SEARCH = 1; // 0x1
+ }
+
public final class MetadataExtras {
field public static final String KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI";
field public static final String KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI";
diff --git a/car/app/app/api/restricted_current.ignore b/car/app/app/api/restricted_current.ignore
new file mode 100644
index 0000000..e1d8307
--- /dev/null
+++ b/car/app/app/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+AddedField: androidx.car.app.mediaextensions.MediaBrowserExtras#KEY_ROOT_HINT_MEDIA_HOST_VERSION:
+ Added field androidx.car.app.mediaextensions.MediaBrowserExtras.KEY_ROOT_HINT_MEDIA_HOST_VERSION
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index 4458c04..6939bce 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -900,9 +900,20 @@
field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+ field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
}
+ public class MediaIntentExtras {
+ field public static final String ACTION_MEDIA_TEMPLATE_V2 = "androidx.car.app.mediaextensions.action.MEDIA_TEMPLATE_V2";
+ field public static final String EXTRA_KEY_MEDIA_COMPONENT = "android.car.intent.extra.MEDIA_COMPONENT";
+ field public static final String EXTRA_KEY_MEDIA_ID = "androidx.car.app.mediaextensions.extra.KEY_MEDIA_ID";
+ field public static final String EXTRA_KEY_SEARCH_ACTION = "androidx.car.app.mediaextensions.extra.KEY_SEARCH_ACTION";
+ field public static final String EXTRA_KEY_SEARCH_QUERY = "android.car.media.extra.SEARCH_QUERY";
+ field public static final int EXTRA_VALUE_NO_SEARCH_ACTION = 0; // 0x0
+ field public static final int EXTRA_VALUE_PLAY_FIRST_ITEM_FROM_SEARCH = 1; // 0x1
+ }
+
public final class MetadataExtras {
field public static final String KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI";
field public static final String KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI";
diff --git a/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java
index 473f447..413cd71 100644
--- a/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java
+++ b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java
@@ -37,6 +37,17 @@
/**
* {@link Bundle} key used in the rootHints bundle passed to
+ * {@link androidx.media.MediaBrowserServiceCompat#onGetRoot(String, int, Bundle)}
+ * to indicate the version of the caller. Note that this should only be used for analytics and
+ * is different than {@link #KEY_ROOT_HINT_MEDIA_SESSION_API}.
+ *
+ * <p>TYPE: string - the version info.
+ */
+ public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION =
+ "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
+
+ /**
+ * {@link Bundle} key used in the rootHints bundle passed to
* {@link androidx.media.MediaBrowserServiceCompat#onGetRoot(String, int, Bundle)} to indicate
* which version of the media api is used by the caller
*
diff --git a/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaIntentExtras.java b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaIntentExtras.java
new file mode 100644
index 0000000..88bcb08
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaIntentExtras.java
@@ -0,0 +1,104 @@
+/*
+ * 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.car.app.mediaextensions;
+
+/**
+ * Defines constants for action and extra keys for CarMediaApp.
+ */
+public class MediaIntentExtras {
+
+ // Do not instantiate
+ private MediaIntentExtras() {
+ }
+
+ /**
+ * Activity Action: Provide media playing through a media template app. The usage is the same as
+ * <a href="https://developer.android.com/reference/android/car/media/CarMediaIntents#ACTION_MEDIA_TEMPLATE">ACTION_MEDIA_TEMPLATE</a>
+ * A V2 is provided so that the media apps can know whether the system they run on supports the
+ * new parameters.
+ * <p> Input: these optional extras
+ * <ul>
+ * <li> {@link #EXTRA_KEY_MEDIA_COMPONENT} </li>
+ * <li> {@link #EXTRA_KEY_MEDIA_ID} </li>
+ * <li> {@link #EXTRA_KEY_SEARCH_QUERY} </li>
+ * <li> {@link #EXTRA_KEY_SEARCH_ACTION} </li>
+ * </ul>
+ * If no extra is specified, the current media source is opened.
+ */
+ public static final String ACTION_MEDIA_TEMPLATE_V2 =
+ "androidx.car.app.mediaextensions.action.MEDIA_TEMPLATE_V2";
+
+ /**
+ * {@link Bundle} key used as a string extra field with {@link #ACTION_MEDIA_TEMPLATE_V2} to
+ * specify the MediaBrowserService that user wants to start the media on.
+ * <p>TYPE: String.
+ * The value of this extra is the same as the
+ * <a href="https://developer.android.com/reference/android/car/media/CarMediaIntents#EXTRA_MEDIA_COMPONENT">EXTRA_MEDIA_COMPONENT</a>
+ * for easy access for 3P developers.
+ */
+ @SuppressWarnings("ActionValue")
+ public static final String EXTRA_KEY_MEDIA_COMPONENT =
+ "android.car.intent.extra.MEDIA_COMPONENT";
+
+ /**
+ * {@link Bundle} key used as a string extra field with {@link #ACTION_MEDIA_TEMPLATE_V2} to
+ * specify the media item that should be displayed in the browse view. Must match the ids used
+ * in the MediaBrowserServiceCompat api.
+ * <p>TYPE: String.
+ */
+ public static final String EXTRA_KEY_MEDIA_ID =
+ "androidx.car.app.mediaextensions.extra.KEY_MEDIA_ID";
+
+ /**
+ * {@link Bundle} key used as a string extra field with {@link #ACTION_MEDIA_TEMPLATE_V2} to
+ * specify the search query to send either to the current MediaBrowserService or the one
+ * specified with {@link #EXTRA_KEY_MEDIA_COMPONENT}.
+ * <p>TYPE: String.
+ * The value of this extra is the same as the
+ * <a href="https://developer.android.com/reference/android/car/media/CarMediaIntents#EXTRA_SEARCH_QUERY">EXTRA_SEARCH_QUERY</a>
+ * for easy access for 3P developers.
+ */
+ @SuppressWarnings("ActionValue")
+ public static final String EXTRA_KEY_SEARCH_QUERY =
+ "android.car.media.extra.SEARCH_QUERY";
+
+ /**
+ * {@link Bundle} key used as an int extra field with {@link #ACTION_MEDIA_TEMPLATE_V2} to
+ * specify the action for the Media Center to do after the search query is loaded.
+ * <p>TYPE: int.
+ * The value will be one of the following:
+ * {@link #EXTRA_VALUE_NO_SEARCH_ACTION},
+ * {@link #EXTRA_VALUE_PLAY_FIRST_ITEM_FROM_SEARCH},
+ * This extra should only be used together with {@link #EXTRA_KEY_SEARCH_QUERY}. If this extra
+ * is not specified, then no further action will be taken after the search results are loaded.
+ */
+ public static final String EXTRA_KEY_SEARCH_ACTION =
+ "androidx.car.app.mediaextensions.extra.KEY_SEARCH_ACTION";
+
+ /**
+ * The extra value to indicate that no further action will be taken after the search results are
+ * loaded from a search query. Used with {@link #EXTRA_KEY_SEARCH_QUERY}.
+ */
+ public static final int EXTRA_VALUE_NO_SEARCH_ACTION = 0;
+
+ /**
+ * The extra value to indicate that the first playable item will automatically be played from
+ * the displayed search results after a search query is done. Used with
+ * {@link #EXTRA_KEY_SEARCH_QUERY}.
+ */
+ public static final int EXTRA_VALUE_PLAY_FIRST_ITEM_FROM_SEARCH = 1;
+}
diff --git a/compose/material3/adaptive/adaptive-layout/build.gradle b/compose/material3/adaptive/adaptive-layout/build.gradle
index f37fbdb..2f637a0 100644
--- a/compose/material3/adaptive/adaptive-layout/build.gradle
+++ b/compose/material3/adaptive/adaptive-layout/build.gradle
@@ -111,10 +111,6 @@
samples(project(":compose:material3:adaptive:adaptive-samples"))
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
// Screenshot tests related setup
android {
compileSdk 35
diff --git a/compose/material3/adaptive/adaptive-navigation/build.gradle b/compose/material3/adaptive/adaptive-navigation/build.gradle
index dbbcdc2..7992414 100644
--- a/compose/material3/adaptive/adaptive-navigation/build.gradle
+++ b/compose/material3/adaptive/adaptive-navigation/build.gradle
@@ -109,10 +109,6 @@
metalavaK2UastEnabled = false
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
// Screenshot tests related setup
android {
compileSdk 35
diff --git a/compose/material3/adaptive/adaptive/build.gradle b/compose/material3/adaptive/adaptive/build.gradle
index 1cb594e..24f0da1 100644
--- a/compose/material3/adaptive/adaptive/build.gradle
+++ b/compose/material3/adaptive/adaptive/build.gradle
@@ -109,10 +109,6 @@
metalavaK2UastEnabled = false
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
// Screenshot tests related setup
android {
compileSdk 35
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
index d57c4a2..da0d64f 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
@@ -24,6 +24,8 @@
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.FavoriteBorder
+import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Alignment
@@ -190,7 +192,7 @@
rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
IconToggleButton(checked = false, onCheckedChange = { /* doSomething() */ }) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -202,7 +204,7 @@
rule.setMaterialContent(darkColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
IconToggleButton(checked = false, onCheckedChange = { /* doSomething() */ }) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -274,7 +276,7 @@
rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
FilledIconToggleButton(checked = false, onCheckedChange = { /* doSomething() */ }) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -290,7 +292,7 @@
onCheckedChange = { /* doSomething() */ },
enabled = false
) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -302,7 +304,7 @@
rule.setMaterialContent(darkColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
FilledIconToggleButton(checked = false, onCheckedChange = { /* doSomething() */ }) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -377,7 +379,7 @@
checked = false,
onCheckedChange = { /* doSomething() */ }
) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -392,7 +394,7 @@
checked = false,
onCheckedChange = { /* doSomething() */ }
) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -434,10 +436,7 @@
rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
OutlinedIconButton(onClick = { /* doSomething() */ }) {
- Icon(
- Icons.Outlined.FavoriteBorder,
- contentDescription = "Localized description"
- )
+ Icon(Icons.Filled.FavoriteBorder, contentDescription = "Localized description")
}
}
}
@@ -449,10 +448,7 @@
rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
OutlinedIconButton(onClick = { /* doSomething() */ }, enabled = false) {
- Icon(
- Icons.Outlined.FavoriteBorder,
- contentDescription = "Localized description"
- )
+ Icon(Icons.Filled.FavoriteBorder, contentDescription = "Localized description")
}
}
}
@@ -464,10 +460,7 @@
rule.setMaterialContent(darkColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
OutlinedIconButton(onClick = { /* doSomething() */ }) {
- Icon(
- Icons.Outlined.FavoriteBorder,
- contentDescription = "Localized description"
- )
+ Icon(Icons.Filled.FavoriteBorder, contentDescription = "Localized description")
}
}
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
index 06b13ec..bfe9fa0 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
@@ -23,7 +23,8 @@
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Edit
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.outlined.KeyboardArrowDown
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Alignment
@@ -33,6 +34,7 @@
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
@@ -52,6 +54,8 @@
private val wrap = Modifier.wrapContentSize(Alignment.Center)
private val wrapperTestTag = "splitButtonWrapper"
+ private val leadingButtonTag = "leadingButton"
+ private val trailingButtonTag = "trailingButton"
@Test
fun splitButton() {
@@ -63,7 +67,7 @@
onClick = { /* Do Nothing */ },
) {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
contentDescription = "Localized description",
)
@@ -100,7 +104,7 @@
checked = false,
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
@@ -136,7 +140,7 @@
onTrailingButtonClick = {},
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
@@ -145,7 +149,7 @@
},
trailingContent = {
Icon(
- Icons.Outlined.KeyboardArrowDown,
+ Icons.Filled.KeyboardArrowDown,
modifier =
Modifier.size(SplitButtonDefaults.TrailingIconSize).graphicsLayer {
this.rotationZ = 180f
@@ -170,7 +174,7 @@
checked = false,
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
@@ -203,7 +207,7 @@
checked = false,
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
@@ -236,7 +240,7 @@
checked = false,
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
@@ -269,7 +273,7 @@
onClick = { /* Do Nothing */ },
) {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
@@ -322,6 +326,82 @@
assertAgainstGolden("splitButton_textLeadingButton_${scheme.name}")
}
+ @Test
+ fun splitButton_leadingButton_pressed() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ SplitButton(
+ leadingButton = {
+ SplitButtonDefaults.LeadingButton(
+ onClick = { /* Do Nothing */ },
+ modifier = Modifier.testTag(leadingButtonTag),
+ ) {
+ Icon(
+ Icons.Filled.Edit,
+ modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
+ contentDescription = "Localized description",
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text("My Button")
+ }
+ },
+ trailingButton = {
+ SplitButtonDefaults.TrailingButton(
+ onClick = {},
+ checked = false,
+ ) {
+ Icon(
+ Icons.Outlined.KeyboardArrowDown,
+ contentDescription = "Localized description",
+ Modifier.size(SplitButtonDefaults.TrailingIconSize)
+ )
+ }
+ }
+ )
+ }
+ }
+
+ assertPressed(leadingButtonTag, "splitButton_leadingButton_pressed_${scheme.name}")
+ }
+
+ @Test
+ fun splitButton_trailingButton_pressed() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ SplitButton(
+ leadingButton = {
+ SplitButtonDefaults.LeadingButton(
+ onClick = { /* Do Nothing */ },
+ ) {
+ Icon(
+ Icons.Filled.Edit,
+ modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
+ contentDescription = "Localized description",
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text("My Button")
+ }
+ },
+ trailingButton = {
+ SplitButtonDefaults.TrailingButton(
+ onClick = {},
+ checked = false,
+ modifier = Modifier.testTag(trailingButtonTag),
+ ) {
+ Icon(
+ Icons.Outlined.KeyboardArrowDown,
+ contentDescription = "Localized description",
+ Modifier.size(SplitButtonDefaults.TrailingIconSize)
+ )
+ }
+ }
+ )
+ }
+ }
+
+ assertPressed(trailingButtonTag, "splitButton_trailingButton_pressed_${scheme.name}")
+ }
+
private fun assertAgainstGolden(goldenName: String) {
rule
.onNodeWithTag(wrapperTestTag)
@@ -329,6 +409,21 @@
.assertAgainstGolden(screenshotRule, goldenName)
}
+ private fun assertPressed(tag: String, goldenName: String) {
+ rule.mainClock.autoAdvance = false
+ rule.onNodeWithTag(tag).performTouchInput { down(center) }
+
+ rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle() // Wait for measure
+ rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+ // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
+ // synchronization. Instead just wait until after the ripples are finished animating.
+ Thread.sleep(300)
+
+ assertAgainstGolden(goldenName)
+ }
+
// Provide the ColorScheme and their name parameter in a ColorSchemeWrapper.
// This makes sure that the default method name and the initial Scuba image generated
// name is as expected.
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonScreenshotTest.kt
index d6dcd91..a699d92 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonScreenshotTest.kt
@@ -22,6 +22,7 @@
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
@@ -448,7 +449,7 @@
Box(Modifier.testTag(wrapperTestTag)) {
ToggleButton(checked = false, onCheckedChange = {}) {
Icon(
- Icons.Filled.Favorite,
+ Icons.Outlined.Favorite,
contentDescription = "Localized description",
modifier = Modifier.size(ToggleButtonDefaults.IconSize)
)
@@ -466,7 +467,7 @@
Box(Modifier.testTag(wrapperTestTag)) {
ToggleButton(checked = false, onCheckedChange = {}, enabled = false) {
Icon(
- Icons.Filled.Favorite,
+ Icons.Outlined.Favorite,
contentDescription = "Localized description",
modifier = Modifier.size(ToggleButtonDefaults.IconSize)
)
@@ -484,7 +485,7 @@
Box(Modifier.testTag(wrapperTestTag)) {
ToggleButton(checked = false, onCheckedChange = {}) {
Icon(
- Icons.Filled.Favorite,
+ Icons.Outlined.Favorite,
contentDescription = "Localized description",
modifier = Modifier.size(ToggleButtonDefaults.IconSize)
)
@@ -539,7 +540,7 @@
Box(Modifier.testTag(wrapperTestTag)) {
ToggleButton(checked = false, onCheckedChange = {}) {
Icon(
- Icons.Filled.Favorite,
+ Icons.Outlined.Favorite,
contentDescription = "Localized description",
modifier = Modifier.size(ToggleButtonDefaults.IconSize)
)
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
index 2ca56a9..bf7443d 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
@@ -697,7 +697,7 @@
initialValue = A,
defaultPositionalThreshold,
defaultVelocityThreshold,
- animationSpec = defaultAnimationSpec,
+ animationSpec = defaultAnimationSpec
)
anchoredDraggableState.updateAnchors(
DraggableAnchors {
@@ -735,40 +735,6 @@
dragJob.cancel()
}
- @Test
- fun anchoredDraggable_anchoredDrag_doesNotUpdateOnConfirmValueChange() = runTest {
- val anchoredDraggableState =
- AnchoredDraggableState(
- initialValue = B,
- defaultPositionalThreshold,
- defaultVelocityThreshold,
- animationSpec = defaultAnimationSpec,
- confirmValueChange = { false }
- )
- anchoredDraggableState.updateAnchors(
- DraggableAnchors {
- A at 0f
- B at 200f
- }
- )
-
- assertThat(anchoredDraggableState.targetValue).isEqualTo(B)
-
- val unexpectedTarget = A
- val targetUpdates = Channel<Float>()
- val dragJob =
- launch(Dispatchers.Unconfined) {
- anchoredDraggableState.anchoredDrag(unexpectedTarget) { anchors, latestTarget ->
- targetUpdates.send(anchors.positionOf(latestTarget))
- suspendIndefinitely()
- }
- }
-
- val firstTarget = targetUpdates.receive()
- assertThat(firstTarget).isEqualTo(200f)
- dragJob.cancel()
- }
-
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun anchoredDraggable_dragCompletesExceptionally_cleansUp() = runTest {
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 6f5dfbc..4ebaa83 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
@@ -380,8 +380,14 @@
modifier =
Modifier.offset {
drawerState.currentOffset.let { offset ->
- if (offset.isNaN()) IntOffset.Zero
- else IntOffset(offset.roundToInt(), 0)
+ val offsetX =
+ when {
+ !offset.isNaN() -> offset.roundToInt()
+ // If offset is NaN, set offset based on open/closed state
+ drawerState.isOpen -> 0
+ else -> -DrawerDefaults.MaximumDrawerWidth.roundToPx()
+ }
+ IntOffset(offsetX, 0)
}
}
.semantics {
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 4cf6819..63959a5 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
@@ -477,7 +477,7 @@
/** Default size for the leading button end corners and trailing button start corners */
// TODO update token to dp size and use it here
val InnerCornerSize = SplitButtonSmallTokens.InnerCornerSize
- private val InnerCornerSizePressed = ShapeDefaults.CornerSmall
+ private val InnerCornerSizePressed = ShapeDefaults.CornerMedium
/**
* Default percentage size for the leading button start corners and trailing button end corners
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
index 09b667b..fa71348 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
@@ -565,7 +565,7 @@
if (anchors.hasAnchorFor(targetValue)) {
try {
dragMutex.mutate(dragPriority) {
- dragTarget = if (confirmValueChange(targetValue)) targetValue else currentValue
+ dragTarget = targetValue
restartable(inputs = { anchors to [email protected] }) {
(latestAnchors, latestTarget) ->
anchoredDragScope.block(latestAnchors, latestTarget)
diff --git a/compose/runtime/runtime-test-utils/src/commonMain/kotlin/androidx/compose/runtime/mock/ViewApplier.kt b/compose/runtime/runtime-test-utils/src/commonMain/kotlin/androidx/compose/runtime/mock/ViewApplier.kt
index 4b1b684..e92c777 100644
--- a/compose/runtime/runtime-test-utils/src/commonMain/kotlin/androidx/compose/runtime/mock/ViewApplier.kt
+++ b/compose/runtime/runtime-test-utils/src/commonMain/kotlin/androidx/compose/runtime/mock/ViewApplier.kt
@@ -20,6 +20,8 @@
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
class ViewApplier(root: View) : AbstractApplier<View>(root) {
+ var called = false
+
var onBeginChangesCalled = 0
private set
@@ -28,29 +30,53 @@
override fun insertTopDown(index: Int, instance: View) {
// Ignored as the tree is built bottom-up.
+ called = true
}
override fun insertBottomUp(index: Int, instance: View) {
current.addAt(index, instance)
+ called = true
}
override fun remove(index: Int, count: Int) {
current.removeAt(index, count)
+ called = true
}
override fun move(from: Int, to: Int, count: Int) {
current.moveAt(from, to, count)
+ called = true
}
override fun onClear() {
root.removeAllChildren()
+ called = true
}
override fun onBeginChanges() {
onBeginChangesCalled++
+ called = true
}
override fun onEndChanges() {
onEndChangesCalled++
+ called = true
+ }
+
+ override var current: View
+ get() = super.current.also { if (it != root) called = true }
+ set(value) {
+ super.current = value
+ called = true
+ }
+
+ override fun down(node: View) {
+ super.down(node)
+ called = true
+ }
+
+ override fun up() {
+ super.up()
+ called = true
}
}
diff --git a/compose/runtime/runtime/api/current.ignore b/compose/runtime/runtime/api/current.ignore
index fc9bb64..f02ef5a 100644
--- a/compose/runtime/runtime/api/current.ignore
+++ b/compose/runtime/runtime/api/current.ignore
@@ -1,19 +1,3 @@
// Baseline format: 1.0
-AddedAbstractMethod: androidx.compose.runtime.Composer#endReplaceGroup():
- Added method androidx.compose.runtime.Composer.endReplaceGroup()
-AddedAbstractMethod: androidx.compose.runtime.Composer#startReplaceGroup(int):
- Added method androidx.compose.runtime.Composer.startReplaceGroup(int)
-AddedAbstractMethod: androidx.compose.runtime.ControlledComposition#abandonChanges():
- Added method androidx.compose.runtime.ControlledComposition.abandonChanges()
-
-
-InvalidNullConversion: androidx.compose.runtime.ComposablesKt#key(Object[], kotlin.jvm.functions.Function0<? extends T>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.ComposablesKt.key(Object[] keys, kotlin.jvm.functions.Function0<? extends T> block)
-InvalidNullConversion: androidx.compose.runtime.ComposablesKt#remember(Object[], kotlin.jvm.functions.Function0<? extends T>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.ComposablesKt.remember(Object[] keys, kotlin.jvm.functions.Function0<? extends T> calculation)
-InvalidNullConversion: androidx.compose.runtime.EffectsKt#DisposableEffect(Object[], kotlin.jvm.functions.Function1<? super androidx.compose.runtime.DisposableEffectScope,? extends androidx.compose.runtime.DisposableEffectResult>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.EffectsKt.DisposableEffect(Object[] keys, kotlin.jvm.functions.Function1<? super androidx.compose.runtime.DisposableEffectScope,? extends androidx.compose.runtime.DisposableEffectResult> effect)
-InvalidNullConversion: androidx.compose.runtime.EffectsKt#LaunchedEffect(Object[], kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.EffectsKt.LaunchedEffect(Object[] keys, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block)
-InvalidNullConversion: androidx.compose.runtime.SnapshotStateKt#produceState(T, Object[], kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>) parameter #1:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.SnapshotStateKt.produceState(T initialValue, Object[] keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer)
+AddedAbstractMethod: androidx.compose.runtime.ControlledComposition#setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>):
+ Added method androidx.compose.runtime.ControlledComposition.setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>)
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 110569b..9212ade 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -22,6 +22,7 @@
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface Applier<N> {
+ method public default void apply(kotlin.jvm.functions.Function2<? super N,java.lang.Object?,kotlin.Unit> block, Object? value);
method public void clear();
method public void down(N node);
method public N getCurrent();
@@ -31,6 +32,7 @@
method public default void onBeginChanges();
method public default void onEndChanges();
method public void remove(int index, int count);
+ method public default void reuse();
method public void up();
property public abstract N current;
}
@@ -286,6 +288,7 @@
method public void recordModificationsOf(java.util.Set<?> values);
method public void recordReadOf(Object value);
method public void recordWriteOf(Object value);
+ method public kotlin.jvm.functions.Function0<java.lang.Boolean>? setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>? shouldPause);
method @SuppressCompatibility @androidx.compose.runtime.InternalComposeApi public void verifyConsistent();
property public abstract boolean hasPendingChanges;
property public abstract boolean isComposing;
@@ -459,6 +462,15 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface NonSkippableComposable {
}
+ public interface PausableComposition extends androidx.compose.runtime.ReusableComposition {
+ method public androidx.compose.runtime.PausedComposition setPausableContent(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method public androidx.compose.runtime.PausedComposition setPausableContentWithReuse(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
+ public final class PausableCompositionKt {
+ method public static androidx.compose.runtime.PausableComposition PausableComposition(androidx.compose.runtime.Applier<? extends java.lang.Object?> applier, androidx.compose.runtime.CompositionContext parent);
+ }
+
public final class PausableMonotonicFrameClock implements androidx.compose.runtime.MonotonicFrameClock {
ctor public PausableMonotonicFrameClock(androidx.compose.runtime.MonotonicFrameClock frameClock);
method public boolean isPaused();
@@ -468,6 +480,14 @@
property public final boolean isPaused;
}
+ public interface PausedComposition {
+ method public void apply();
+ method public void cancel();
+ method public boolean isComplete();
+ method public boolean resume(kotlin.jvm.functions.Function0<java.lang.Boolean> shouldPause);
+ property public abstract boolean isComplete;
+ }
+
public final class PrimitiveSnapshotStateKt {
method public static inline operator float getValue(androidx.compose.runtime.FloatState, Object? thisObj, kotlin.reflect.KProperty<? extends java.lang.Object?> property);
method @androidx.compose.runtime.snapshots.StateFactoryMarker public static androidx.compose.runtime.MutableFloatState mutableFloatStateOf(float value);
diff --git a/compose/runtime/runtime/api/restricted_current.ignore b/compose/runtime/runtime/api/restricted_current.ignore
index 5826792..f02ef5a 100644
--- a/compose/runtime/runtime/api/restricted_current.ignore
+++ b/compose/runtime/runtime/api/restricted_current.ignore
@@ -1,23 +1,3 @@
// Baseline format: 1.0
-AddedAbstractMethod: androidx.compose.runtime.Composer#endReplaceGroup():
- Added method androidx.compose.runtime.Composer.endReplaceGroup()
-AddedAbstractMethod: androidx.compose.runtime.Composer#startReplaceGroup(int):
- Added method androidx.compose.runtime.Composer.startReplaceGroup(int)
-AddedAbstractMethod: androidx.compose.runtime.ControlledComposition#abandonChanges():
- Added method androidx.compose.runtime.ControlledComposition.abandonChanges()
-
-
-InvalidNullConversion: androidx.compose.runtime.ComposablesKt#key(Object[], kotlin.jvm.functions.Function0<? extends T>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.ComposablesKt.key(Object[] keys, kotlin.jvm.functions.Function0<? extends T> block)
-InvalidNullConversion: androidx.compose.runtime.ComposablesKt#remember(Object[], kotlin.jvm.functions.Function0<? extends T>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.ComposablesKt.remember(Object[] keys, kotlin.jvm.functions.Function0<? extends T> calculation)
-InvalidNullConversion: androidx.compose.runtime.EffectsKt#DisposableEffect(Object[], kotlin.jvm.functions.Function1<? super androidx.compose.runtime.DisposableEffectScope,? extends androidx.compose.runtime.DisposableEffectResult>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.EffectsKt.DisposableEffect(Object[] keys, kotlin.jvm.functions.Function1<? super androidx.compose.runtime.DisposableEffectScope,? extends androidx.compose.runtime.DisposableEffectResult> effect)
-InvalidNullConversion: androidx.compose.runtime.EffectsKt#LaunchedEffect(Object[], kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.EffectsKt.LaunchedEffect(Object[] keys, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block)
-InvalidNullConversion: androidx.compose.runtime.SnapshotStateKt#produceState(T, Object[], kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>) parameter #1:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.SnapshotStateKt.produceState(T initialValue, Object[] keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer)
-
-
-RemovedClass: androidx.compose.runtime.ExpectKt:
- Removed class androidx.compose.runtime.ExpectKt
+AddedAbstractMethod: androidx.compose.runtime.ControlledComposition#setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>):
+ Added method androidx.compose.runtime.ControlledComposition.setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>)
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 186ecc4..9580ec3 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -26,6 +26,7 @@
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface Applier<N> {
+ method public default void apply(kotlin.jvm.functions.Function2<? super N,java.lang.Object?,kotlin.Unit> block, Object? value);
method public void clear();
method public void down(N node);
method public N getCurrent();
@@ -35,6 +36,7 @@
method public default void onBeginChanges();
method public default void onEndChanges();
method public void remove(int index, int count);
+ method public default void reuse();
method public void up();
property public abstract N current;
}
@@ -313,6 +315,7 @@
method public void recordModificationsOf(java.util.Set<?> values);
method public void recordReadOf(Object value);
method public void recordWriteOf(Object value);
+ method public kotlin.jvm.functions.Function0<java.lang.Boolean>? setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>? shouldPause);
method @SuppressCompatibility @androidx.compose.runtime.InternalComposeApi public void verifyConsistent();
property public abstract boolean hasPendingChanges;
property public abstract boolean isComposing;
@@ -487,6 +490,15 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface NonSkippableComposable {
}
+ public interface PausableComposition extends androidx.compose.runtime.ReusableComposition {
+ method public androidx.compose.runtime.PausedComposition setPausableContent(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method public androidx.compose.runtime.PausedComposition setPausableContentWithReuse(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
+ public final class PausableCompositionKt {
+ method public static androidx.compose.runtime.PausableComposition PausableComposition(androidx.compose.runtime.Applier<? extends java.lang.Object?> applier, androidx.compose.runtime.CompositionContext parent);
+ }
+
public final class PausableMonotonicFrameClock implements androidx.compose.runtime.MonotonicFrameClock {
ctor public PausableMonotonicFrameClock(androidx.compose.runtime.MonotonicFrameClock frameClock);
method public boolean isPaused();
@@ -496,6 +508,14 @@
property public final boolean isPaused;
}
+ public interface PausedComposition {
+ method public void apply();
+ method public void cancel();
+ method public boolean isComplete();
+ method public boolean resume(kotlin.jvm.functions.Function0<java.lang.Boolean> shouldPause);
+ property public abstract boolean isComplete;
+ }
+
public final class PrimitiveSnapshotStateKt {
method public static inline operator float getValue(androidx.compose.runtime.FloatState, Object? thisObj, kotlin.reflect.KProperty<? extends java.lang.Object?> property);
method @androidx.compose.runtime.snapshots.StateFactoryMarker public static androidx.compose.runtime.MutableFloatState mutableFloatStateOf(float value);
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
index 7335b2c..d298a52 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
+++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
@@ -17,6 +17,7 @@
package androidx.compose.runtime.benchmark
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
@@ -30,8 +31,13 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.text.drawText
+import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp
import androidx.test.annotation.UiThreadTest
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -236,6 +242,21 @@
fun benchmark_f_compose_Rect_100() = runBlockingTestWithFrameClock {
measureComposeFocused { repeat(100) { Rect() } }
}
+
+ @UiThreadTest
+ @Test
+ fun benchmark_g_group_eliding_focused_1000() = runBlockingTestWithFrameClock {
+ measureCompose { repeat(1000) { MyLayout { SimpleText("Value: $it") } } }
+ }
+}
+
+@Composable
+fun MyLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
+ Layout(content = content, measurePolicy = EmptyMeasurePolicy, modifier = modifier)
+}
+
+internal val EmptyMeasurePolicy = MeasurePolicy { _, constraints ->
+ layout(constraints.minWidth, constraints.minHeight) {}
}
class ColorModel(color: Color = Color.Black) {
@@ -254,6 +275,12 @@
}
@Composable
+private fun SimpleText(text: String) {
+ val measurer = rememberTextMeasurer()
+ Box(modifier = Modifier.drawBehind { drawText(measurer, text) })
+}
+
+@Composable
private fun Rect(color: Color) {
val modifier = remember(color) { Modifier.background(color) }
Column(modifier) {}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
index e1cd951..5b33661 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
@@ -174,6 +174,16 @@
* root to be used as the target of a new composition in the future.
*/
fun clear()
+
+ /** Apply a change to the current node. */
+ fun apply(block: N.(Any?) -> Unit, value: Any?) {
+ current.block(value)
+ }
+
+ /** Notify [current] is is being reused in reusable content. */
+ fun reuse() {
+ (current as? ComposeNodeLifecycleCallback)?.onReuse()
+ }
}
/**
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index 28f2850..172e582 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -101,6 +101,15 @@
priority: Int,
endRelativeAfter: Int
)
+
+ /** The restart scope is pausing */
+ fun rememberPausingScope(scope: RecomposeScopeImpl)
+
+ /** The restart scope is resuming */
+ fun startResumingScope(scope: RecomposeScopeImpl)
+
+ /** The restart scope is finished resuming */
+ fun endResumingScope(scope: RecomposeScopeImpl)
}
/**
@@ -1356,6 +1365,9 @@
private var insertAnchor: Anchor = insertTable.read { it.anchor(0) }
private var insertFixups = FixupList()
+ private var pausable: Boolean = false
+ private var shouldPauseCallback: (() -> Boolean)? = null
+
override val applyCoroutineContext: CoroutineContext
@TestOnly get() = parentContext.effectCoroutineContext
@@ -2726,7 +2738,10 @@
providerCache = null
// Invoke the scope's composition function
+ val shouldRestartReusing = !reusing && firstInRange.scope.reusing
+ if (shouldRestartReusing) reusing = true
firstInRange.scope.compose(this)
+ if (shouldRestartReusing) reusing = false
// We could have moved out of a provider so the provider cache is invalid.
providerCache = null
@@ -3038,8 +3053,34 @@
}
@ComposeCompilerApi
- @Suppress("UNUSED")
override fun shouldExecute(parametersChanged: Boolean, flags: Int): Boolean {
+ // We only want to pause when we are not resuming and only when inserting new content or
+ // when reusing content. This 0 bit of `flags` is only 1 if this function was restarted by
+ // the restart lambda. The other bits of this flags are currently all 0's and are reserved
+ // for future use.
+ if (((flags and 1) == 0) && (inserting || reusing)) {
+ val callback = shouldPauseCallback ?: return true
+ val scope = currentRecomposeScope ?: return true
+ val pausing = callback()
+ if (pausing) {
+ scope.used = true
+ // Force the composer back into the reusing state when this scope restarts.
+ scope.reusing = reusing
+ scope.paused = true
+ // Remember a place-holder object to ensure all remembers are sent in the correct
+ // order. The remember manager will record the remember callback for the resumed
+ // content into a place-holder to ensure that, when the remember callbacks are
+ // dispatched, the callbacks for the resumed content are dispatched in the same
+ // order they would have been had the content not paused.
+ changeListWriter.rememberPausingScope(scope)
+ parentContext.reportPausedScope(scope)
+ return false
+ }
+ return true
+ }
+
+ // Otherwise we should execute the function if the parameters have changed or when
+ // skipping is disabled.
return parametersChanged || !skipping
}
@@ -3118,6 +3159,11 @@
}
invalidateStack.push(scope)
scope.start(compositionToken)
+ if (scope.paused) {
+ scope.paused = false
+ scope.resuming = true
+ changeListWriter.startResumingScope(scope)
+ }
}
}
@@ -3133,8 +3179,16 @@
// exception stack unwinding that might have not called the doneJoin/endRestartGroup in the
// the correct order.
val scope = if (invalidateStack.isNotEmpty()) invalidateStack.pop() else null
- scope?.requiresRecompose = false
- scope?.end(compositionToken)?.let { changeListWriter.endCompositionScope(it, composition) }
+ if (scope != null) {
+ scope.requiresRecompose = false
+ scope.end(compositionToken)?.let {
+ changeListWriter.endCompositionScope(it, composition)
+ }
+ if (scope.resuming) {
+ scope.resuming = false
+ changeListWriter.endResumingScope(scope)
+ }
+ }
val result =
if (scope != null && !scope.skipped && (scope.used || forceRecomposeScopes)) {
if (scope.anchor == null) {
@@ -3438,10 +3492,16 @@
*/
internal fun composeContent(
invalidationsRequested: ScopeMap<RecomposeScopeImpl, Any>,
- content: @Composable () -> Unit
+ content: @Composable () -> Unit,
+ shouldPause: (() -> Boolean)?
) {
runtimeCheck(changes.isEmpty()) { "Expected applyChanges() to have been called" }
- doCompose(invalidationsRequested, content)
+ this.shouldPauseCallback = shouldPause
+ try {
+ doCompose(invalidationsRequested, content)
+ } finally {
+ this.shouldPauseCallback = null
+ }
}
internal fun prepareCompose(block: () -> Unit) {
@@ -3460,6 +3520,7 @@
*/
internal fun recompose(
invalidationsRequested: ScopeMap<RecomposeScopeImpl, Any>,
+ shouldPause: (() -> Boolean)?
): Boolean {
runtimeCheck(changes.isEmpty()) { "Expected applyChanges() to have been called" }
// even if invalidationsRequested is empty we still need to recompose if the Composer has
@@ -3467,7 +3528,12 @@
// there were a change for a state which was used by the child composition. such changes
// will be tracked and added into `invalidations` list.
if (invalidationsRequested.size > 0 || invalidations.isNotEmpty() || forciblyRecompose) {
- doCompose(invalidationsRequested, null)
+ shouldPauseCallback = shouldPause
+ try {
+ doCompose(invalidationsRequested, null)
+ } finally {
+ shouldPauseCallback = null
+ }
return changes.isNotEmpty()
}
return false
@@ -3786,6 +3852,10 @@
parentContext.unregisterComposition(composition)
}
+ override fun reportPausedScope(scope: RecomposeScopeImpl) {
+ parentContext.reportPausedScope(scope)
+ }
+
override val effectCoroutineContext: CoroutineContext
get() = parentContext.effectCoroutineContext
@@ -3802,6 +3872,20 @@
parentContext.composeInitial(composition, content)
}
+ override fun composeInitialPaused(
+ composition: ControlledComposition,
+ shouldPause: () -> Boolean,
+ content: @Composable () -> Unit
+ ): ScatterSet<RecomposeScopeImpl> =
+ parentContext.composeInitialPaused(composition, shouldPause, content)
+
+ override fun recomposePaused(
+ composition: ControlledComposition,
+ shouldPause: () -> Boolean,
+ invalidScopes: ScatterSet<RecomposeScopeImpl>
+ ): ScatterSet<RecomposeScopeImpl> =
+ parentContext.recomposePaused(composition, shouldPause, invalidScopes)
+
override fun invalidate(composition: ControlledComposition) {
// Invalidate ourselves with our parent before we invalidate a child composer.
// This ensures that when we are scheduling recompositions, parents always
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index e2efd82..f674dbe 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -18,13 +18,12 @@
package androidx.compose.runtime
-import androidx.collection.MutableIntList
import androidx.collection.MutableScatterSet
-import androidx.collection.mutableScatterSetOf
import androidx.compose.runtime.changelist.ChangeList
import androidx.compose.runtime.collection.ScopeMap
import androidx.compose.runtime.collection.fastForEach
import androidx.compose.runtime.internal.AtomicReference
+import androidx.compose.runtime.internal.RememberEventDispatcher
import androidx.compose.runtime.internal.trace
import androidx.compose.runtime.snapshots.ReaderKind
import androidx.compose.runtime.snapshots.StateObjectImpl
@@ -289,6 +288,29 @@
* used to compose as if the scopes have already been changed.
*/
fun <R> delegateInvalidations(to: ControlledComposition?, groupIndex: Int, block: () -> R): R
+
+ /**
+ * Sets the [shouldPause] callback allowing a composition to be pausable if it is not `null`.
+ * Setting the callback to `null` disables pausing.
+ *
+ * @return the previous value of the callback which will be restored once the callback is no
+ * longer needed.
+ * @see PausableComposition
+ */
+ fun setShouldPauseCallback(shouldPause: (() -> Boolean)?): (() -> Boolean)?
+}
+
+/** Utility function to set and restore a should pause callback. */
+internal inline fun <R> ControlledComposition.pausable(
+ noinline shouldPause: () -> Boolean,
+ block: () -> R
+): R {
+ val previous = setShouldPauseCallback(shouldPause)
+ return try {
+ block()
+ } finally {
+ setShouldPauseCallback(previous)
+ }
}
/**
@@ -409,7 +431,12 @@
/** The applier to use to update the tree managed by the composition. */
private val applier: Applier<*>,
recomposeContext: CoroutineContext? = null
-) : ControlledComposition, ReusableComposition, RecomposeScopeOwner, CompositionServices {
+) :
+ ControlledComposition,
+ ReusableComposition,
+ RecomposeScopeOwner,
+ CompositionServices,
+ PausableComposition {
/**
* `null` if a composition isn't pending to apply. `Set<Any>` or `Array<Set<Any>>` if there are
* modifications to record [PendingApplyNoModifications] if a composition is pending to apply,
@@ -520,6 +547,14 @@
@Suppress("MemberVisibilityCanBePrivate") // published as internal
internal var pendingInvalidScopes = false
+ /**
+ * If the [shouldPause] callback is set the composition is pausable and should pause whenever
+ * the [shouldPause] callback returns `true`.
+ */
+ private var shouldPause: (() -> Boolean)? = null
+
+ private var pendingPausedComposition: PausedCompositionImpl? = null
+
private var invalidationDelegate: CompositionImpl? = null
private var invalidationDelegateGroup: Int = 0
@@ -572,10 +607,16 @@
get() = synchronized(lock) { composer.hasPendingChanges }
override fun setContent(content: @Composable () -> Unit) {
+ checkPrecondition(pendingPausedComposition == null) {
+ "A pausable composition is in progress"
+ }
composeInitial(content)
}
override fun setContentWithReuse(content: @Composable () -> Unit) {
+ checkPrecondition(pendingPausedComposition == null) {
+ "A pausable composition is in progress"
+ }
composer.startReuseFromRoot()
composeInitial(content)
@@ -583,6 +624,50 @@
composer.endReuseFromRoot()
}
+ override fun setPausableContent(content: @Composable () -> Unit): PausedComposition {
+ checkPrecondition(!disposed) { "The composition is disposed" }
+ checkPrecondition(pendingPausedComposition == null) {
+ "A pausable composition is in progress"
+ }
+ val pausedComposition =
+ PausedCompositionImpl(
+ composition = this,
+ context = parent,
+ composer = composer,
+ content = content,
+ reusable = false,
+ abandonSet = abandonSet,
+ applier = applier,
+ lock = lock,
+ )
+ pendingPausedComposition = pausedComposition
+ return pausedComposition
+ }
+
+ override fun setPausableContentWithReuse(content: @Composable () -> Unit): PausedComposition {
+ checkPrecondition(!disposed) { "The composition is disposed" }
+ checkPrecondition(pendingPausedComposition == null) {
+ "A pausable composition is in progress"
+ }
+ val pausedComposition =
+ PausedCompositionImpl(
+ composition = this,
+ context = parent,
+ composer = composer,
+ content = content,
+ reusable = true,
+ abandonSet = abandonSet,
+ applier = applier,
+ lock = lock,
+ )
+ pendingPausedComposition = pausedComposition
+ return pausedComposition
+ }
+
+ internal fun pausedCompositionFinished() {
+ pendingPausedComposition = null
+ }
+
private fun composeInitial(content: @Composable () -> Unit) {
checkPrecondition(!disposed) { "The composition is disposed" }
this.composable = content
@@ -701,7 +786,7 @@
invalidations.asMap() as Map<RecomposeScope, Set<Any>>
)
}
- composer.composeContent(invalidations, content)
+ composer.composeContent(invalidations, content, shouldPause)
observer?.onEndComposition(this)
}
}
@@ -911,7 +996,7 @@
this,
invalidations.asMap() as Map<RecomposeScope, Set<Any>>
)
- composer.recompose(invalidations).also { shouldDrain ->
+ composer.recompose(invalidations, shouldPause).also { shouldDrain ->
// Apply would normally do this for us; do it now if apply shouldn't happen.
if (!shouldDrain) drainPendingModificationsLocked()
observer?.onEndComposition(this)
@@ -939,11 +1024,13 @@
try {
if (changes.isEmpty()) return
trace("Compose:applyChanges") {
+ val applier = pendingPausedComposition?.pausableApplier ?: applier
+ val rememberManager = pendingPausedComposition?.rememberManager ?: manager
applier.onBeginChanges()
// Apply all changes
slotTable.write { slots ->
- changes.executeAndFlushAllPendingChanges(applier, slots, manager)
+ changes.executeAndFlushAllPendingChanges(applier, slots, rememberManager)
}
applier.onEndChanges()
}
@@ -962,9 +1049,12 @@
}
}
} finally {
- // Only dispatch abandons if we do not have any late changes. The instances in the
- // abandon set can be remembered in the late changes.
- if (this.lateChanges.isEmpty()) manager.dispatchAbandons()
+ // Only dispatch abandons if we do not have any late changes or pending paused
+ // compositions. The instances in the abandon set can be remembered in the late changes
+ // or when the paused composition is applied.
+ if (this.lateChanges.isEmpty() && pendingPausedComposition == null) {
+ manager.dispatchAbandons()
+ }
}
}
@@ -1062,6 +1152,12 @@
} else block()
}
+ override fun setShouldPauseCallback(shouldPause: (() -> Boolean)?): (() -> Boolean)? {
+ val previous = this.shouldPause
+ this.shouldPause = shouldPause
+ return previous
+ }
+
override fun invalidate(scope: RecomposeScopeImpl, instance: Any?): InvalidationResult {
if (scope.defaultsInScope) {
scope.defaultsInvalid = true
@@ -1241,218 +1337,6 @@
// This is only used in tests to ensure the stacks do not silently leak.
internal fun composerStacksSizes(): Int = composer.stacksSize()
-
- /** Helper for collecting remember observers for later strictly ordered dispatch. */
- private class RememberEventDispatcher(private val abandoning: MutableSet<RememberObserver>) :
- RememberManager {
- private val remembering = mutableListOf<RememberObserver>()
- private val leaving = mutableListOf<Any>()
- private val sideEffects = mutableListOf<() -> Unit>()
- private var releasing: MutableScatterSet<ComposeNodeLifecycleCallback>? = null
- private val pending = mutableListOf<Any>()
- private val priorities = MutableIntList()
- private val afters = MutableIntList()
-
- override fun remembering(instance: RememberObserver) {
- remembering.add(instance)
- }
-
- override fun forgetting(
- instance: RememberObserver,
- endRelativeOrder: Int,
- priority: Int,
- endRelativeAfter: Int
- ) {
- recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
- }
-
- override fun sideEffect(effect: () -> Unit) {
- sideEffects += effect
- }
-
- override fun deactivating(
- instance: ComposeNodeLifecycleCallback,
- endRelativeOrder: Int,
- priority: Int,
- endRelativeAfter: Int
- ) {
- recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
- }
-
- override fun releasing(
- instance: ComposeNodeLifecycleCallback,
- endRelativeOrder: Int,
- priority: Int,
- endRelativeAfter: Int
- ) {
- val releasing =
- releasing
- ?: mutableScatterSetOf<ComposeNodeLifecycleCallback>().also { releasing = it }
-
- releasing += instance
- recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
- }
-
- fun dispatchRememberObservers() {
- // Add any pending out-of-order forgotten objects
- processPendingLeaving(Int.MIN_VALUE)
-
- // Send forgets and node callbacks
- if (leaving.isNotEmpty()) {
- trace("Compose:onForgotten") {
- val releasing = releasing
- for (i in leaving.size - 1 downTo 0) {
- val instance = leaving[i]
- if (instance is RememberObserver) {
- abandoning.remove(instance)
- instance.onForgotten()
- }
- if (instance is ComposeNodeLifecycleCallback) {
- // node callbacks are in the same queue as forgets to ensure ordering
- if (releasing != null && instance in releasing) {
- instance.onRelease()
- } else {
- instance.onDeactivate()
- }
- }
- }
- }
- }
-
- // Send remembers
- if (remembering.isNotEmpty()) {
- trace("Compose:onRemembered") {
- remembering.fastForEach { instance ->
- abandoning.remove(instance)
- instance.onRemembered()
- }
- }
- }
- }
-
- fun dispatchSideEffects() {
- if (sideEffects.isNotEmpty()) {
- trace("Compose:sideeffects") {
- sideEffects.fastForEach { sideEffect -> sideEffect() }
- sideEffects.clear()
- }
- }
- }
-
- fun dispatchAbandons() {
- if (abandoning.isNotEmpty()) {
- trace("Compose:abandons") {
- val iterator = abandoning.iterator()
- // remove elements one by one to ensure that abandons will not be dispatched
- // second time in case [onAbandoned] throws.
- while (iterator.hasNext()) {
- val instance = iterator.next()
- iterator.remove()
- instance.onAbandoned()
- }
- }
- }
- }
-
- private fun recordLeaving(
- instance: Any,
- endRelativeOrder: Int,
- priority: Int,
- endRelativeAfter: Int
- ) {
- processPendingLeaving(endRelativeOrder)
- if (endRelativeAfter in 0 until endRelativeOrder) {
- pending.add(instance)
- priorities.add(priority)
- afters.add(endRelativeAfter)
- } else {
- leaving.add(instance)
- }
- }
-
- private fun processPendingLeaving(endRelativeOrder: Int) {
- if (pending.isNotEmpty()) {
- var index = 0
- var toAdd: MutableList<Any>? = null
- var toAddAfter: MutableIntList? = null
- var toAddPriority: MutableIntList? = null
- while (index < afters.size) {
- if (endRelativeOrder <= afters[index]) {
- val instance = pending.removeAt(index)
- val endRelativeAfter = afters.removeAt(index)
- val priority = priorities.removeAt(index)
-
- if (toAdd == null) {
- toAdd = mutableListOf(instance)
- toAddAfter = MutableIntList().also { it.add(endRelativeAfter) }
- toAddPriority = MutableIntList().also { it.add(priority) }
- } else {
- toAddPriority as MutableIntList
- toAddAfter as MutableIntList
- toAdd.add(instance)
- toAddAfter.add(endRelativeAfter)
- toAddPriority.add(priority)
- }
- } else {
- index++
- }
- }
- if (toAdd != null) {
- toAddPriority as MutableIntList
- toAddAfter as MutableIntList
-
- // Sort the list into [after, -priority] order where it is ordered by after
- // in ascending order as the primary key and priority in descending order as
- // secondary key.
-
- // For example if remember occurs after a child group it must be added after
- // all the remembers of the child. This is reported with an after which is the
- // slot index of the child's last slot. As this slot might be at the same
- // location as where its parents ends this would be ambiguous which should
- // first if both the two groups request a slot to be after the same slot.
- // Priority is used to break the tie here which is the group index of the group
- // which is leaving. Groups that are lower must be added before the parent's
- // remember when they have the same after.
-
- // The sort must be stable as as consecutive remembers in the same group after
- // the same child will have the same after and priority.
-
- // A selection sort is used here because it is stable and the groups are
- // typically very short so this quickly exit list of one and not loop for
- // for sizes of 2. As the information is split between three lists, to
- // reduce allocations, [MutableList.sort] cannot be used as it doesn't have
- // an option to supply a custom swap.
- for (i in 0 until toAdd.size - 1) {
- for (j in i + 1 until toAdd.size) {
- val iAfter = toAddAfter[i]
- val jAfter = toAddAfter[j]
- if (
- iAfter < jAfter ||
- (jAfter == iAfter && toAddPriority[i] < toAddPriority[j])
- ) {
- toAdd.swap(i, j)
- toAddPriority.swap(i, j)
- toAddAfter.swap(i, j)
- }
- }
- }
- leaving.addAll(toAdd)
- }
- }
- }
- }
-}
-
-private fun <T> MutableList<T>.swap(a: Int, b: Int) {
- val item = this[a]
- this[a] = this[b]
- this[b] = item
-}
-
-private fun MutableIntList.swap(a: Int, b: Int) {
- val item = this[a]
- this[a] = this[b]
- this[b] = item
}
internal object ScopeInvalidated
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
index 561890c..e5b6d6b 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
@@ -16,6 +16,7 @@
package androidx.compose.runtime
+import androidx.collection.ScatterSet
import androidx.compose.runtime.internal.persistentCompositionLocalHashMapOf
import androidx.compose.runtime.tooling.CompositionData
import kotlin.coroutines.CoroutineContext
@@ -52,6 +53,20 @@
content: @Composable () -> Unit
)
+ internal abstract fun composeInitialPaused(
+ composition: ControlledComposition,
+ shouldPause: () -> Boolean,
+ content: @Composable () -> Unit
+ ): ScatterSet<RecomposeScopeImpl>
+
+ internal abstract fun recomposePaused(
+ composition: ControlledComposition,
+ shouldPause: () -> Boolean,
+ invalidScopes: ScatterSet<RecomposeScopeImpl>
+ ): ScatterSet<RecomposeScopeImpl>
+
+ internal abstract fun reportPausedScope(scope: RecomposeScopeImpl)
+
internal abstract fun invalidate(composition: ControlledComposition)
internal abstract fun invalidateScope(scope: RecomposeScopeImpl)
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
new file mode 100644
index 0000000..0eff8d6
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt
@@ -0,0 +1,381 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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:OptIn(InternalComposeApi::class)
+
+package androidx.compose.runtime
+
+import androidx.collection.emptyScatterSet
+import androidx.collection.mutableIntListOf
+import androidx.collection.mutableObjectListOf
+import androidx.compose.runtime.internal.RememberEventDispatcher
+
+/**
+ * A [PausableComposition] is a sub-composition that can be composed incrementally as it supports
+ * being paused and resumed.
+ *
+ * Pausable sub-composition can be used between frames to prepare a sub-composition before it is
+ * required by the main composition. For example, this is used in lazy lists to prepare list items
+ * in between frames to that are likely to be scrolled in. The composition is paused when the start
+ * of the next frame is near allowing composition to be spread across multiple frames without
+ * delaying the production of the next frame.
+ *
+ * The result of the composition should not be used (e.g. the nodes should not added to a layout
+ * tree or placed in layout) until [PausedComposition.isComplete] is `true` and
+ * [PausedComposition.apply] has been called. The composition is incomplete and will not
+ * automatically recompose until after [PausedComposition.apply] is called.
+ *
+ * A [PausableComposition] is a [ReusableComposition] but [setPausableContent] should be used
+ * instead of [ReusableComposition.setContentWithReuse] to create a paused composition.
+ *
+ * If [Composition.setContent] or [ReusableComposition.setContentWithReuse] are used then the
+ * composition behaves as if it wasn't pausable. If there is a [PausedComposition] that has not yet
+ * been applied, an exception is thrown.
+ *
+ * @see Composition
+ * @see ReusableComposition
+ */
+interface PausableComposition : ReusableComposition {
+ /**
+ * Set the content of the composition. A [PausedComposition] that is currently paused. No
+ * composition is performed until [PausedComposition.resume] is called.
+ * [PausedComposition.resume] should be called until [PausedComposition.isComplete] is `true`.
+ * The composition should not be used until [PausedComposition.isComplete] is `true` and
+ * [PausedComposition.apply] has been called.
+ *
+ * @see Composition.setContent
+ * @see ReusableComposition.setContentWithReuse
+ */
+ fun setPausableContent(content: @Composable () -> Unit): PausedComposition
+
+ /**
+ * Set the content of a resuable composition. A [PausedComposition] that is currently paused. No
+ * composition is performed until [PausedComposition.resume] is called.
+ * [PausedComposition.resume] should be called until [PausedComposition.isComplete] is `true`.
+ * The composition should not be used until [PausedComposition.isComplete] is `true` and
+ * [PausedComposition.apply] has been called.
+ *
+ * @see Composition.setContent
+ * @see ReusableComposition.setContentWithReuse
+ */
+ fun setPausableContentWithReuse(content: @Composable () -> Unit): PausedComposition
+}
+
+/**
+ * [PausedComposition] is the result of calling [PausableComposition.setContent] or
+ * [PausableComposition.setContentWithReuse]. It is used to drive the paused composition to
+ * completion. A [PausedComposition] should not be used until [isComplete] is `true` and [apply] has
+ * been called.
+ *
+ * A [PausedComposition] is created paused and will only compose the `content` parameter when
+ * [resume] is called the first time.
+ */
+interface PausedComposition {
+ /**
+ * Returns `true` when the [PausedComposition] is complete. [isComplete] matches the last value
+ * returned from [resume]. Once a [PausedComposition] is [isComplete] the [apply] method should
+ * be called.
+ */
+ val isComplete: Boolean
+
+ /**
+ * Resume the composition that has been paused. This method should be called until [resume]
+ * returns `true` or [isComplete] is `true` which has the same result as the last result of
+ * calling [resume]. The [shouldPause] parameter is a lambda that returns whether the
+ * composition should be paused. For example, in lazy lists this returns `false` until just
+ * prior to the next frame starting in which it returns `true`
+ *
+ * Calling [resume] after it returns `true` or when `isComplete` is true will throw an
+ * exception.
+ *
+ * @param shouldPause A lambda that is used to determine if the composition should be paused.
+ * This lambda is called often so should be a very simple calculation. Returning `true` does
+ * not guarantee the composition will pause, it should only be considered a request to pause
+ * the composition. Not all composable functions are pausable and only pausable composition
+ * functions will pause.
+ * @return `true` if the composition is complete and `false` if one or more calls to `resume`
+ * are required to complete composition.
+ */
+ fun resume(shouldPause: () -> Boolean): Boolean
+
+ /**
+ * Apply the composition. This is the last step of a paused composition and is required to be
+ * called prior to the composition is usable.
+ */
+ fun apply()
+
+ /**
+ * Cancels the paused composition. This should only be used if the composition is going to be
+ * disposed and the entire composition is not going to be used.
+ */
+ fun cancel()
+}
+
+/**
+ * Create a [PausableComposition]. A [PausableComposition] can create a [PausedComposition] which
+ * allows pausing and resuming the composition.
+ *
+ * @param applier The [Applier] instance to be used in the composition.
+ * @param parent The parent [CompositionContext].
+ * @see Applier
+ * @see CompositionContext
+ * @see PausableComposition
+ */
+fun PausableComposition(applier: Applier<*>, parent: CompositionContext): PausableComposition =
+ CompositionImpl(parent, applier)
+
+internal enum class PausedCompositionState {
+ Invalid,
+ Cancelled,
+ InitialPending,
+ RecomposePending,
+ ApplyPending,
+ Applied,
+}
+
+internal class PausedCompositionImpl(
+ val composition: CompositionImpl,
+ val context: CompositionContext,
+ val composer: ComposerImpl,
+ abandonSet: MutableSet<RememberObserver>,
+ val content: @Composable () -> Unit,
+ val reusable: Boolean,
+ val applier: Applier<*>,
+ val lock: SynchronizedObject,
+) : PausedComposition {
+ private var state = PausedCompositionState.InitialPending
+ private var invalidScopes = emptyScatterSet<RecomposeScopeImpl>()
+ internal val rememberManager = RememberEventDispatcher(abandonSet)
+ internal val pausableApplier = RecordingApplier(applier.current)
+
+ override val isComplete: Boolean
+ get() = state >= PausedCompositionState.ApplyPending
+
+ override fun resume(shouldPause: () -> Boolean): Boolean {
+ try {
+ when (state) {
+ PausedCompositionState.InitialPending -> {
+ if (reusable) composer.startReuseFromRoot()
+ try {
+ invalidScopes =
+ context.composeInitialPaused(composition, shouldPause, content)
+ } finally {
+ if (reusable) composer.endReuseFromRoot()
+ }
+ state = PausedCompositionState.RecomposePending
+ if (invalidScopes.isEmpty()) markComplete()
+ }
+ PausedCompositionState.RecomposePending -> {
+ invalidScopes = context.recomposePaused(composition, shouldPause, invalidScopes)
+ if (invalidScopes.isEmpty()) markComplete()
+ }
+ PausedCompositionState.ApplyPending ->
+ error("Pausable composition is complete and apply() should be applied")
+ PausedCompositionState.Applied -> error("The paused composition has been applied")
+ PausedCompositionState.Cancelled ->
+ error("The paused composition has been cancelled")
+ PausedCompositionState.Invalid ->
+ error("The paused composition is invalid because of a previous exception")
+ }
+ } catch (e: Exception) {
+ state = PausedCompositionState.Invalid
+ }
+ return isComplete
+ }
+
+ override fun apply() {
+ try {
+ when (state) {
+ PausedCompositionState.InitialPending,
+ PausedCompositionState.RecomposePending ->
+ error("The paused composition has not completed yet")
+ PausedCompositionState.ApplyPending -> {
+ applyChanges()
+ state = PausedCompositionState.Applied
+ }
+ PausedCompositionState.Applied ->
+ error("The paused composition has already been applied")
+ PausedCompositionState.Cancelled ->
+ error("The paused composition has been cancelled")
+ PausedCompositionState.Invalid ->
+ error("The paused composition is invalid because of a previous exception")
+ }
+ } catch (e: Exception) {
+ state = PausedCompositionState.Invalid
+ throw e
+ }
+ }
+
+ override fun cancel() {
+ state = PausedCompositionState.Cancelled
+ rememberManager.dispatchAbandons()
+ composition.pausedCompositionFinished()
+ }
+
+ private fun markComplete() {
+ state = PausedCompositionState.ApplyPending
+ }
+
+ private fun applyChanges() {
+ synchronized(lock) {
+ @Suppress("UNCHECKED_CAST")
+ try {
+ pausableApplier.playTo(applier as Applier<Any?>)
+ rememberManager.dispatchRememberObservers()
+ rememberManager.dispatchSideEffects()
+ } finally {
+ rememberManager.dispatchAbandons()
+ composition.pausedCompositionFinished()
+ }
+ }
+ }
+}
+
+internal class RecordingApplier<N>(root: N) : Applier<N> {
+ private val stack = mutableObjectListOf<N>()
+ private val operations = mutableIntListOf()
+ private val instances = mutableObjectListOf<Any?>()
+
+ override var current: N = root
+
+ 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) {
+ operations.add(REMOVE)
+ operations.add(index)
+ operations.add(count)
+ }
+
+ override fun move(from: Int, to: Int, count: Int) {
+ operations.add(MOVE)
+ operations.add(from)
+ operations.add(to)
+ operations.add(count)
+ }
+
+ override fun clear() {
+ operations.add(CLEAR)
+ }
+
+ override fun insertBottomUp(index: Int, instance: N) {
+ operations.add(INSERT_BOTTOM_UP)
+ operations.add(index)
+ instances.add(instance)
+ }
+
+ override fun insertTopDown(index: Int, instance: N) {
+ operations.add(INSERT_TOP_DOWN)
+ operations.add(index)
+ instances.add(instance)
+ }
+
+ override fun apply(block: N.(Any?) -> Unit, value: Any?) {
+ operations.add(APPLY)
+ instances.add(block)
+ instances.add(value)
+ }
+
+ override fun reuse() {
+ operations.add(REUSE)
+ }
+
+ fun playTo(applier: Applier<N>) {
+ var currentOperation = 0
+ var currentInstance = 0
+ val operations = operations
+ val size = operations.size
+ val instances = instances
+ applier.onBeginChanges()
+ try {
+ while (currentOperation < size) {
+ val operation = operations[currentOperation++]
+ when (operation) {
+ UP -> {
+ applier.up()
+ }
+ DOWN -> {
+ @Suppress("UNCHECKED_CAST") val node = instances[currentInstance++] as N
+ applier.down(node)
+ }
+ REMOVE -> {
+ val index = operations[currentOperation++]
+ val count = operations[currentOperation++]
+ applier.remove(index, count)
+ }
+ MOVE -> {
+ val from = operations[currentOperation++]
+ val to = operations[currentOperation++]
+ val count = operations[currentOperation++]
+ applier.move(from, to, count)
+ }
+ CLEAR -> {
+ applier.clear()
+ }
+ INSERT_TOP_DOWN -> {
+ val index = operations[currentOperation++]
+
+ @Suppress("UNCHECKED_CAST") val instance = instances[currentInstance++] as N
+ applier.insertTopDown(index, instance)
+ }
+ INSERT_BOTTOM_UP -> {
+ val index = operations[currentOperation++]
+
+ @Suppress("UNCHECKED_CAST") val instance = instances[currentInstance++] as N
+ applier.insertBottomUp(index, instance)
+ }
+ APPLY -> {
+ @Suppress("UNCHECKED_CAST")
+ val block = instances[currentInstance++] as Any?.(Any?) -> Unit
+ val value = instances[currentInstance++]
+ applier.apply(block, value)
+ }
+ REUSE -> {
+ applier.reuse()
+ }
+ }
+ }
+ runtimeCheck(currentInstance == instances.size) { "Applier operation size mismatch" }
+ instances.clear()
+ operations.clear()
+ } finally {
+ applier.onEndChanges()
+ }
+ }
+
+ // These commands need to be an integer, not just a enum value, as they are stored along side
+ // the commands integer parameters, so the values are explicitly set.
+ companion object {
+ const val UP = 0
+ const val DOWN = UP + 1
+ const val REMOVE = DOWN + 1
+ const val MOVE = REMOVE + 1
+ const val CLEAR = MOVE + 1
+ const val INSERT_BOTTOM_UP = CLEAR + 1
+ const val INSERT_TOP_DOWN = INSERT_BOTTOM_UP + 1
+ const val APPLY = INSERT_TOP_DOWN + 1
+ const val REUSE = APPLY + 1
+ }
+}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
index 88245a7..48657d5 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
@@ -55,13 +55,16 @@
((lowBits shl 1) and highBits))
}
-private const val UsedFlag = 0x01
-private const val DefaultsInScopeFlag = 0x02
-private const val DefaultsInvalidFlag = 0x04
-private const val RequiresRecomposeFlag = 0x08
-private const val SkippedFlag = 0x10
-private const val RereadingFlag = 0x20
-private const val ForcedRecomposeFlag = 0x40
+private const val UsedFlag = 0x001
+private const val DefaultsInScopeFlag = 0x002
+private const val DefaultsInvalidFlag = 0x004
+private const val RequiresRecomposeFlag = 0x008
+private const val SkippedFlag = 0x010
+private const val RereadingFlag = 0x020
+private const val ForcedRecomposeFlag = 0x040
+private const val ForceReusing = 0x080
+private const val Paused = 0x100
+private const val Resuming = 0x200
internal interface RecomposeScopeOwner {
fun invalidate(scope: RecomposeScopeImpl, instance: Any?): InvalidationResult
@@ -110,11 +113,51 @@
var used: Boolean
get() = flags and UsedFlag != 0
set(value) {
- if (value) {
- flags = flags or UsedFlag
- } else {
- flags = flags and UsedFlag.inv()
- }
+ flags =
+ if (value) {
+ flags or UsedFlag
+ } else {
+ flags and UsedFlag.inv()
+ }
+ }
+
+ /**
+ * Used to force a scope to the reusing state when a composition is paused while reusing
+ * content.
+ */
+ var reusing: Boolean
+ get() = flags and ForceReusing != 0
+ set(value) {
+ flags =
+ if (value) {
+ flags or ForceReusing
+ } else {
+ flags and ForceReusing.inv()
+ }
+ }
+
+ /** Used to flag a scope as paused for pausable compositions */
+ var paused: Boolean
+ get() = flags and Paused != 0
+ set(value) {
+ flags =
+ if (value) {
+ flags or Paused
+ } else {
+ flags and Paused.inv()
+ }
+ }
+
+ /** Used to flag a scope as paused for pausable compositions */
+ var resuming: Boolean
+ get() = flags and Resuming != 0
+ set(value) {
+ flags =
+ if (value) {
+ flags or Resuming
+ } else {
+ flags and Resuming.inv()
+ }
}
/**
@@ -299,7 +342,9 @@
}
fun scopeSkipped() {
- skipped = true
+ if (!reusing) {
+ skipped = true
+ }
}
/**
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index 67d0d8a..e347076 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -17,12 +17,15 @@
package androidx.compose.runtime
import androidx.collection.MutableScatterSet
+import androidx.collection.ScatterSet
+import androidx.collection.emptyScatterSet
import androidx.collection.mutableScatterSetOf
import androidx.compose.runtime.collection.fastForEach
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.collection.wrapIntoSet
import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentSetOf
import androidx.compose.runtime.internal.AtomicReference
+import androidx.compose.runtime.internal.SnapshotThreadLocal
import androidx.compose.runtime.internal.logError
import androidx.compose.runtime.internal.trace
import androidx.compose.runtime.snapshots.MutableSnapshot
@@ -232,6 +235,7 @@
// End properties guarded by stateLock
private val _state = MutableStateFlow(State.Inactive)
+ private val pausedScopes = SnapshotThreadLocal<MutableScatterSet<RecomposeScopeImpl>?>()
/**
* A [Job] used as a parent of any effects created by this [Recomposer]'s compositions. Its
@@ -1116,6 +1120,54 @@
}
}
+ internal override fun composeInitialPaused(
+ composition: ControlledComposition,
+ shouldPause: () -> Boolean,
+ content: @Composable () -> Unit
+ ): ScatterSet<RecomposeScopeImpl> {
+ return try {
+ composition.pausable(shouldPause) {
+ composeInitial(composition, content)
+ pausedScopes.get() ?: emptyScatterSet()
+ }
+ } finally {
+ pausedScopes.set(null)
+ }
+ }
+
+ internal override fun recomposePaused(
+ composition: ControlledComposition,
+ shouldPause: () -> Boolean,
+ invalidScopes: ScatterSet<RecomposeScopeImpl>
+ ): ScatterSet<RecomposeScopeImpl> {
+ return try {
+ recordComposerModifications()
+ composition.recordModificationsOf(invalidScopes.wrapIntoSet())
+ composition.pausable(shouldPause) {
+ val needsApply = performRecompose(composition, null)
+ if (needsApply != null) {
+ performInitialMovableContentInserts(composition)
+ needsApply.applyChanges()
+ needsApply.applyLateChanges()
+ }
+ pausedScopes.get() ?: emptyScatterSet()
+ }
+ } finally {
+ pausedScopes.set(null)
+ }
+ }
+
+ override fun reportPausedScope(scope: RecomposeScopeImpl) {
+ val scopes =
+ pausedScopes.get()
+ ?: run {
+ val newScopes = mutableScatterSetOf<RecomposeScopeImpl>()
+ pausedScopes.set(newScopes)
+ newScopes
+ }
+ scopes.add(scope)
+ }
+
private fun performInitialMovableContentInserts(composition: ControlledComposition) {
synchronized(stateLock) {
if (!compositionValuesAwaitingInsert.fastAny { it.composition == composition }) return
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ChangeList.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ChangeList.kt
index 4780cdb..e2eaa76 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ChangeList.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ChangeList.kt
@@ -25,6 +25,7 @@
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.MovableContentState
import androidx.compose.runtime.MovableContentStateReference
+import androidx.compose.runtime.RecomposeScopeImpl
import androidx.compose.runtime.RememberManager
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.SlotTable
@@ -40,6 +41,7 @@
import androidx.compose.runtime.changelist.Operation.EndCompositionScope
import androidx.compose.runtime.changelist.Operation.EndCurrentGroup
import androidx.compose.runtime.changelist.Operation.EndMovableContentPlacement
+import androidx.compose.runtime.changelist.Operation.EndResumingScope
import androidx.compose.runtime.changelist.Operation.EnsureGroupStarted
import androidx.compose.runtime.changelist.Operation.EnsureRootGroupStarted
import androidx.compose.runtime.changelist.Operation.InsertSlots
@@ -48,11 +50,13 @@
import androidx.compose.runtime.changelist.Operation.MoveNode
import androidx.compose.runtime.changelist.Operation.ReleaseMovableGroupAtCurrent
import androidx.compose.runtime.changelist.Operation.Remember
+import androidx.compose.runtime.changelist.Operation.RememberPausingScope
import androidx.compose.runtime.changelist.Operation.RemoveCurrentGroup
import androidx.compose.runtime.changelist.Operation.RemoveNode
import androidx.compose.runtime.changelist.Operation.ResetSlots
import androidx.compose.runtime.changelist.Operation.SideEffect
import androidx.compose.runtime.changelist.Operation.SkipToEndOfCurrentGroup
+import androidx.compose.runtime.changelist.Operation.StartResumingScope
import androidx.compose.runtime.changelist.Operation.TrimParentValues
import androidx.compose.runtime.changelist.Operation.UpdateAnchoredValue
import androidx.compose.runtime.changelist.Operation.UpdateAuxData
@@ -87,6 +91,18 @@
operations.push(Remember) { setObject(Remember.Value, value) }
}
+ fun pushRememberPausingScope(scope: RecomposeScopeImpl) {
+ operations.push(RememberPausingScope) { setObject(RememberPausingScope.Scope, scope) }
+ }
+
+ fun pushStartResumingScope(scope: RecomposeScopeImpl) {
+ operations.push(StartResumingScope) { setObject(StartResumingScope.Scope, scope) }
+ }
+
+ fun pushEndResumingScope(scope: RecomposeScopeImpl) {
+ operations.push(EndResumingScope) { setObject(EndResumingScope.Scope, scope) }
+ }
+
fun pushUpdateValue(value: Any?, groupSlotIndex: Int) {
operations.push(UpdateValue) {
setObject(UpdateValue.Value, value)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ComposerChangeListWriter.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ComposerChangeListWriter.kt
index 74c7146..9b87d45 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ComposerChangeListWriter.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ComposerChangeListWriter.kt
@@ -25,6 +25,7 @@
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.MovableContentState
import androidx.compose.runtime.MovableContentStateReference
+import androidx.compose.runtime.RecomposeScopeImpl
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.SlotReader
import androidx.compose.runtime.SlotTable
@@ -192,6 +193,18 @@
changeList.pushRemember(value)
}
+ fun rememberPausingScope(scope: RecomposeScopeImpl) {
+ changeList.pushRememberPausingScope(scope)
+ }
+
+ fun startResumingScope(scope: RecomposeScopeImpl) {
+ changeList.pushStartResumingScope(scope)
+ }
+
+ fun endResumingScope(scope: RecomposeScopeImpl) {
+ changeList.pushEndResumingScope(scope)
+ }
+
fun updateValue(value: Any?, groupSlotIndex: Int) {
pushSlotTableOperationPreamble(useParentSlot = true)
changeList.pushUpdateValue(value, groupSlotIndex)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt
index 894f3d47..15aa234 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt
@@ -18,7 +18,6 @@
import androidx.compose.runtime.Anchor
import androidx.compose.runtime.Applier
-import androidx.compose.runtime.ComposeNodeLifecycleCallback
import androidx.compose.runtime.Composition
import androidx.compose.runtime.CompositionContext
import androidx.compose.runtime.ControlledComposition
@@ -171,6 +170,66 @@
}
}
+ object RememberPausingScope : Operation(objects = 1) {
+ inline val Scope
+ get() = ObjectParameter<RecomposeScopeImpl>(0)
+
+ override fun objectParamName(parameter: ObjectParameter<*>): String =
+ when (parameter) {
+ Scope -> "scope"
+ else -> super.objectParamName(parameter)
+ }
+
+ override fun OperationArgContainer.execute(
+ applier: Applier<*>,
+ slots: SlotWriter,
+ rememberManager: RememberManager
+ ) {
+ val scope = getObject(Scope)
+ rememberManager.rememberPausingScope(scope)
+ }
+ }
+
+ object StartResumingScope : Operation(objects = 1) {
+ inline val Scope
+ get() = ObjectParameter<RecomposeScopeImpl>(0)
+
+ override fun objectParamName(parameter: ObjectParameter<*>): String =
+ when (parameter) {
+ Scope -> "scope"
+ else -> super.objectParamName(parameter)
+ }
+
+ override fun OperationArgContainer.execute(
+ applier: Applier<*>,
+ slots: SlotWriter,
+ rememberManager: RememberManager
+ ) {
+ val scope = getObject(Scope)
+ rememberManager.startResumingScope(scope)
+ }
+ }
+
+ object EndResumingScope : Operation(objects = 1) {
+ inline val Scope
+ get() = ObjectParameter<RecomposeScopeImpl>(0)
+
+ override fun objectParamName(parameter: ObjectParameter<*>): String =
+ when (parameter) {
+ Scope -> "scope"
+ else -> super.objectParamName(parameter)
+ }
+
+ override fun OperationArgContainer.execute(
+ applier: Applier<*>,
+ slots: SlotWriter,
+ rememberManager: RememberManager
+ ) {
+ val scope = getObject(Scope)
+ rememberManager.endResumingScope(scope)
+ }
+ }
+
object AppendValue : Operation(objects = 2) {
inline val Anchor
get() = ObjectParameter<Anchor>(0)
@@ -467,7 +526,7 @@
slots: SlotWriter,
rememberManager: RememberManager
) {
- (applier.current as ComposeNodeLifecycleCallback).onReuse()
+ applier.reuse()
}
}
@@ -492,7 +551,7 @@
) {
val value = getObject(Value)
val block = getObject(Block)
- applier.current.block(value)
+ applier.apply(block, value)
}
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/RememberEventDispatcher.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/RememberEventDispatcher.kt
new file mode 100644
index 0000000..d9d78ea
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/RememberEventDispatcher.kt
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.runtime.internal
+
+import androidx.collection.MutableIntList
+import androidx.collection.MutableScatterMap
+import androidx.collection.MutableScatterSet
+import androidx.collection.mutableScatterMapOf
+import androidx.collection.mutableScatterSetOf
+import androidx.compose.runtime.ComposeNodeLifecycleCallback
+import androidx.compose.runtime.RecomposeScopeImpl
+import androidx.compose.runtime.RememberManager
+import androidx.compose.runtime.RememberObserver
+import androidx.compose.runtime.Stack
+import androidx.compose.runtime.snapshots.fastForEach
+
+/**
+ * Used as a placeholder for paused compositions to ensure the remembers are dispatch in the correct
+ * order. While the paused composition is resuming all remembered objects are placed into the this
+ * classes list instead of the main list. As remembers are dispatched, this will dispatch remembers
+ * to the object remembered in the paused composition's content in the order that they would have
+ * been dispatched had the composition not been paused.
+ */
+internal class PausedCompositionRemembers(private val abandoning: MutableSet<RememberObserver>) :
+ RememberObserver {
+ val pausedRemembers = mutableListOf<RememberObserver>()
+
+ override fun onRemembered() {
+ pausedRemembers.fastForEach {
+ abandoning.remove(it)
+ it.onRemembered()
+ }
+ }
+
+ // These are never called
+ override fun onForgotten() {}
+
+ override fun onAbandoned() {}
+}
+
+/** Helper for collecting remember observers for later strictly ordered dispatch. */
+internal class RememberEventDispatcher(private val abandoning: MutableSet<RememberObserver>) :
+ RememberManager {
+ private val remembering = mutableListOf<RememberObserver>()
+ private var currentRememberingList = remembering
+ private val leaving = mutableListOf<Any>()
+ private val sideEffects = mutableListOf<() -> Unit>()
+ private var releasing: MutableScatterSet<ComposeNodeLifecycleCallback>? = null
+ private var pausedPlaceholders:
+ MutableScatterMap<RecomposeScopeImpl, PausedCompositionRemembers>? =
+ null
+ private val pending = mutableListOf<Any>()
+ private val priorities = MutableIntList()
+ private val afters = MutableIntList()
+ private var nestedRemembersLists: Stack<MutableList<RememberObserver>>? = null
+
+ override fun remembering(instance: RememberObserver) {
+ currentRememberingList.add(instance)
+ }
+
+ override fun forgetting(
+ instance: RememberObserver,
+ endRelativeOrder: Int,
+ priority: Int,
+ endRelativeAfter: Int
+ ) {
+ recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
+ }
+
+ override fun sideEffect(effect: () -> Unit) {
+ sideEffects += effect
+ }
+
+ override fun deactivating(
+ instance: ComposeNodeLifecycleCallback,
+ endRelativeOrder: Int,
+ priority: Int,
+ endRelativeAfter: Int
+ ) {
+ recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
+ }
+
+ override fun releasing(
+ instance: ComposeNodeLifecycleCallback,
+ endRelativeOrder: Int,
+ priority: Int,
+ endRelativeAfter: Int
+ ) {
+ val releasing =
+ releasing ?: mutableScatterSetOf<ComposeNodeLifecycleCallback>().also { releasing = it }
+
+ releasing += instance
+ recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
+ }
+
+ override fun rememberPausingScope(scope: RecomposeScopeImpl) {
+ val pausedPlaceholder = PausedCompositionRemembers(abandoning)
+ (pausedPlaceholders
+ ?: mutableScatterMapOf<RecomposeScopeImpl, PausedCompositionRemembers>().also {
+ pausedPlaceholders = it
+ })[scope] = pausedPlaceholder
+ this.currentRememberingList.add(pausedPlaceholder)
+ }
+
+ override fun startResumingScope(scope: RecomposeScopeImpl) {
+ val placeholder = pausedPlaceholders?.get(scope)
+ if (placeholder != null) {
+ (nestedRemembersLists
+ ?: Stack<MutableList<RememberObserver>>().also { nestedRemembersLists = it })
+ .push(currentRememberingList)
+ currentRememberingList = placeholder.pausedRemembers
+ }
+ }
+
+ override fun endResumingScope(scope: RecomposeScopeImpl) {
+ val pausedPlaceholders = pausedPlaceholders
+ if (pausedPlaceholders != null) {
+ val placeholder = pausedPlaceholders[scope]
+ if (placeholder != null) {
+ nestedRemembersLists?.pop()?.let { currentRememberingList = it }
+ pausedPlaceholders.remove(scope)
+ }
+ }
+ }
+
+ fun dispatchRememberObservers() {
+ // Add any pending out-of-order forgotten objects
+ processPendingLeaving(Int.MIN_VALUE)
+
+ // Send forgets and node callbacks
+ if (leaving.isNotEmpty()) {
+ trace("Compose:onForgotten") {
+ val releasing = releasing
+ for (i in leaving.size - 1 downTo 0) {
+ val instance = leaving[i]
+ if (instance is RememberObserver) {
+ abandoning.remove(instance)
+ instance.onForgotten()
+ }
+ if (instance is ComposeNodeLifecycleCallback) {
+ // node callbacks are in the same queue as forgets to ensure ordering
+ if (releasing != null && instance in releasing) {
+ instance.onRelease()
+ } else {
+ instance.onDeactivate()
+ }
+ }
+ }
+ }
+ }
+
+ // Send remembers
+ if (remembering.isNotEmpty()) {
+ trace("Compose:onRemembered") { dispatchRememberList(remembering) }
+ }
+ }
+
+ private fun dispatchRememberList(list: List<RememberObserver>) {
+ list.fastForEach { instance ->
+ abandoning.remove(instance)
+ instance.onRemembered()
+ }
+ }
+
+ fun dispatchSideEffects() {
+ if (sideEffects.isNotEmpty()) {
+ trace("Compose:sideeffects") {
+ sideEffects.fastForEach { sideEffect -> sideEffect() }
+ sideEffects.clear()
+ }
+ }
+ }
+
+ fun dispatchAbandons() {
+ if (abandoning.isNotEmpty()) {
+ trace("Compose:abandons") {
+ val iterator = abandoning.iterator()
+ // remove elements one by one to ensure that abandons will not be dispatched
+ // second time in case [onAbandoned] throws.
+ while (iterator.hasNext()) {
+ val instance = iterator.next()
+ iterator.remove()
+ instance.onAbandoned()
+ }
+ }
+ }
+ }
+
+ private fun recordLeaving(
+ instance: Any,
+ endRelativeOrder: Int,
+ priority: Int,
+ endRelativeAfter: Int
+ ) {
+ processPendingLeaving(endRelativeOrder)
+ if (endRelativeAfter in 0 until endRelativeOrder) {
+ pending.add(instance)
+ priorities.add(priority)
+ afters.add(endRelativeAfter)
+ } else {
+ leaving.add(instance)
+ }
+ }
+
+ private fun processPendingLeaving(endRelativeOrder: Int) {
+ if (pending.isNotEmpty()) {
+ var index = 0
+ var toAdd: MutableList<Any>? = null
+ var toAddAfter: MutableIntList? = null
+ var toAddPriority: MutableIntList? = null
+ while (index < afters.size) {
+ if (endRelativeOrder <= afters[index]) {
+ val instance = pending.removeAt(index)
+ val endRelativeAfter = afters.removeAt(index)
+ val priority = priorities.removeAt(index)
+
+ if (toAdd == null) {
+ toAdd = mutableListOf(instance)
+ toAddAfter = MutableIntList().also { it.add(endRelativeAfter) }
+ toAddPriority = MutableIntList().also { it.add(priority) }
+ } else {
+ toAddPriority as MutableIntList
+ toAddAfter as MutableIntList
+ toAdd.add(instance)
+ toAddAfter.add(endRelativeAfter)
+ toAddPriority.add(priority)
+ }
+ } else {
+ index++
+ }
+ }
+ if (toAdd != null) {
+ toAddPriority as MutableIntList
+ toAddAfter as MutableIntList
+
+ // Sort the list into [after, -priority] order where it is ordered by after
+ // in ascending order as the primary key and priority in descending order as
+ // secondary key.
+
+ // For example if remember occurs after a child group it must be added after
+ // all the remembers of the child. This is reported with an after which is the
+ // slot index of the child's last slot. As this slot might be at the same
+ // location as where its parents ends this would be ambiguous which should
+ // first if both the two groups request a slot to be after the same slot.
+ // Priority is used to break the tie here which is the group index of the group
+ // which is leaving. Groups that are lower must be added before the parent's
+ // remember when they have the same after.
+
+ // The sort must be stable as as consecutive remembers in the same group after
+ // the same child will have the same after and priority.
+
+ // A selection sort is used here because it is stable and the groups are
+ // typically very short so this quickly exit list of one and not loop for
+ // for sizes of 2. As the information is split between three lists, to
+ // reduce allocations, [MutableList.sort] cannot be used as it doesn't have
+ // an option to supply a custom swap.
+ for (i in 0 until toAdd.size - 1) {
+ for (j in i + 1 until toAdd.size) {
+ val iAfter = toAddAfter[i]
+ val jAfter = toAddAfter[j]
+ if (
+ iAfter < jAfter ||
+ (jAfter == iAfter && toAddPriority[i] < toAddPriority[j])
+ ) {
+ toAdd.swap(i, j)
+ toAddPriority.swap(i, j)
+ toAddAfter.swap(i, j)
+ }
+ }
+ }
+ leaving.addAll(toAdd)
+ }
+ }
+ }
+}
+
+private fun <T> MutableList<T>.swap(a: Int, b: Int) {
+ val item = this[a]
+ this[a] = this[b]
+ this[b] = item
+}
+
+private fun MutableIntList.swap(a: Int, b: Int) {
+ val item = this[a]
+ this[a] = this[b]
+ this[b] = item
+}
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
index c2560dd..9328a61 100644
--- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -4800,16 +4800,16 @@
private val rob_reports_to_alice = Report("Rob", "Alice")
private val clark_reports_to_lois = Report("Clark", "Lois")
-private interface Counted {
+internal interface Counted {
val count: Int
}
-private interface Ordered {
+internal interface Ordered {
val rememberOrder: Int
val forgetOrder: Int
}
-private interface Named {
+internal interface Named {
val name: String
}
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/PausableCompositionTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/PausableCompositionTests.kt
new file mode 100644
index 0000000..36a9747
--- /dev/null
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/PausableCompositionTests.kt
@@ -0,0 +1,606 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.runtime
+
+import androidx.compose.runtime.mock.EmptyApplier
+import androidx.compose.runtime.mock.Linear
+import androidx.compose.runtime.mock.MockViewValidator
+import androidx.compose.runtime.mock.Text
+import androidx.compose.runtime.mock.View
+import androidx.compose.runtime.mock.ViewApplier
+import androidx.compose.runtime.mock.compositionTest
+import androidx.compose.runtime.mock.validate
+import androidx.compose.runtime.mock.view
+import kotlin.coroutines.resume
+import kotlin.test.Ignore
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.test.runTest
+
+@Stable
+class PausableCompositionTests {
+ @Test
+ fun canCreateARootPausableComposition() = runTest {
+ val recomposer = Recomposer(coroutineContext)
+ val pausableComposition = PausableComposition(EmptyApplier(), recomposer)
+ pausableComposition.dispose()
+ recomposer.cancel()
+ recomposer.close()
+ }
+
+ @Test
+ fun canCreateANestedPausableComposition() = compositionTest {
+ compose {
+ val parent = rememberCompositionContext()
+ DisposableEffect(Unit) {
+ val pausableComposition = PausableComposition(EmptyApplier(), parent)
+ onDispose { pausableComposition.dispose() }
+ }
+ }
+ }
+
+ @Test
+ fun canRecordAComposition() = compositionTest {
+ // This just tests the recording mechanism used in the tests below.
+ val recording = recordTest {
+ compose { A() }
+
+ validate { this.A() }
+ }
+
+ // Legend for the recording:
+ // +N: Enter N for functions A, B, C, D, (where A:1 is the first lambda in A())
+ // -N: Exit N
+ // *N: Calling N (e.g *B is recorded before B() is called).
+ // ^n: calling remember for some value
+
+ // Here we expect the normal, synchronous, execution as the recorded composition is not
+ // pausable. That is if we see a *B that should immediately followed by a B+ its content and
+ // a B-.
+ assertEquals(
+ recording,
+ "+A, ^z, ^Y, *B, +B, *Linear, +A:1, *C, +C, ^x, *Text, -C, *D, +D, +D:1, *C, +C, " +
+ "^x, *Text, -C, *C, +C, ^x, *Text, -C, *C, +C, ^x, *Text, -C, -D:1, -D, -A:1, " +
+ "-B, -A"
+ )
+ }
+
+ @Test
+ @Ignore // Requires compiler support
+ fun canPauseContent() = compositionTest {
+ val awaiter = Awaiter()
+ var receivedIteration = 0
+ val recording = recordTest {
+ compose {
+ PausableContent(
+ normalWorkflow {
+ receivedIteration = iteration
+ awaiter.done()
+ }
+ ) {
+ A()
+ }
+ }
+ awaiter.await()
+ }
+ validate { this.PausableContent { this.A() } }
+ assertEquals(10, receivedIteration)
+
+ // Same Legend as canRecordAComposition
+ // Here we expect all functions to exit before the content of the function is executed
+ // because the above will pause at every pause point. If we see a B* we should not receive
+ // a B+ until after the caller finishes. (e.g. A-).
+ assertEquals(
+ recording,
+ "+A, ^z, ^Y, *B, -A, +B, *Linear, -B, +A:1, *C, *D, -A:1, +C, " +
+ "^x, *Text, -C, +D, -D, +D:1, *C, *C, *C, -D:1, +C, ^x, *Text, -C, +C, ^x, *Text, " +
+ "-C, +C, ^x, *Text, -C"
+ )
+ }
+
+ @Test
+ @Ignore // Requires compiler support
+ fun canPauseReusableContent() = compositionTest {
+ val awaiter = Awaiter()
+ var receivedIteration = 0
+ val recording = recordTest {
+ compose {
+ PausableContent(
+ reuseWorkflow {
+ receivedIteration = iteration
+ awaiter.done()
+ }
+ ) {
+ A()
+ }
+ }
+ awaiter.await()
+ }
+ validate { this.PausableContent { this.A() } }
+ assertEquals(10, receivedIteration)
+ // Same Legend as canRecordAComposition
+ // Here we expect the result to be the same as if we were inserting new content as in
+ // canPauseContent
+ assertEquals(
+ "+A, ^z, ^Y, *B, -A, +B, *Linear, -B, +A:1, *C, *D, -A:1, +C, " +
+ "^x, *Text, -C, +D, -D, +D:1, *C, *C, *C, -D:1, +C, ^x, *Text, -C, +C, ^x, *Text, " +
+ "-C, +C, ^x, *Text, -C",
+ recording
+ )
+ }
+
+ @Test
+ @Ignore // Requires compiler support
+ fun canPauseReusingContent() = compositionTest {
+ val awaiter = Awaiter()
+ var recording = ""
+ val workflow: Workflow = {
+ // Create the content
+ setContentWithReuse()
+ resumeTillComplete { false }
+ apply()
+
+ // Reuse the content
+ recording = recordTest {
+ setContentWithReuse()
+ resumeTillComplete { true }
+ apply()
+ }
+ awaiter.done()
+ }
+
+ compose { PausableContent(workflow) { A() } }
+ awaiter.await()
+ // Same Legend as canRecordAComposition
+ // Here we expect the result to be the same as if we were inserting new content as in
+ // canPauseContent
+ assertArrayEquals(
+ ("+A, ^z, ^Y, *B, -A, +B, *Linear, -B, +A:1, *C, *D, -A:1, +C, " +
+ "^x, *Text, -C, +D, -D, +D:1, *C, *C, *C, -D:1, +C, ^x, *Text, -C, +C, ^x, *Text, " +
+ "-C, +C, ^x, *Text, -C")
+ .splitRecording(),
+ recording.splitRecording()
+ )
+ }
+
+ @Test
+ fun applierOnlyCalledInApply() = compositionTest {
+ val awaiter = Awaiter()
+ var applier: ViewApplier? = null
+
+ val workflow = workflow {
+ setContent()
+
+ assertFalse(applier?.called == true, "Applier was called during set content")
+
+ resumeTillComplete { false }
+
+ assertFalse(applier?.called == true, "Applier was called during resume")
+
+ apply()
+
+ assertTrue(applier?.called == true, "Applier wasn't called")
+
+ awaiter.done()
+ }
+
+ compose {
+ PausableContent(workflow, { view -> ViewApplier(view).also { applier = it } }) { A() }
+ }
+ awaiter.await()
+ }
+
+ @Test
+ @Ignore // Requires compiler support
+ fun rememberOnlyCalledInApply() = compositionTest {
+ val awaiter = Awaiter()
+ var onRememberCalled = false
+
+ val workflow = workflow {
+ setContent()
+ assertFalse(onRememberCalled, "onRemember called during set content")
+
+ resumeTillComplete {
+ assertFalse(onRememberCalled, "onRemember called during resume")
+ true
+ }
+ assertFalse(onRememberCalled, "onRemember called before resume returned")
+
+ apply()
+
+ assertTrue(onRememberCalled, "onRemember was not called in apply")
+
+ awaiter.done()
+ }
+
+ fun rememberedObject(name: String) =
+ object : RememberObserver {
+ val name = name
+
+ override fun onRemembered() {
+ onRememberCalled = true
+ report("+$name")
+ }
+
+ override fun onForgotten() {
+ report("-$name")
+ }
+
+ override fun onAbandoned() {
+ report("!$name")
+ }
+ }
+
+ val recording = recordTest {
+ compose {
+ PausableContent(workflow) {
+ val a = remember { rememberedObject("a") }
+ report("C(${a.name})")
+ B {
+ val b = remember { rememberedObject("b") }
+ report("C(${b.name})")
+ B {
+ val c = remember { rememberedObject("c") }
+ report("C(${c.name})")
+ C()
+ val d = remember { rememberedObject("d") }
+ report("C(${d.name})")
+ D()
+ }
+ }
+ }
+ }
+
+ awaiter.await()
+ }
+ // Same Legend as canRecordAComposition except the addition of the C(N) added above and
+ // +a, +b, etc. which records when the remembered object are sent the on-remember. This
+ // ensures that all onRemember calls are made after the composition has completed.
+ assertEquals(
+ "C(a), +B, *Linear, -B, C(b), +B, *Linear, -B, C(c), C(d), +C, ^x, *Text, -C, +D, " +
+ "-D, +D:1, *C, *C, *C, -D:1, +C, ^x, *Text, -C, +C, ^x, *Text, -C, +C, ^x, *Text, " +
+ "-C, +a, +b, +c, +d",
+ recording
+ )
+ }
+
+ @Suppress("ListIterator")
+ @Test
+ fun pausable_testRemember_RememberForgetOrder() = compositionTest {
+ var order = 0
+ val objects = mutableListOf<Any>()
+ val newRememberObject = { name: String ->
+ object : RememberObserver, Counted, Ordered, Named {
+ override var name = name
+ override var count = 0
+ override var rememberOrder = -1
+ override var forgetOrder = -1
+
+ override fun onRemembered() {
+ assertEquals(-1, rememberOrder, "Only one call to onRemembered expected")
+ rememberOrder = order++
+ count++
+ }
+
+ override fun onForgotten() {
+ assertEquals(-1, forgetOrder, "Only one call to onForgotten expected")
+ forgetOrder = order++
+ count--
+ }
+
+ override fun onAbandoned() {
+ assertEquals(0, count, "onAbandoned called after onRemembered")
+ }
+ }
+ .also { objects.add(it) }
+ }
+
+ @Suppress("UNUSED_PARAMETER") fun used(v: Any) {}
+
+ @Composable
+ fun Tree() {
+ used(remember { newRememberObject("L0B") })
+ Linear {
+ used(remember { newRememberObject("L1B") })
+ Linear {
+ used(remember { newRememberObject("L2B") })
+ Linear {
+ used(remember { newRememberObject("L3B") })
+ Linear { used(remember { newRememberObject("Leaf") }) }
+ used(remember { newRememberObject("L3A") })
+ }
+ used(remember { newRememberObject("L2A") })
+ }
+ used(remember { newRememberObject("L1A") })
+ }
+ used(remember { newRememberObject("L0A") })
+ }
+
+ val awaiter = Awaiter()
+ val workFlow = normalWorkflow { awaiter.done() }
+
+ compose { PausableContent(workFlow) { Tree() } }
+ awaiter.await()
+
+ // Legend:
+ // L<N><B|A>: where N is the nesting level and B is before the children and
+ // A is after the children.
+ // Leaf: the object remembered in the middle.
+ // This is asserting that the remember order is the same as it would have been had the
+ // above composition was not paused.
+ assertEquals(
+ "L0B, L1B, L2B, L3B, Leaf, L3A, L2A, L1A, L0A",
+ objects
+ .mapNotNull { it as? Ordered }
+ .sortedBy { it.rememberOrder }
+ .joinToString { (it as Named).name },
+ "Expected enter order",
+ )
+ }
+}
+
+fun String.splitRecording() = split(", ")
+
+typealias Workflow = suspend PausableContentWorkflowScope.() -> Unit
+
+fun workflow(workflow: Workflow): Workflow = workflow
+
+fun reuseWorkflow(done: Workflow = {}) = workflow {
+ setContentWithReuse()
+ resumeTillComplete { true }
+ apply()
+ done()
+}
+
+fun normalWorkflow(done: Workflow = {}) = workflow {
+ setContent()
+ resumeTillComplete { true }
+ apply()
+ done()
+}
+
+private interface TestRecorder {
+ fun log(message: String)
+
+ fun logs(): String
+
+ fun clear()
+}
+
+private var recorder: TestRecorder =
+ object : TestRecorder {
+ override fun log(message: String) {}
+
+ override fun logs(): String = ""
+
+ override fun clear() {}
+ }
+
+private inline fun recordTest(block: () -> Unit): String {
+ val result = mutableListOf<String>()
+ val oldRecorder = recorder
+ recorder =
+ object : TestRecorder {
+ override fun log(message: String) {
+ result.add(message)
+ }
+
+ override fun logs() = result.joinToString()
+
+ override fun clear() {
+ result.clear()
+ }
+ }
+ block()
+ recorder = oldRecorder
+ return result.joinToString()
+}
+
+private fun report(message: String) {
+ synchronized(recorder) { recorder.log(message) }
+}
+
+private inline fun report(message: String, block: () -> Unit) {
+ report("+$message")
+ block()
+ report("-$message")
+}
+
+@Composable
+private fun A() {
+ report("A") {
+ report("^z")
+ val z = remember { 0 }
+ report("^Y")
+ val y = remember { 1 }
+ Text("A: $z $y")
+ report("*B")
+ B {
+ report("A:1") {
+ report("*C")
+ C()
+ report("*D")
+ D()
+ }
+ }
+ }
+}
+
+private fun MockViewValidator.PausableContent(content: MockViewValidator.() -> Unit) {
+ this.view("PausableContentHost") { this.view("PausableContent", content) }
+}
+
+private fun MockViewValidator.A() {
+ Text("A: 0 1")
+ this.B {
+ this.C()
+ this.D()
+ }
+}
+
+@Composable
+private fun B(content: @Composable () -> Unit) {
+ report("B") {
+ report("*Linear")
+ Linear(content)
+ }
+}
+
+private fun MockViewValidator.B(content: MockViewValidator.() -> Unit) {
+ this.Linear(content)
+}
+
+@Composable
+private fun C() {
+ report("C") {
+ report("^x")
+ val x = remember { 3 }
+ report("*Text")
+ Text("C: $x")
+ }
+}
+
+private fun MockViewValidator.C() {
+ this.Text("C: 3")
+}
+
+@Composable
+private fun D() {
+ report("D") {
+ Linear {
+ report("D:1") {
+ repeat(3) {
+ report("*C")
+ C()
+ }
+ }
+ }
+ }
+}
+
+private fun MockViewValidator.D() {
+ this.Linear { repeat(3) { this.C() } }
+}
+
+interface PausableContentWorkflowScope {
+ val iteration: Int
+ val applied: Boolean
+
+ fun setContent(): PausedComposition
+
+ fun setContentWithReuse(): PausedComposition
+
+ fun resumeTillComplete(shouldPause: () -> Boolean)
+
+ fun apply()
+}
+
+fun PausableContentWorkflowScope.run(shouldPause: () -> Boolean = { true }) {
+ setContent()
+ resumeTillComplete(shouldPause)
+ apply()
+}
+
+class PausableContentWorkflowDriver(
+ private val composition: PausableComposition,
+ private val content: @Composable () -> Unit,
+ private var host: View?,
+ private var contentView: View?
+) : PausableContentWorkflowScope {
+ private var pausedComposition: PausedComposition? = null
+ override var iteration = 0
+ override val applied: Boolean
+ get() = host == null && pausedComposition == null
+
+ override fun setContent(): PausedComposition {
+ checkPrecondition(pausedComposition == null)
+ return composition.setPausableContent(content).also { pausedComposition = it }
+ }
+
+ override fun setContentWithReuse(): PausedComposition {
+ checkPrecondition(pausedComposition == null)
+ return composition.setPausableContentWithReuse(content).also { pausedComposition = it }
+ }
+
+ override fun resumeTillComplete(shouldPause: () -> Boolean) {
+ val pausedComposition = pausedComposition
+ checkPrecondition(pausedComposition != null)
+ while (!pausedComposition.isComplete) {
+ pausedComposition.resume(shouldPause)
+ iteration++
+ }
+ }
+
+ override fun apply() {
+ val pausedComposition = pausedComposition
+ checkPrecondition(pausedComposition != null && pausedComposition.isComplete)
+ pausedComposition.apply()
+ this.pausedComposition = null
+ val host = host
+ val contentView = contentView
+ if (host != null && contentView != null) {
+ host.children.add(contentView)
+ this.host = null
+ this.contentView = null
+ }
+ }
+}
+
+@Composable
+private fun PausableContent(
+ workflow: suspend PausableContentWorkflowScope.() -> Unit = { run() },
+ createApplier: (view: View) -> Applier<View> = { ViewApplier(it) },
+ content: @Composable () -> Unit
+) {
+ val host = View().also { it.name = "PausableContentHost" }
+ val pausableContent = View().also { it.name = "PausableContent" }
+ ComposeNode<View, ViewApplier>(factory = { host }, update = {})
+ val parent = rememberCompositionContext()
+ val composition =
+ remember(parent) { PausableComposition(createApplier(pausableContent), parent) }
+ LaunchedEffect(content as Any) {
+ val scope = PausableContentWorkflowDriver(composition, content, host, pausableContent)
+ scope.workflow()
+ }
+ DisposableEffect(Unit) { onDispose { composition.dispose() } }
+}
+
+private class Awaiter {
+ private var continuation: CancellableContinuation<Unit>? = null
+ private var done = false
+
+ suspend fun await() {
+ if (!done) {
+ suspendCancellableCoroutine { continuation = it }
+ }
+ }
+
+ fun resume() {
+ val current = continuation
+ continuation = null
+ current?.resume(Unit)
+ }
+
+ fun done() {
+ done = true
+ resume()
+ }
+}
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt
index e1b9966..30b464c 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt
@@ -111,7 +111,7 @@
@Composable
private fun BoxWithMissingContentDescription() {
Box(
- Modifier.size(20.dp).semantics {
+ Modifier.size(48.dp).semantics {
// The SemanticsModifier will make this node importantForAccessibility
// Having no content description is now a violation
this.contentDescription = ""
diff --git a/compose/ui/ui/api/current.ignore b/compose/ui/ui/api/current.ignore
index 074fe37..8caaf36 100644
--- a/compose/ui/ui/api/current.ignore
+++ b/compose/ui/ui/api/current.ignore
@@ -13,5 +13,9 @@
Removed method androidx.compose.ui.semantics.SemanticsProperties.getInvisibleToUser() from compatibility checked API surface
+ChangedType: androidx.compose.ui.layout.MeasureResult#getAlignmentLines():
+ Method androidx.compose.ui.layout.MeasureResult.getAlignmentLines has changed return type from java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> to java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer>
+
+
RemovedMethod: androidx.compose.ui.layout.LayoutCoordinates#transformToScreen(float[]):
Removed method androidx.compose.ui.layout.LayoutCoordinates.transformToScreen(float[])
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index cc929b6..afed775 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -2338,20 +2338,20 @@
}
public interface MeasureResult {
- method public java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
+ method public java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
method public int getHeight();
method public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? getRulers();
method public int getWidth();
method public void placeChildren();
- property public abstract java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
+ property public abstract java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
property public abstract int height;
property public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers;
property public abstract int width;
}
@androidx.compose.ui.layout.MeasureScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureScope extends androidx.compose.ui.layout.IntrinsicMeasureScope {
- method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
- method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+ method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+ method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
}
@kotlin.DslMarker public @interface MeasureScopeMarker {
diff --git a/compose/ui/ui/api/restricted_current.ignore b/compose/ui/ui/api/restricted_current.ignore
index 074fe37..8caaf36 100644
--- a/compose/ui/ui/api/restricted_current.ignore
+++ b/compose/ui/ui/api/restricted_current.ignore
@@ -13,5 +13,9 @@
Removed method androidx.compose.ui.semantics.SemanticsProperties.getInvisibleToUser() from compatibility checked API surface
+ChangedType: androidx.compose.ui.layout.MeasureResult#getAlignmentLines():
+ Method androidx.compose.ui.layout.MeasureResult.getAlignmentLines has changed return type from java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> to java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer>
+
+
RemovedMethod: androidx.compose.ui.layout.LayoutCoordinates#transformToScreen(float[]):
Removed method androidx.compose.ui.layout.LayoutCoordinates.transformToScreen(float[])
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 2c9b7cf..29ad080 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -2341,20 +2341,20 @@
}
public interface MeasureResult {
- method public java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
+ method public java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
method public int getHeight();
method public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? getRulers();
method public int getWidth();
method public void placeChildren();
- property public abstract java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
+ property public abstract java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
property public abstract int height;
property public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers;
property public abstract int width;
}
@androidx.compose.ui.layout.MeasureScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureScope extends androidx.compose.ui.layout.IntrinsicMeasureScope {
- method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
- method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+ method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+ method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
}
@kotlin.DslMarker public @interface MeasureScopeMarker {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
index 2273b6a..3ea0bed 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.zIndex
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
@@ -104,6 +105,52 @@
assertThat(visitedChildren).containsExactly(child1, child2, child3).inOrder()
}
+ @Test
+ fun visitChildrenInOtherLayoutNodesInDrawOrder_zIndex() {
+ // Arrange.
+ abstract class TrackedNode : Modifier.Node()
+ val (node, child1, child2, child3) = List(5) { object : TrackedNode() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(Modifier.elementOf(node)) {
+ Box(Modifier.elementOf(child1).zIndex(10f))
+ Box(Modifier.elementOf(child2).zIndex(-10f))
+ Box(Modifier.elementOf(child3))
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node.visitChildren(Nodes.Any, zOrder = true) {
+ @Suppress("KotlinConstantConditions") if (it is TrackedNode) visitedChildren.add(it)
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child2, child3, child1).inOrder()
+ }
+
+ @Test
+ fun visitChildrenInOtherLayoutNodesInDrawOrder_subcompose() {
+ // Arrange.
+ val (node, child1, child2, child3) = List(5) { object : Modifier.Node() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ ReverseMeasureLayout(
+ Modifier.elementOf(node),
+ { Box(Modifier.elementOf(child1)) },
+ { Box(Modifier.elementOf(child2)) },
+ { Box(Modifier.elementOf(child3)) }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { node.visitChildren(Nodes.Any, zOrder = true) { visitedChildren.add(it) } }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child1, child2, child3).inOrder()
+ }
+
@Ignore("b/278765590")
@Test
fun skipsUnattachedLocalChild() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
index 63f82d9..6f1e610 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.zIndex
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
@@ -134,6 +135,58 @@
.inOrder()
}
+ @Test
+ fun visitsItemsAcrossLayoutNodesInDrawOrder_zIndex() {
+ // Arrange.
+ abstract class TrackedNode : Modifier.Node()
+ val (node, child1, child2, child3) = List(5) { object : TrackedNode() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(Modifier.elementOf(node)) {
+ Box(Modifier.elementOf(child1).zIndex(10f))
+ Box(Modifier.elementOf(child2).zIndex(-10f))
+ Box(Modifier.elementOf(child3))
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node.visitSubtreeIf(Nodes.Any, zOrder = true) {
+ @Suppress("KotlinConstantConditions") if (it is TrackedNode) visitedChildren.add(it)
+ true
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child2, child3, child1).inOrder()
+ }
+
+ @Test
+ fun visitsItemsAcrossLayoutNodesInDrawOrder_subcompose() {
+ // Arrange.
+ val (node, child1, child2, child3) = List(5) { object : Modifier.Node() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ ReverseMeasureLayout(
+ Modifier.elementOf(node),
+ { Box(Modifier.elementOf(child1)) },
+ { Box(Modifier.elementOf(child2)) },
+ { Box(Modifier.elementOf(child3)) }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node.visitSubtreeIf(Nodes.Any, zOrder = true) {
+ visitedChildren.add(it)
+ true
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child1, child2, child3).inOrder()
+ }
+
@Ignore("b/278765590")
@Test
fun skipsUnattachedItems() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
index 59d8aed..c2b585b5 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.zIndex
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
@@ -67,9 +68,6 @@
assertThat(visitedChildren).containsExactly(localChild1, localChild2).inOrder()
}
- // TODO(ralu): I feel that this order of visiting children is incorrect, and we should
- // visit children in the order of composition. So instead of a stack, we probably need
- // to use a queue to hold the intermediate nodes.
@Test
fun differentLayoutNodes() {
// Arrange.
@@ -79,10 +77,10 @@
val visitedChildren = mutableListOf<Modifier.Node>()
rule.setContent {
Box(Modifier.elementOf(node).elementOf(child1).elementOf(child2)) {
- Box(Modifier.elementOf(child5).elementOf(child6)) {
- Box(Modifier.elementOf(child7).elementOf(child8))
+ Box(Modifier.elementOf(child3).elementOf(child4)) {
+ Box(Modifier.elementOf(child5).elementOf(child6))
}
- Box { Box(Modifier.elementOf(child3).elementOf(child4)) }
+ Box { Box(Modifier.elementOf(child7).elementOf(child8)) }
}
}
@@ -95,6 +93,54 @@
.inOrder()
}
+ @Test
+ fun differentLayoutNodesInDrawOrder_zIndex() {
+ // Arrange.
+ abstract class TrackedNode : Modifier.Node()
+ val (node, child1, child2, child3, child4) = List(5) { object : TrackedNode() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(Modifier.elementOf(node)) {
+ Box(Modifier.elementOf(child1))
+ Box(Modifier.elementOf(child2).zIndex(10f)) {
+ Box(Modifier.elementOf(child3).zIndex(-10f))
+ }
+ Box { Box(Modifier.elementOf(child4)) }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node.visitSubtree(Nodes.Any, zOrder = true) {
+ @Suppress("KotlinConstantConditions") if (it is TrackedNode) visitedChildren.add(it)
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child1, child4, child2, child3).inOrder()
+ }
+
+ @Test
+ fun differentLayoutNodesInDrawOrder_subcompose() {
+ // Arrange.
+ val (node, child1, child2, child3, child4) = List(5) { object : Modifier.Node() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ ReverseMeasureLayout(
+ Modifier.elementOf(node),
+ { Box(Modifier.elementOf(child1)) },
+ { Box(Modifier.elementOf(child2)) { Box(Modifier.elementOf(child3)) } },
+ { Box { Box(Modifier.elementOf(child4)) } }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { node.visitSubtree(Nodes.Any, zOrder = true) { visitedChildren.add(it) } }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child1, child2, child3, child4).inOrder()
+ }
+
@Ignore("b/278765590")
@Test
fun skipsUnattached() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt
deleted file mode 100644
index 74a3c0e..0000000
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt
+++ /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.compose.ui.node
-
-import androidx.compose.runtime.collection.mutableVectorOf
-import org.junit.Assert
-import org.junit.Test
-
-class NestedVectorStackTests {
-
- @Test
- fun testPushPopOrder() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3))
- stack.push(mutableVectorOf(4, 5, 6))
- stack.push(mutableVectorOf())
- stack.push(mutableVectorOf(7))
- stack.push(mutableVectorOf(8, 9))
- val result = buildString {
- while (stack.isNotEmpty()) {
- append(stack.pop())
- }
- }
- Assert.assertEquals("987654321", result)
- }
-
- @Test
- fun testPopInBetweenPushes() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3, 4))
- stack.pop()
- stack.push(mutableVectorOf(4, 5, 6))
- stack.pop()
- stack.pop()
- stack.push(mutableVectorOf())
- stack.push(mutableVectorOf(5, 6, 7))
- stack.push(mutableVectorOf(8, 9))
- val result = buildString {
- while (stack.isNotEmpty()) {
- append(stack.pop())
- }
- }
- Assert.assertEquals("987654321", result)
- }
-}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
index 29b9c0a..e77e8d6 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
@@ -16,7 +16,10 @@
package androidx.compose.ui.node
+import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.InspectorInfo
/**
@@ -38,3 +41,30 @@
name = "testNode"
}
}
+
+@Composable
+internal fun ReverseMeasureLayout(modifier: Modifier, vararg contents: @Composable () -> Unit) =
+ SubcomposeLayout(modifier) { constraints ->
+ var layoutWidth = constraints.minWidth
+ var layoutHeight = constraints.minHeight
+ val subcomposes = mutableListOf<List<Placeable>>()
+
+ // Measure in reverse order
+ contents.reversed().forEachIndexed { index, content ->
+ subcomposes.add(
+ 0,
+ subcompose(index, content).map {
+ it.measure(constraints).also { placeable ->
+ layoutWidth = maxOf(layoutWidth, placeable.width)
+ layoutHeight = maxOf(layoutHeight, placeable.height)
+ }
+ }
+ )
+ }
+
+ layout(layoutWidth, layoutHeight) {
+
+ // But place in direct order - it sets direct draw order
+ subcomposes.forEach { placeables -> placeables.forEach { it.place(0, 0) } }
+ }
+ }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
index 5abbb34..5906b9f 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
@@ -15,8 +15,11 @@
*/
package androidx.compose.ui.window
-import android.content.res.Configuration
+import android.util.DisplayMetrics
import android.view.KeyEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_UP
+import android.view.View
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
@@ -35,8 +38,11 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.lerp
+import androidx.compose.ui.gesture.MotionEvent
+import androidx.compose.ui.gesture.PointerProperties
+import androidx.compose.ui.input.pointer.PointerCoords
import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertIsDisplayed
@@ -259,9 +265,9 @@
fun canFillScreenWidth_dependingOnProperty() {
var box1Width = 0
var box2Width = 0
- lateinit var configuration: Configuration
+ lateinit var displayMetrics: DisplayMetrics
rule.setContent {
- configuration = LocalConfiguration.current
+ displayMetrics = LocalView.current.context.resources.displayMetrics
Dialog(
onDismissRequest = {},
properties = DialogProperties(usePlatformDefaultWidth = false)
@@ -272,7 +278,7 @@
Box(Modifier.fillMaxSize().onSizeChanged { box2Width = it.width })
}
}
- val expectedWidth = with(rule.density) { configuration.screenWidthDp.dp.roundToPx() }
+ val expectedWidth = with(rule.density) { displayMetrics.widthPixels }
assertThat(box1Width).isEqualTo(expectedWidth)
assertThat(box2Width).isLessThan(box1Width)
}
@@ -313,6 +319,75 @@
}
}
+ @Test
+ fun dismissWhenClickingOutsideContent() {
+ var dismissed = false
+ var clicked = false
+ lateinit var composeView: View
+ val clickBoxTag = "clickBox"
+ rule.setContent {
+ Dialog(
+ onDismissRequest = { dismissed = true },
+ properties =
+ DialogProperties(
+ usePlatformDefaultWidth = false,
+ decorFitsSystemWindows = false
+ )
+ ) {
+ composeView = LocalView.current
+ Box(Modifier.size(10.dp).testTag(clickBoxTag).clickable { clicked = true })
+ }
+ }
+
+ // click inside the compose view
+ rule.onNodeWithTag(clickBoxTag).performClick()
+
+ rule.waitForIdle()
+
+ assertThat(dismissed).isFalse()
+ assertThat(clicked).isTrue()
+
+ clicked = false
+
+ // click outside the compose view
+ rule.waitForIdle()
+ var root = composeView
+ while (root.parent is View) {
+ root = root.parent as View
+ }
+
+ rule.runOnIdle {
+ val x = root.width / 4f
+ val y = root.height / 4f
+ val down =
+ MotionEvent(
+ eventTime = 0,
+ action = ACTION_DOWN,
+ numPointers = 1,
+ actionIndex = 0,
+ pointerProperties = arrayOf(PointerProperties(0)),
+ pointerCoords = arrayOf(PointerCoords(x, y)),
+ root
+ )
+ root.dispatchTouchEvent(down)
+ val up =
+ MotionEvent(
+ eventTime = 10,
+ action = ACTION_UP,
+ numPointers = 1,
+ actionIndex = 0,
+ pointerProperties = arrayOf(PointerProperties(0)),
+ pointerCoords = arrayOf(PointerCoords(x, y)),
+ root
+ )
+ root.dispatchTouchEvent(up)
+ }
+ rule.waitForIdle()
+
+ assertThat(dismissed).isTrue()
+ assertThat(clicked).isFalse()
+ }
+
private fun setupDialogTest(
closeDialogOnDismiss: Boolean = true,
dialogProperties: DialogProperties = DialogProperties(),
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
index e85b14c..cde57e5 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
@@ -15,12 +15,16 @@
*/
package androidx.compose.ui.window
+import android.content.res.Configuration
+import android.os.Build
import android.view.View
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.material.TextField
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Alignment
@@ -29,10 +33,18 @@
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.SoftwareKeyboardController
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.core.graphics.Insets
@@ -41,8 +53,10 @@
import androidx.core.view.WindowInsetsControllerCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
import org.junit.Assert.assertNotEquals
import org.junit.Rule
import org.junit.Test
@@ -129,6 +143,90 @@
assertNotEquals(Insets.NONE, imeInsets)
}
+ @Test
+ fun dialogCanTakeEntireScreen() {
+ var size = IntSize.Zero
+ var displayWidth = 0
+ var displayHeight = 0
+ var insetsLeft = 0
+ var insetsTop = 0
+ var insetsRight = 0
+ var insetsBottom = 0
+ var textTop = 0
+ var controller: SoftwareKeyboardController? = null
+ rule.setContent {
+ val displayMetrics = LocalView.current.resources.displayMetrics
+ controller = LocalSoftwareKeyboardController.current
+ displayWidth = displayMetrics.widthPixels
+ displayHeight = displayMetrics.heightPixels
+ Box(Modifier.fillMaxSize()) {
+ Dialog(
+ {},
+ properties =
+ DialogProperties(
+ decorFitsSystemWindows = false,
+ usePlatformDefaultWidth = false
+ )
+ ) {
+ val insets = WindowInsets.safeDrawing
+
+ Box(
+ Modifier.fillMaxSize()
+ .layout { m, c ->
+ val p = m.measure(c)
+ size = IntSize(p.width, p.height)
+ insetsTop = insets.getTop(this)
+ insetsLeft = insets.getLeft(this, layoutDirection)
+ insetsBottom = insets.getBottom(this)
+ insetsRight = insets.getRight(this, layoutDirection)
+ layout(p.width, p.height) { p.place(0, 0) }
+ }
+ .safeDrawingPadding()
+ ) {
+ TextField(
+ value = "Hello",
+ onValueChange = {},
+ Modifier.align(Alignment.BottomStart).testTag("textField").onPlaced {
+ layoutCoordinates ->
+ textTop = layoutCoordinates.positionInRoot().y.roundToInt()
+ }
+ )
+ }
+ }
+ }
+ }
+ rule.waitForIdle()
+
+ if (
+ Build.VERSION.SDK_INT >= 35 &&
+ rule.activity.applicationContext.applicationInfo.targetSdkVersion >= 35
+ ) {
+ // On SDK >= 35, the metrics is the size of the entire screen
+ assertThat(size.width).isEqualTo(displayWidth)
+ assertThat(size.height).isEqualTo(displayHeight)
+ } else {
+ // On SDK < 35, the metrics is the size of the screen with some insets removed
+ assertThat(size.width).isAtLeast(displayWidth)
+ assertThat(size.height).isAtLeast(displayHeight)
+ }
+ // There is going to be some insets
+ assertThat(maxOf(insetsLeft, insetsTop, insetsRight, insetsBottom)).isNotEqualTo(0)
+
+ val hardKeyboardHidden =
+ rule.runOnUiThread { rule.activity.resources.configuration.hardKeyboardHidden }
+ if (hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) {
+ return // can't launch the IME when the hardware keyboard is up.
+ }
+ val bottomInsetsBeforeIme = insetsBottom
+ val textTopBeforeIme = textTop
+ rule.onNodeWithTag("textField").requestFocus()
+ rule.waitUntil {
+ controller?.show()
+ insetsBottom != bottomInsetsBeforeIme
+ }
+ rule.runOnIdle { assertThat(textTop).isLessThan(textTopBeforeIme) }
+ }
+
private fun findDialogWindowProviderInParent(view: View): DialogWindowProvider? {
if (view is DialogWindowProvider) {
return view
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.android.kt
index ef4b85e..2a1655b 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.android.kt
@@ -45,4 +45,8 @@
super.onEndChanges()
root.owner?.onEndApplyChanges()
}
+
+ override fun reuse() {
+ current.onReuse()
+ }
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
index 83c30cd..475f5dba 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
@@ -20,13 +20,16 @@
import android.graphics.Outline
import android.os.Build
import android.view.ContextThemeWrapper
+import android.view.Gravity
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
+import android.view.View.OnLayoutChangeListener
import android.view.ViewGroup
import android.view.ViewOutlineProvider
import android.view.Window
import android.view.WindowManager
+import android.widget.FrameLayout
import androidx.activity.ComponentDialog
import androidx.activity.addCallback
import androidx.compose.runtime.Composable
@@ -57,8 +60,11 @@
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMaxBy
-import androidx.compose.ui.util.fastRoundToInt
+import androidx.core.view.OnApplyWindowInsetsListener
+import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsAnimationCompat
+import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.findViewTreeViewModelStoreOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
@@ -77,16 +83,19 @@
* @property securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the
* dialog's window.
* @property usePlatformDefaultWidth Whether the width of the dialog's content should be limited to
- * the platform default, which is smaller than the screen width.
+ * the platform default, which is smaller than the screen width. It is recommended to use
+ * [decorFitsSystemWindows] set to `false` when [usePlatformDefaultWidth] is false to support
+ * using the entire screen and avoiding UI glitches on some devices when the IME animates in.
* @property decorFitsSystemWindows Sets [WindowCompat.setDecorFitsSystemWindows] value. Set to
* `false` to use WindowInsets. If `false`, the
* [soft input mode][WindowManager.LayoutParams.softInputMode] will be changed to
* [WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE] and `android:windowIsFloating` is set to
- * `false` for Android [R][Build.VERSION_CODES.R] and earlier.
+ * `false` when [decorFitsSystemWindows] is false. When
+ * `targetSdk` >= [Build.VERSION_CODES.VANILLA_ICE_CREAM], [decorFitsSystemWindows] can only be
+ * `false` and this property doesn't have any effect.
*/
@Immutable
-actual class DialogProperties
-constructor(
+actual class DialogProperties(
actual val dismissOnBackPress: Boolean = true,
actual val dismissOnClickOutside: Boolean = true,
val securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
@@ -218,6 +227,7 @@
private var content: @Composable () -> Unit by mutableStateOf({})
var usePlatformDefaultWidth = false
+ var decorFitsSystemWindows = false
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
private set
@@ -229,50 +239,16 @@
createComposition()
}
- override fun internalOnMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- if (usePlatformDefaultWidth) {
- super.internalOnMeasure(widthMeasureSpec, heightMeasureSpec)
- } else {
- // usePlatformDefaultWidth false, so don't want to limit the dialog width to the Android
- // platform default. Therefore, we create a new measure spec for width, which
- // corresponds to the full screen width. We do the same for height, even if
- // ViewRootImpl gives it to us from the first measure.
- val displayWidthMeasureSpec =
- MeasureSpec.makeMeasureSpec(displayWidth, MeasureSpec.AT_MOST)
- val displayHeightMeasureSpec =
- MeasureSpec.makeMeasureSpec(displayHeight, MeasureSpec.AT_MOST)
- super.internalOnMeasure(displayWidthMeasureSpec, displayHeightMeasureSpec)
- }
- }
-
- override fun internalOnLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
- super.internalOnLayout(changed, left, top, right, bottom)
- // Now set the content size as fixed layout params, such that ViewRootImpl knows
- // the exact window size.
- if (!usePlatformDefaultWidth) {
- val child = getChildAt(0) ?: return
- window.setLayout(child.measuredWidth, child.measuredHeight)
- }
- }
-
- private val displayWidth: Int
- get() {
- val density = context.resources.displayMetrics.density
- return (context.resources.configuration.screenWidthDp * density).fastRoundToInt()
- }
-
- private val displayHeight: Int
- get() {
- val density = context.resources.displayMetrics.density
- return (context.resources.configuration.screenHeightDp * density).fastRoundToInt()
- }
-
@Composable
override fun Content() {
content()
}
}
+private fun adjustedDecorFitsSystemWindows(dialogProperties: DialogProperties, context: Context) =
+ dialogProperties.decorFitsSystemWindows &&
+ context.applicationInfo.targetSdkVersion < Build.VERSION_CODES.VANILLA_ICE_CREAM
+
private class DialogWrapper(
private var onDismissRequest: () -> Unit,
private var properties: DialogProperties,
@@ -288,16 +264,16 @@
*/
ContextThemeWrapper(
composeView.context,
- if (
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.S || properties.decorFitsSystemWindows
- ) {
+ if (adjustedDecorFitsSystemWindows(properties, composeView.context)) {
R.style.DialogWindowTheme
} else {
R.style.FloatingDialogWindowTheme
}
)
),
- ViewRootForInspector {
+ ViewRootForInspector,
+ OnApplyWindowInsetsListener,
+ OnLayoutChangeListener {
private val dialogLayout: DialogLayout
@@ -308,15 +284,12 @@
override val subCompositionView: AbstractComposeView
get() = dialogLayout
- private val defaultSoftInputMode: Int
-
init {
val window = window ?: error("Dialog has no window")
- defaultSoftInputMode =
- window.attributes.softInputMode and WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST
window.requestFeature(Window.FEATURE_NO_TITLE)
window.setBackgroundDrawableResource(android.R.color.transparent)
- WindowCompat.setDecorFitsSystemWindows(window, properties.decorFitsSystemWindows)
+ val decorFitsSystemWindows = adjustedDecorFitsSystemWindows(properties, context)
+ WindowCompat.setDecorFitsSystemWindows(window, decorFitsSystemWindows)
dialogLayout =
DialogLayout(context, window).apply {
// Set unique id for AbstractComposeView. This allows state restoration for the
@@ -336,10 +309,8 @@
override fun getOutline(view: View, result: Outline) {
result.setRect(0, 0, view.width, view.height)
// We set alpha to 0 to hide the view's shadow and let the composable to
- // draw
- // its own shadow. This still enables us to get the extra space needed
- // in the
- // surface.
+ // draw its own shadow. This still enables us to get the extra space
+ // needed in the surface.
result.alpha = 0f
}
}
@@ -359,7 +330,38 @@
// Turn of all clipping so shadows can be drawn outside the window
(window.decorView as? ViewGroup)?.disableClipping()
- setContentView(dialogLayout)
+ // Center the ComposeView in a FrameLayout
+ val frameLayout = FrameLayout(context)
+ frameLayout.addView(
+ dialogLayout,
+ FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT
+ )
+ .also { it.gravity = Gravity.CENTER }
+ )
+ frameLayout.setOnClickListener { onDismissRequest() }
+ ViewCompat.setOnApplyWindowInsetsListener(frameLayout, this)
+ ViewCompat.setWindowInsetsAnimationCallback(
+ frameLayout,
+ object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
+ override fun onProgress(
+ insets: WindowInsetsCompat,
+ runningAnimations: MutableList<WindowInsetsAnimationCompat>
+ ): WindowInsetsCompat {
+ return insets.inset(
+ dialogLayout.left,
+ dialogLayout.top,
+ frameLayout.width - dialogLayout.right,
+ frameLayout.height - dialogLayout.bottom
+ )
+ }
+ }
+ )
+ dialogLayout.addOnLayoutChangeListener(this)
+ frameLayout.addOnLayoutChangeListener(this)
+
+ setContentView(frameLayout)
dialogLayout.setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
dialogLayout.setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
dialogLayout.setViewTreeSavedStateRegistryOwner(
@@ -430,21 +432,42 @@
this.properties = properties
setSecurePolicy(properties.securePolicy)
setLayoutDirection(layoutDirection)
- if (properties.usePlatformDefaultWidth && !dialogLayout.usePlatformDefaultWidth) {
- // Undo fixed size in internalOnLayout, which would suppress size changes when
- // usePlatformDefaultWidth is true.
- window?.setLayout(
- WindowManager.LayoutParams.WRAP_CONTENT,
- WindowManager.LayoutParams.WRAP_CONTENT
- )
- }
dialogLayout.usePlatformDefaultWidth = properties.usePlatformDefaultWidth
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
- if (properties.decorFitsSystemWindows) {
- window?.setSoftInputMode(defaultSoftInputMode)
- } else {
- @Suppress("DEPRECATION")
- window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
+ val decorFitsSystemWindows = adjustedDecorFitsSystemWindows(properties, context)
+ dialogLayout.decorFitsSystemWindows = decorFitsSystemWindows
+ val window = window
+ if (window != null) {
+ val softInput =
+ when {
+ decorFitsSystemWindows ->
+ WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
+ @Suppress("DEPRECATION") WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+ else -> WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
+ }
+ window.setSoftInputMode(softInput)
+ val attrs = window.attributes
+ val measurementWidth =
+ if (properties.usePlatformDefaultWidth) {
+ WindowManager.LayoutParams.WRAP_CONTENT
+ } else {
+ WindowManager.LayoutParams.MATCH_PARENT
+ }
+ val measurementHeight =
+ if (properties.usePlatformDefaultWidth || decorFitsSystemWindows) {
+ WindowManager.LayoutParams.WRAP_CONTENT
+ } else {
+ WindowManager.LayoutParams.MATCH_PARENT
+ }
+ if (
+ attrs.width != measurementWidth ||
+ attrs.height != measurementHeight ||
+ attrs.gravity != Gravity.CENTER
+ ) {
+ attrs.width = measurementWidth
+ attrs.height = measurementHeight
+ attrs.gravity = Gravity.CENTER
+ window.attributes = attrs
}
}
}
@@ -466,6 +489,28 @@
// Prevents the dialog from dismissing itself
return
}
+
+ override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
+ val left = dialogLayout.left
+ val top = dialogLayout.top
+ val right = v.width - dialogLayout.right
+ val bottom = v.height - dialogLayout.bottom
+ return insets.inset(left, top, right, bottom)
+ }
+
+ override fun onLayoutChange(
+ v: View,
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ oldLeft: Int,
+ oldTop: Int,
+ oldRight: Int,
+ oldBottom: Int
+ ) {
+ v.requestApplyInsets()
+ }
}
@Composable
diff --git a/compose/ui/ui/src/androidMain/res/values/styles.xml b/compose/ui/ui/src/androidMain/res/values/styles.xml
index e1211d4..d0e837b 100644
--- a/compose/ui/ui/src/androidMain/res/values/styles.xml
+++ b/compose/ui/ui/src/androidMain/res/values/styles.xml
@@ -19,11 +19,13 @@
<style name="DialogWindowTheme">
<item name="android:windowClipToOutline">false</item>
</style>
- <!-- Style for decorFitsSystemWindows = false on API 30 and earlier. WindowInsets won't
- be set on Dialogs without android:windowIsFloating set to false. -->
+ <!-- Style for decorFitsSystemWindows = false -->
<style name="FloatingDialogWindowTheme">
<item name="android:windowClipToOutline">false</item>
<item name="android:dialogTheme">@style/FloatingDialogTheme</item>
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+ <item name="android:backgroundDimEnabled">true</item>
</style>
<style name="FloatingDialogTheme">
<item name="android:windowIsFloating">false</item>
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
index c1363c7..0328f65 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
@@ -209,14 +209,14 @@
layout(d)
}
val recorder = Recorder()
- x.visitSubtree(Nodes.Draw, recorder)
+ x.visitSubtree(Nodes.Draw, block = recorder)
assertThat(recorder.recorded)
.isEqualTo(
listOf(
a.wrapped,
b,
- d,
c.wrapped,
+ d,
)
)
}
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt
deleted file mode 100644
index 951cb3d..0000000
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt
+++ /dev/null
@@ -1,76 +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.compose.ui.node
-
-import androidx.compose.runtime.collection.mutableVectorOf
-import com.google.common.truth.Truth
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class NestedVectorStackTest {
-
- @Test
- fun testEnumerationOrder() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3))
- stack.push(mutableVectorOf(4, 5, 6))
-
- Truth.assertThat(stack.enumerate()).isEqualTo(listOf(6, 5, 4, 3, 2, 1))
- }
-
- @Test
- fun testEnumerationOrderPartiallyPoppingMiddleVectors() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3))
-
- Truth.assertThat(stack.pop()).isEqualTo(3)
-
- stack.push(mutableVectorOf(4, 5, 6))
-
- Truth.assertThat(stack.pop()).isEqualTo(6)
-
- Truth.assertThat(stack.enumerate()).isEqualTo(listOf(5, 4, 2, 1))
- }
-
- @Test
- fun testEnumerationOrderFullyPoppingMiddleVectors() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3))
-
- Truth.assertThat(stack.pop()).isEqualTo(3)
- Truth.assertThat(stack.pop()).isEqualTo(2)
- Truth.assertThat(stack.pop()).isEqualTo(1)
-
- stack.push(mutableVectorOf(4, 5, 6))
-
- Truth.assertThat(stack.pop()).isEqualTo(6)
-
- Truth.assertThat(stack.enumerate()).isEqualTo(listOf(5, 4))
- }
-}
-
-internal fun <T> NestedVectorStack<T>.enumerate(): List<T> {
- val result = mutableListOf<T>()
- var item: T? = pop()
- while (item != null) {
- result.add(item)
- item = if (isNotEmpty()) pop() else null
- }
- return result
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
index 8010fb9..d64d81b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
@@ -90,7 +90,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
index dc853ae..1bdba48 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
@@ -351,7 +351,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
@@ -365,7 +365,7 @@
override val height: Int
get() = h
- override val alignmentLines: Map<AlignmentLine, Int>
+ override val alignmentLines: Map<out AlignmentLine, Int>
get() = alignmentLines
override val rulers: (RulerScope.() -> Unit)?
@@ -385,7 +385,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
@@ -399,7 +399,7 @@
override val height: Int
get() = h
- override val alignmentLines: Map<AlignmentLine, Int>
+ override val alignmentLines: Map<out AlignmentLine, Int>
get() = alignmentLines
override val rulers: (RulerScope.() -> Unit)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt
index a2b16cc..e7a3137 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt
@@ -19,7 +19,7 @@
* Alignment lines that can be used by parents to align this layout. This only includes the
* alignment lines of this layout and not children.
*/
- val alignmentLines: Map<AlignmentLine, Int>
+ val alignmentLines: Map<out AlignmentLine, Int>
/**
* An optional lambda function used to create [Ruler]s for child layout. This may be
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt
index 36e7ea8..978165e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt
@@ -47,7 +47,7 @@
fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
+ alignmentLines: Map<out AlignmentLine, Int> = emptyMap(),
placementBlock: Placeable.PlacementScope.() -> Unit
) = layout(width, height, alignmentLines, null, placementBlock)
@@ -69,7 +69,7 @@
fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
+ alignmentLines: Map<out AlignmentLine, Int> = emptyMap(),
rulers: (RulerScope.() -> Unit)? = null,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index 442dada..08a5f0c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -911,7 +911,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
@@ -923,7 +923,7 @@
override val height: Int
get() = height
- override val alignmentLines: Map<AlignmentLine, Int>
+ override val alignmentLines: Map<out AlignmentLine, Int>
get() = alignmentLines
override val rulers: (RulerScope.() -> Unit)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index ffc88f5..f967cd8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -99,48 +99,33 @@
return null
}
-internal inline fun DelegatableNode.visitSubtree(mask: Int, block: (Modifier.Node) -> Unit) {
- // TODO(lmr): we might want to add some safety wheels to prevent this from being called
- // while one of the chains is being diffed / updated.
- checkPrecondition(node.isAttached) { "visitSubtree called on an unattached node" }
- var node: Modifier.Node? = node.child
- var layout: LayoutNode? = requireLayoutNode()
- // we use this bespoke data structure here specifically for traversing children. In the
- // depth first traversal you would typically do a `stack.addAll(node.children)` type
- // call, but to avoid enumerating the vector and moving into our stack, we simply keep
- // a stack of vectors and keep track of where we are in each
- val nodes = NestedVectorStack<LayoutNode>()
- while (layout != null) {
- // NOTE: the ?: is important here for the starting condition, since we are starting
- // at THIS node, and not the head of this node chain.
- node = node ?: layout.nodes.head
- if (node.aggregateChildKindSet and mask != 0) {
- while (node != null) {
- if (node.kindSet and mask != 0) {
- block(node)
- }
- node = node.child
- }
- }
- node = null
- nodes.push(layout._children)
- layout = if (nodes.isNotEmpty()) nodes.pop() else null
+private fun LayoutNode.getChildren(zOrder: Boolean) =
+ if (zOrder) {
+ zSortedChildren
+ } else {
+ _children
}
+
+private fun MutableVector<Modifier.Node>.addLayoutNodeChildren(
+ node: Modifier.Node,
+ zOrder: Boolean,
+) {
+ node.requireLayoutNode().getChildren(zOrder).forEachReversed { add(it.nodes.head) }
}
-private fun MutableVector<Modifier.Node>.addLayoutNodeChildren(node: Modifier.Node) {
- node.requireLayoutNode()._children.forEachReversed { add(it.nodes.head) }
-}
-
-internal inline fun DelegatableNode.visitChildren(mask: Int, block: (Modifier.Node) -> Unit) {
+internal inline fun DelegatableNode.visitChildren(
+ mask: Int,
+ zOrder: Boolean,
+ block: (Modifier.Node) -> Unit
+) {
check(node.isAttached) { "visitChildren called on an unattached node" }
val branches = mutableVectorOf<Modifier.Node>()
val child = node.child
- if (child == null) branches.addLayoutNodeChildren(node) else branches.add(child)
+ if (child == null) branches.addLayoutNodeChildren(node, zOrder) else branches.add(child)
while (branches.isNotEmpty()) {
val branch = branches.removeAt(branches.lastIndex)
if (branch.aggregateChildKindSet and mask == 0) {
- branches.addLayoutNodeChildren(branch)
+ branches.addLayoutNodeChildren(branch, zOrder)
// none of these nodes match the mask, so don't bother traversing them
continue
}
@@ -159,11 +144,15 @@
* visit the shallow tree of children of a given mask, but if block returns true, we will continue
* traversing below it
*/
-internal inline fun DelegatableNode.visitSubtreeIf(mask: Int, block: (Modifier.Node) -> Boolean) {
+internal inline fun DelegatableNode.visitSubtreeIf(
+ mask: Int,
+ zOrder: Boolean,
+ block: (Modifier.Node) -> Boolean
+) {
checkPrecondition(node.isAttached) { "visitSubtreeIf called on an unattached node" }
val branches = mutableVectorOf<Modifier.Node>()
val child = node.child
- if (child == null) branches.addLayoutNodeChildren(node) else branches.add(child)
+ if (child == null) branches.addLayoutNodeChildren(node, zOrder) else branches.add(child)
outer@ while (branches.isNotEmpty()) {
val branch = branches.removeAt(branches.size - 1)
if (branch.aggregateChildKindSet and mask != 0) {
@@ -176,7 +165,7 @@
node = node.child
}
}
- branches.addLayoutNodeChildren(branch)
+ branches.addLayoutNodeChildren(branch, zOrder)
}
}
@@ -264,33 +253,41 @@
return null
}
-internal inline fun <reified T> DelegatableNode.visitSubtree(
- type: NodeKind<T>,
- block: (T) -> Unit
-) = visitSubtree(type.mask) { it.dispatchForKind(type, block) }
-
internal inline fun <reified T> DelegatableNode.visitChildren(
type: NodeKind<T>,
+ zOrder: Boolean = false,
block: (T) -> Unit
-) = visitChildren(type.mask) { it.dispatchForKind(type, block) }
+) = visitChildren(type.mask, zOrder) { it.dispatchForKind(type, block) }
internal inline fun <reified T> DelegatableNode.visitSelfAndChildren(
type: NodeKind<T>,
+ zOrder: Boolean = false,
block: (T) -> Unit
) {
node.dispatchForKind(type, block)
- visitChildren(type.mask) { it.dispatchForKind(type, block) }
+ visitChildren(type.mask, zOrder) { it.dispatchForKind(type, block) }
}
internal inline fun <reified T> DelegatableNode.visitSubtreeIf(
type: NodeKind<T>,
+ zOrder: Boolean = false,
block: (T) -> Boolean
) =
- visitSubtreeIf(type.mask) foo@{ node ->
+ visitSubtreeIf(type.mask, zOrder) foo@{ node ->
node.dispatchForKind(type) { if (!block(it)) return@foo false }
true
}
+internal inline fun <reified T> DelegatableNode.visitSubtree(
+ type: NodeKind<T>,
+ zOrder: Boolean = false,
+ block: (T) -> Unit
+) =
+ visitSubtreeIf(type.mask, zOrder) {
+ it.dispatchForKind(type, block)
+ true
+ }
+
internal fun DelegatableNode.has(type: NodeKind<*>): Boolean =
node.aggregateChildKindSet and type.mask != 0
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
index a4d667fb..fe22dff 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
@@ -94,9 +94,9 @@
/** The alignment lines of this layout, inherited + intrinsic */
private val alignmentLineMap: MutableMap<AlignmentLine, Int> = hashMapOf()
- fun getLastCalculation(): Map<AlignmentLine, Int> = alignmentLineMap
+ fun getLastCalculation(): Map<out AlignmentLine, Int> = alignmentLineMap
- protected abstract val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>
+ protected abstract val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>
protected abstract fun NodeCoordinator.getPositionFor(alignmentLine: AlignmentLine): Int
@@ -201,7 +201,7 @@
internal class LayoutNodeAlignmentLines(alignmentLinesOwner: AlignmentLinesOwner) :
AlignmentLines(alignmentLinesOwner) {
- override val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>
+ override val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>
get() = measureResult.alignmentLines
override fun NodeCoordinator.getPositionFor(alignmentLine: AlignmentLine): Int =
@@ -215,7 +215,7 @@
internal class LookaheadAlignmentLines(alignmentLinesOwner: AlignmentLinesOwner) :
AlignmentLines(alignmentLinesOwner) {
- override val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>
+ override val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>
get() = lookaheadDelegate!!.measureResult.alignmentLines
override fun NodeCoordinator.getPositionFor(alignmentLine: AlignmentLine): Int =
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 3df4e68..bc03b51 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
@@ -950,7 +950,7 @@
return true
}
- override fun calculateAlignmentLines(): Map<AlignmentLine, Int> {
+ override fun calculateAlignmentLines(): Map<out AlignmentLine, Int> {
if (!duringAlignmentLinesQuery) {
// Mark alignments used by modifier
if (layoutState == LayoutState.Measuring) {
@@ -1278,7 +1278,7 @@
}
}
- override fun calculateAlignmentLines(): Map<AlignmentLine, Int> {
+ override fun calculateAlignmentLines(): Map<out AlignmentLine, Int> {
if (!duringAlignmentLinesQuery) {
if (layoutState == LayoutState.LookaheadMeasuring) {
// Mark alignments used by modifier
@@ -1894,7 +1894,7 @@
fun layoutChildren()
/** Recalculate the alignment lines if dirty, and layout children as needed. */
- fun calculateAlignmentLines(): Map<AlignmentLine, Int>
+ fun calculateAlignmentLines(): Map<out AlignmentLine, Int>
/**
* Parent [AlignmentLinesOwner]. This will be the AlignmentLinesOwner for the same pass but for
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 f5e4439..8bf8d59 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
@@ -203,7 +203,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: PlacementScope.() -> Unit
): MeasureResult {
@@ -215,7 +215,7 @@
override val height: Int
get() = height
- override val alignmentLines: Map<AlignmentLine, Int>
+ override val alignmentLines: Map<out AlignmentLine, Int>
get() = alignmentLines
override val rulers: (RulerScope.() -> Unit)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NestedVectorStack.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NestedVectorStack.kt
deleted file mode 100644
index 7f93d07..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NestedVectorStack.kt
+++ /dev/null
@@ -1,58 +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.compose.ui.node
-
-import androidx.compose.runtime.collection.MutableVector
-
-internal class NestedVectorStack<T> {
- // number of vectors in the stack
- private var size = 0
- // holds the current "top" index for each vector
- private var currentIndexes = IntArray(16)
- private var vectors = arrayOfNulls<MutableVector<T>>(16)
-
- fun isNotEmpty(): Boolean {
- return size > 0 && currentIndexes[size - 1] >= 0
- }
-
- fun pop(): T {
- check(size > 0) { "Cannot call pop() on an empty stack. Guard with a call to isNotEmpty()" }
- val indexOfVector = size - 1
- val indexOfItem = currentIndexes[indexOfVector]
- val vector = vectors[indexOfVector]!!
- if (indexOfItem > 0) currentIndexes[indexOfVector]--
- else if (indexOfItem == 0) {
- vectors[indexOfVector] = null
- size--
- }
- return vector[indexOfItem]
- }
-
- fun push(vector: MutableVector<T>) {
- // if the vector is empty there is no reason for us to add it
- if (vector.isEmpty()) return
- val nextIndex = size
- // check to see that we have capacity to add another vector
- if (nextIndex >= currentIndexes.size) {
- currentIndexes = currentIndexes.copyOf(currentIndexes.size * 2)
- vectors = vectors.copyOf(vectors.size * 2)
- }
- currentIndexes[nextIndex] = vector.size - 1
- vectors[nextIndex] = vector
- size++
- }
-}
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 b522a01..9a1ba69 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
@@ -1407,7 +1407,7 @@
@Suppress("PrimitiveInCollection")
private fun compareEquals(
a: MutableObjectIntMap<AlignmentLine>?,
- b: Map<AlignmentLine, Int>
+ b: Map<out AlignmentLine, Int>
): Boolean {
if (a == null) return false
if (a.size != b.size) return false
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 ed0ed4d..007491b 100644
--- a/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
@@ -37,6 +37,7 @@
import androidx.test.filters.MediumTest;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -365,6 +366,7 @@
*/
@MediumTest
@Test
+ @Ignore("JobIntentService is deprecated and no longer maintained")
public void testEnqueueOne() throws Throwable {
initStatics();
@@ -386,6 +388,7 @@
*/
@MediumTest
@Test
+ @Ignore("JobIntentService is deprecated and no longer maintained")
public void testEnqueueMultiple() throws Throwable {
initStatics();
@@ -410,6 +413,7 @@
*/
@MediumTest
@Test
+ @Ignore("JobIntentService is deprecated and no longer maintained")
public void testEnqueueSubWork() throws Throwable {
initStatics();
@@ -439,6 +443,7 @@
*/
@MediumTest
@Test
+ @Ignore("JobIntentService is deprecated and no longer maintained")
@RequiresApi(26)
public void testStopWhileWorking() throws Throwable {
if (Build.VERSION.SDK_INT < 26) {
diff --git a/development/update_studio.sh b/development/update_studio.sh
index cdd888e..3fa7ebb 100755
--- a/development/update_studio.sh
+++ b/development/update_studio.sh
@@ -48,7 +48,7 @@
sed -i "s/androidStudio = .*/androidStudio = \"$STUDIO_VERSION\"/g" gradle/libs.versions.toml
# update settings.gradle
-sed -i "s/com.android.settings:com.android.settings.gradle.plugin:.*/com.android.settings:com.android.settings.gradle.plugin:$AGP_VERSION\")/g" settings.gradle
+sed -i "s/com.android.settings:com.android.settings.gradle.plugin:[0-9a-z\.\-]*/com.android.settings:com.android.settings.gradle.plugin:$AGP_VERSION\")/g" settings.gradle
# Pull all UTP artifacts for ADT version
ADT_VERSION=${3:-$LINT_VERSION}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5165817..99e5b63 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -67,7 +67,7 @@
spdxGradlePlugin = "0.6.0"
sqldelight = "1.3.0"
retrofit = "2.7.2"
-wire = "4.9.7"
+wire = "5.0.0"
core = "1.12.0"
xmlApis = "1.4.01"
yarn = "1.22.17"
diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys
index efdf962..4a0d0b9 100644
--- a/gradle/verification-keyring.keys
+++ b/gradle/verification-keyring.keys
@@ -3,7 +3,7 @@
sub CF771F914C2A4A73
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBE2fCWARBAC3v9wYo5kmynmVP+43ccamidflSLQjjpsXpSDLPFokGxeuw0OC
QJy46m8b5ACoCqRlfwnRRcEHxiSlaBATJA6hi7NRO41R39C62JXsIxNJR16JNQ5k
@@ -33,7 +33,7 @@
pub 82B5574242C20D6F
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBFC1VWUBDADZwqBEEmSjwy2JADG0qCpvVQzC5KszL0CjzqTLPMBmLKNuc/36
26MU4yI8Y+pcCTnC3LN9hrI0hxiP4zFFFyLYKkUWCZRAwj4OQlnyTDKa9frKBMed
@@ -52,7 +52,7 @@
sub 43115D7B115DB0C0
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFSR0DQBCADw8XL+xgFg9WVPknAIqqb0sUIZ3yNNr8LkuNtwQXnwAcSJkHSt
C1k2CIKwRPPfcLsb51l3SpxFTs/s5yhyiknDfjqP8IFtLocBSsn3kD4VRjcxFQhc
@@ -80,7 +80,7 @@
uid The Legion of the Bouncy Castle Inc. (Maven Repository Artifact Signer) <[email protected]>
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGR/8HUBDADJ+V5VgTXFG4xVI/1r07a/pTXoAQhHyJMkVdFScGARsps07VXI
IsYgPsifOFU55E7uRMZPTLAx5F1uxoZAWGtXIz0d4ISKhobFquH8jZe7TnsJBJNV
@@ -101,7 +101,7 @@
sub 594E23256A36A392
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBEqQOcwBEACdPSfBAkHm1b2GdOjB3gGerx/JDn3zYNnNpcQrM8Do0bxDwlfT
qwLA0P9ju4mzTfHU5kEvm2lrXz8QCZPLe9eY6GxzzSbeXtt+4fP84/YGmsK6DQTy
@@ -146,7 +146,7 @@
sub 8B2A34A7D4A9B8B3
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFrKW9IBEACkqUvM7hU1WqOOeb1gZ7pUsRliHuoUvYIrd+hdp+qhPmJ0NG0W
YhZK5UtJBmqvtHKRkbwYxUuya9zlBmCfQFf0GpFKJ65JSrPSkZADI3aZ4aUkxIUw
@@ -191,7 +191,7 @@
sub 8832A83FA3060393
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBD9AzmcRBACMqgb7IFvC/nLxw7mUAgHENeZXY3JOQJ8wVBevIbbMEeFvzHE2
diFydqUXocPexduYr0ahkf033WvWdAiNqDLfVW/HFOsc1TpjbHkqPUHtJ62Ya5tg
@@ -220,7 +220,7 @@
sub 51F5B36C761AA122
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFoQh54BEADOuivAfgGKc4/zDwx+AwJdctjTT0znL9knRTYG6ediv2Eq+CXm
gBM9m5twl+qhUB1NtrdHb4BH49VY9/gHr3JDyo5ewu96qkbeQl4pxW0zmHg/yJx7
@@ -263,7 +263,7 @@
pub 86FDC7E2A11262CB
sub 59BA7BFEAD3D7F94
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBE2kzuwBCACYV+G9yxNkSjAKSji0B5ipMGM74JAL1Ogtcu+993pLHHYsdXri
WWXi37x9PLjeHxw63mN26SFyrbMJ4A8erLB03PDjw0DEzAwiu9P2vSvL/RFxGBbk
@@ -291,7 +291,7 @@
sub 1AFEC329B615D06C
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBEdddbQRBADRgstdUZq7ceq3NYcR5kpoU2tN2Zvg1vptE9FxpDbL73gdLWnI
C7IAx+NNjdG7Ncdg+u10UZv6OSmhWAd8ubWcD9JxKtS4UXkNPHxhHFHqVPHuCwsQ
@@ -322,7 +322,7 @@
pub 88BB19A33A18445F
sub FF59C22B07640A16
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBE//SjoBCADao3lh/I96fWIY2ZU49ljtHR4Vnzmifm3URFNuv/c8McWGxxCy
Y1+oolgVuJcy4hCqcgbkwTiAfBhjZSmsC1QK/2Vs1awFzGccPcgTBakFw/TUav12
@@ -350,7 +350,7 @@
sub 5E9AEEBA28836032
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGUVRogBEAChVh0t3YAJIdreb6SP/lf4x097IRpOiJ7Ww+DDtXFUhKJBwgfC
4T10TBGP835tV6TfkEeCPGWABoxaD88zUlSHs7k7v/SfedwfOKbOE3c+oR43JL7P
@@ -395,7 +395,7 @@
sub E98008460EB9BB34
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBF8kuOUBCACo8/VYVfmglgTgmai5FvmNzKi9XIJIK4fHCA1r+t47aGkGy36E
dSOlApDjqbtuodnyH4jiyBvT599yeMA0O/Pr+zL+dOwdT1kYL/owvT0U9oczvwUj
@@ -422,7 +422,7 @@
pub 8E3F0DE7AE354651
sub D3047B0BA4452AE1
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFMnpeABCAC+vckg+AqDG5Sg+GKbA5t2knu72aD000Qle1X//SjTvPHz0L1v
rUNzwrqlmah17usczZHOoOCaGjSUFl3nPmBEOlLBh6L4+e2Av8PSbP0qUneaQVgi
@@ -450,7 +450,7 @@
sub 37AE8263DA3084E5
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBFu8+5UBDAC74QfHuMgQVUqSmwgE+zWX1YKY4w9a0vKrj7E4tRY8JXaX6GtH
TWnOkAndsxK3kpUyRx8S7f4HL4Sxf05Tar22nrNkuiQddKjLsdlH7VIolGW1eFm2
@@ -487,7 +487,7 @@
sub 9D149DAC4AC24632
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFPzzfABCADK/wEIRhUCUTj00TcBOxGTPs5ad8jn5D01P7P5ILpLOgmnUp1I
E3EYy54PQYjDIeOFvEmEywvwMRV8yCVhhYGpOPqbegKwcebXoiMGhJjuRf2nPbdZ
@@ -516,7 +516,7 @@
sub E04D6BFB21395F43
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBF5ZgzYBEADQvBgzh4vKJqO3amYjIUJ85OPCjJdK5G0xSH/nqOGZbo78DjLx
3PosyuYqV6sIfaCx+NWv+pYnpKdQHbAnQygggjOTByIQJtpmMT80dUsXTxAk6Aim
@@ -561,7 +561,7 @@
sub F57552EA2A2B5F3F
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFUITeIBCADHIijQBuGmC+Oo/XE5qIXxzZ2cK26uD0tlDqaPhRLWt5RP3EbU
b6X8ZLE2AlmawFzU0IqndrCDxSyuo9+ZFQRYT+stf+qHFjtvVQJh2+4L2LpcPrnf
@@ -590,7 +590,7 @@
sub 684EB33FB007E676
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQSuBEwVyy4RDAC9hprQuF4fCPCYdtMlb0Mfb+6G2TqerT1MebLm8/KHCRnPbFLg
PwGgcyynLX5R2nXUb6oBZQByDN/Dal0UMuC19KeZX83LTcFE9vr516BMXLXXKmM9
@@ -643,7 +643,7 @@
pub 971B04F56669B805
sub D3664677F6280E44
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBEzZjwMBCAC0ecfE/qkdgq8uJv1c1ZlzegeWH/lxW0W3SWK2RwaHx7LrfpiN
WhxLkXbK6fkf86FN4579W1+9Qef2yjZCwTfLe6bqj3zZGQWSu7HPw1mmhf9lbhJ9
@@ -671,7 +671,7 @@
sub CC3328A2F49A80C8
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFhlXQUBCACoN2nTeSRVZnGoktKHyiCgeYQ/hEKKKDDAbWubnnQwonCTILaN
Qw3GmIT6plmi9iy4rl+rJprSzDeQDZngQCx1KPYcXCrrc0pnjERDaogw9fC3c3z2
@@ -700,7 +700,7 @@
sub C327DD2B96A50E1C
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBF6WyHgBEADOrbvGGDYVckFcUofqKiYrBneClFJH1ANheF+KIekmnFV2SH1Z
RS2rw12IbpCjwqjhFTMWH2UTLF6pAsSGIufTrSVUAF2WxHw84Y60KmwuYayJCVd3
@@ -745,7 +745,7 @@
sub B89991D171A02F5C
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBF9amNkBEADKyJj5snYd8bZpONpu1QHf7c/TK9HxcMzGZaIv9QzViX6CtEHb
2Q2x6ejXQ2frECMrvns5JAJd21B6215EhlOqrHSMkTrQ6fvOIfWd0huZ0QHr4FME
@@ -791,7 +791,7 @@
sub 80CFA7C482552DC3
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGJGMxoBDADF9xkWwxwN72wRh0al9ARzTTIHpcVBIjDij1Xr768zMMRdKOsQ
aEHRTBKArAfGl6Xt6CfYnu3wMgEDUfh50s9NPOKvhpKtqdIlUxZLEJ807ebW3MD+
@@ -829,7 +829,7 @@
sub 6C907406A9482E08
sub B2581403B6FA2318
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBEJDQwIRBAD8GFadoCUDLBvFZaR/xu2KS+k8dgfqtYKXpEQ2CH05lpFWrTXo
C6h9koiHcsMKtgLFE0LG6nHTUbLs2W7gBCaCk9HzMmsFI5D7RDbyga0wvvg96y4d
@@ -877,7 +877,7 @@
sub D66472CF54179CC4
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFKD+PgBEAC8IkWujQlmU0/7+QPZFsc/z/rXgg7BQyo330QK4HeMzeCK6WHa
SWzVDM9h6nFDs6Xln6YexbZUjLsxS/a/Ox2i26Qg8B+NghgiratbdJsByRrU/3la
@@ -920,7 +920,7 @@
pub 9A2C7A98E457C53D
sub 92AECA6AC21DB816
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFjN6bABCADHL68BhnXVXyJhOA9kO9cBwJXKmav2RftpcpXfaeHJTy+CMQa4
rFxokx5W7E1IPlLg0qJfKSMeWhimVLOsLhY1MZV8Mb4fkK+SlDz/ah+5ej6dzOs3
@@ -948,7 +948,7 @@
sub 2F3C9EEB05D1D1E3
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBEzH8KcBEADyHAdW2cHj2SfvmdAG3yG0NIlfdSWXG06k7BGUatjNfaIGHVSv
r0U3WlGlUowiLqPhZfQf3v/tvd7yDKZ1Tk3p3A3rEVEZQ26u/o66QgTNjl15YmaR
@@ -993,7 +993,7 @@
sub 458AF764D812A037
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBEWjofgRBACePEiXmSvjcjUgWkNAFQ/w7w2VSEqe1vuTCrta+ER9JsvhwipP
2/BEHigFf99TlU0p1UC591LMeYP2UXfQnb3jiyEPKxA06aj1fTGGMoNMAilymvgd
@@ -1026,7 +1026,7 @@
sub 32E3DF6FC5E91334
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBEzDDl0BEADHvJW2uff8vfxbfy0IvNOK4aytU+HVEvKEmuSqYEzC8i3BF6RT
LOxTeRFlu92rYz5ypD0mdNCzQaH0xbkcjialP6FpPCByrM9fFv6hmxZFSY71rvqz
@@ -1071,7 +1071,7 @@
sub 81176177BB514041
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBF3xPHMBCAC7OXU5uXXKttUU/BwWm6q08NBC3ybk0fNIfoITWiFA1RtxO7S3
K4ijImBnLLb7ivjpTtIWzUwFAfSZHc3LgS/TBQJQ2PGsO4/AdaMAcs69irgfoPYY
@@ -1100,7 +1100,7 @@
sub 923C08F9417B222D
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFKws7QBEADEy9+PqF0cjeS1yG4xMRBV+teFNsS+WZW1ATDBl5ETASqMZT7R
zFWjMWq8Kf3iTMfmPlKVCPIFH1FG+SgMvWpQEEcLCOmUkJR7UYtn2y3vaXXYqawz
@@ -1145,7 +1145,7 @@
sub E3F6790A5A167F5A
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGHDIagBEADpzdCwVjVlHuo8qpu9HtmqNpEW4TB7y6+NX7Q39mj8w+iVskE1
sL0+BOCdP6ZMiQziWbOQ2FxCd3mD0ixZ7v1i7+0jowySPacJbVNaPPECP38gDte4
@@ -1190,7 +1190,7 @@
sub BA6D22590B3F9BEA
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBE4waOEBCADHDHNTq1NRR5TSooIrKY0BTQnaLfjKZfcJOwp+btBJrOUO7+e/
V3M4DZQclj/e8SBiVmRPK8Oyrv6i5B5+Ee/qNlLjWiO10AJ/PLRjYdoW1V6PlTm7
@@ -1219,7 +1219,7 @@
sub 6366592024774157
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBDsSIk4RBADSCj6rUjV64tYCGT1DYKYR7GthyWpNdGHSYLbETBcDatAe1dzQ
5NsCgfrlybfyeY+y1lxr3T9bqf6zJWDw/718wff96qmmv1qzexSYtmIrj+h53V82
@@ -1248,7 +1248,7 @@
sub 6A2038967E03726F
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFRdA40BCAC0zSALsOjfjr+gO8q+HV4qPWuIRB8S4z//jCEpKypyCRR9sA0W
IDHG6OqG5fO1bP6VsHvSx32E8YUf0bi8eGgpKj5gJ9jmausRvRHtUHJ0pvZRBw51
@@ -1277,7 +1277,7 @@
sub 8183E80D264EE073
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBE8YNGIBEADEgcfvs8TL3X2Ql62HJ6SrXWAOoHw5CquJxUQkvBGesIT1Hk24
exiPwrlNE1qUjbVlef1Cwk9ZfwMOpJdfP2MQQbx0nxxqv+JtsoeXUy9bTSvZYBUL
@@ -1322,7 +1322,7 @@
sub 21200D723F53CE38
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFy+swoBCADGyV4k02OjVCrziziYIvIO+qDm8Yqxt4KVd+ISw2DvmKVcP7lx
z5WVGvxVdAl+Xy7FdcrIJYFCsYfFFxPz+BM6+np2c477HkdIcDwBWiHEoOqMehax
@@ -1351,7 +1351,7 @@
sub 9C4C23E6FFE405BD
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBE+xZxIBCACzKctn4ez8xOC0pGThhAwjYWGkzcwK4HNaC1usHThBFz3/t8JN
OqUXRixLyi5wELN6GHlsGVUQS3IfB4JtuhScsieSB8PTree68/knMq6JI08mJqZr
@@ -1379,7 +1379,7 @@
uid Tobias Warneke (for development purposes) <[email protected]>
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBFJQhigBDADpuhND/VUQwJT0nnJxfjAIur59hyaZZ3Ph/KIgmCneyq7lzYO6
xa1ucH8mqNBVNLLBhs4CjihBddU/ZKTX3WnZyhQKQMZr3Tg+TCNFmAR4/hnZ3NjZ
@@ -1397,7 +1397,7 @@
pub A730529CA355A63E
sub D5A25EF82542C54A
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBEUQYOcRBADsCu4zTVaB4TOhV7NyTvHhG1bqN+3Va5t4vpGQJg4M4U0Yu0ut
4bCZP8I6rlXGj+TqDKVUx9kfGpIKX6Kw2TvZUYbHIDWh3UhQO1hD4xy4b8rOak1w
@@ -1427,7 +1427,7 @@
pub A7764F502A938C99
sub F20DB7FEF61CE1E8
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFc7oMQBCADaIPEUzMrwF9gnEC+PRn2cSPG8OV4RxXxa88TZm0L7NF7D+F5N
MNUAZ58oVqFUW+ytgb5iey3X7KjlJXZnuqES4m2Id4N7FlnvrmpeOg7MUc9VmNkt
@@ -1455,7 +1455,7 @@
sub FB4C179C9305F3B1
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGJZox0BDAC/pjQlGW0w4nlUz/pJo69HlaFXNcTw8B6oGwIAhzer/iJIYaPM
OYM44uifatxD16n4eFk3ZLHkIYbU+2wfprLlfsMhBuh+esY5qIHqFlhos0yQATGE
@@ -1492,7 +1492,7 @@
sub 4044EDF1BB73EFEA
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFELz9cBCAC1cr1c5jWUreRdPYYvk6DK7DwF6dgt7iN4rN2QT75M6ob9Yxow
QeO709C7V0JXpVCOJ+7gCxnllmktpchRpj7hj3iDdvhVuKMEF4pl+tDyoyzK4Xvh
@@ -1527,7 +1527,7 @@
sub D36DB5C489BAAC5B
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGBoC2ABDACyCWLqqAo9NeThE90hBoYomtgLci5I8+7PxSYeQfzUYjXzZcnh
6d/zHaeC0zxGhT2LNe5i3p2e36xSeFDobjG2Il/nv+4jFCgbn3TZ2hEingPuPsg5
@@ -1565,7 +1565,7 @@
sub 1A94B14C6A03458D
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGI8r9sBEACZJBV2TNUSsLRo89uC4lfmQxfNDqkE0uZghfFY/p0fr6fkBybO
WDkPFskAPD32fzrWxZd2kkyCRyUrOmAUC22q8hw96t28+RqZymvetIa0f8GQGgkO
@@ -1610,7 +1610,7 @@
sub 3737A3AACF645E77
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGZPgeABEACy60J6lwPVmvj7LnAwkaggbBVA2kZ3P+YxWLV/hhdMQplYGJKR
mwd3bnBdR8duAwyEh+VlEsw7+FP14bCV6DihTOhzKwVliprV8Jkt6cog4ccFIjHI
@@ -1656,7 +1656,7 @@
sub 501B5ADEF57CE6A3
sub 5D9FFE7B8E3DEA8B
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBF0YzcYBCADmNIEEzvSsnJnxH0u89Hb5vCCkl+45dWHyCMsCLNty8yL214LV
B35gnU+6BvRXN3DmTpreCV8/wgI2h1eq83dTO2AsnJTxTjvYpiwAtWhONxWxCU1Y
@@ -1704,7 +1704,7 @@
sub 7B92B768F9D37337
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGHu5IUBEAC5appY0S1OLTgUnwbM49Y5Km/pL0SWE1nLwGPQKG/YBpcVaKhE
zn1w7/3gtqrfQr811OpMVjrV0LAKh+gPg25m4GIYpqtqgO1u3T7e5Za5dq8f0fAP
@@ -1749,7 +1749,7 @@
sub 3F7EB3ADB58CF1E0
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mDMEYlrX6hYJKwYBBAHaRw8BAQdAuMmfV7N5GrH0mrA6JbgOFXi5Qx4V+DN6DiEJ
yQWXOcu0JlBpZXJyZS1ZdmVzIFJpY2F1IDxweS5yaWNhdUBnbWFpbC5jb20+uDgE
@@ -1765,7 +1765,7 @@
sub 38185785755267BD
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQMuBFKTz1wRCADOdMCDOKXlBuQpG7mnQ/5rppqhS0SXdKvNZ5pYrJKib1LLtlS/
LOeABja3E1ky+znvTqnEEtai7fNhw36zPdUjhPKE0TZwn2aK5fyctkcfqBFsja3E
@@ -1806,7 +1806,7 @@
sub C707929E5065E0BC
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGJm9OEBEAClTz80QmRmi9bpX4m77aas5Q+x+gRtlEg6IWU6QfrGdazVO/3S
brF3KmsEnxW8fjqv5drswed8FmUVdEsTcco31jxeD+fiBFCAU8BnrpL/+iIALMRY
@@ -1851,7 +1851,7 @@
sub 7892707E9657EBD4
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFdbSfIBCACrFI0ai/abnV2U2Wa9QQZwGk3Fegc8laiuTKc0GoYdyptd83/H
hD5S61ppdkOugBjVTHdgda3xJ7zBZdnwjZvV/TyayQltbh6hU+BMlEolzXLgyvY7
@@ -1878,7 +1878,7 @@
pub B16698A4ADF4D638
sub 32784D4F004B405B
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFM1v9ABCADD0KoXq2ZKlUHeIVovQy3gFmW9oFAaraV48ouv8cYvqdf+s91H
NyqeyNPT/ihFeNqZJUAMyPdwN5xrWD6gxMrOCR7BFhA5kLmAKz4HfFCQ05ViyQdI
@@ -1904,7 +1904,7 @@
pub B341DDB020FCB6AB
sub 315693699F8D102F
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBEowbDsRBAD2jx/Q2jNuCkgiS3fzIj6EzDP+2kipIKH2LEnpnTiBlds2PFYM
xYibVab/grgQODxTdDnAKifbJA/4h1/T7ba+OV+xIUoSI5MbgaF3USidiDHPX0pY
@@ -1934,7 +1934,7 @@
pub B57BD58EF6D0A713
sub 781D1F35916E0113
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBFsZf3oBDADUgeJsq9asQLaUajkGON9KmxKBtJS+IbGa0jgvx37T4LDigKS/
wh4axvdJ0mE31uXKitBVDkr5TptyxA0jojYwlt5YLXsotnskdHrIg35Q8xpMp72K
@@ -1968,7 +1968,7 @@
pub B5A9E81B565E89E0
sub 28FA4026A9B24A91
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFIsmpIBEACzV3plLr6UEdvMiarCYzoK3W0Gzzd6BWtEuQdOsDkR/XCGOEkY
hNQ9sB7QdA3ysFdRGf9IFcd7E4Y9dQABFXDlLEDGewPdZ1ahMTz9kK5k6R/1mxeu
@@ -2010,7 +2010,7 @@
pub B7C3B43D18EAA8B7
sub 02A4A6FB70018AD9
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQQNBFT3aMQBIACl/07e2aAdqLGTocp3J694BSGxjH1M4T8BevXH0UTRTXbge0l2
3IONp63KF1tmHg0skzUu/1Ybau6Zw7k+jRFN+9VmslRprk4fjHjgxmT5U8p1ualk
@@ -2084,7 +2084,7 @@
pub BAC30622339994C4
sub FC9BDC25FB378008
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFlMExYBCACmdTDSXPwSJeYbfYvHoDl5C7vx/0+LOTunDGJN38pNQHYQAZnv
Gyoc9ZmChrhLoim7z4ILqmNo8eegknepQ3dGdUij4NVIhR+m+8irayTbsNHvo3UG
@@ -2110,7 +2110,7 @@
pub BB2914C1FA0811C3
sub 7AEAF265B448E2F3
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBFHwyNYRBACkCXpipiMx0lCEccXXzv0bE7LHHbcQYtb1vT/o9WXYoP8JMChJ
cvuAe8Tvg+s7EUjKHJRhu7I7kie+IJ2wtH5uVARkYxoP2OslYN6MSXa/bmwU8fwQ
@@ -2142,7 +2142,7 @@
sub C9F04E6E2DC4F7F8
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFKneXIBCACtnX3ZQmPujf6ocvdnhsBheze71DSl34TfebyW2Qt+g9NhMxo4
DaJy+iFNnsaMwLZRr6k/qf+ISE3A4opWAQlbk+Wb5s6DPPA2cHH6W4GdkxtuJzqt
@@ -2171,7 +2171,7 @@
sub E05A9780475FAB55
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFFGZXQBCADeZK9xuCrDwJ7v37y8RITlchzBfJEWv7cSbrSIBlFNAsUUoshW
Y8U6xYKe0GdiLVta2F8bzs0Si4LcDeglQNi9Fxvh3/jfs0MEJUfSeZ4z1Mn5WY35
@@ -2199,7 +2199,7 @@
pub BEABCFBEE059E4E5
sub 6579F3D193AD0019
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFOv78cBCACj4w72ksYDdLAY3GzwpRa1fo6S4aF7r96PitlETY83ct7AVF7j
XaBGk5ylNAZXan3vlsSAKtxlI7skZOE5iKjqDo7SUfohs1WXdmL765mUNsSmkbG+
@@ -2227,7 +2227,7 @@
sub 4BE257B370130000
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBFv1EEwBDAC61jyEM99KH18hI3zlfuqvGoNjTLIh0wge5vXAH8VxMR0ndOID
HYSBT2+L6OeiqKlyhCgF1km48F/dMzyJdTASkNO1Ni+B2Ric1sBxjsSPufkjl4en
@@ -2264,7 +2264,7 @@
sub C163B490C5CDC967
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQQNBFT3VuYBIADPQxdM6fJajMSyeiKbfpSjllBkGA16DE9IFJ76B6281k8sfya2
k6UOAKNIprxY3JCRulbnkn3BcdbY1vZDhaf/fbdkvJ+o/XVzrxojq1jS3tvSq95L
@@ -2340,7 +2340,7 @@
pub BF984B4145EA13F7
sub 84761D363E7B0FC4
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBF7rgogBCADU9OwoEFdIgN5U0JU5pI7s3T1T1LeDMzAQ8l2Hq4jFrhnrjcEA
ieDSut1YIv5NTBoZv4CrklaKvvQNUXPvKrFImA4OKhBodKV3wsq2efCATDGa1JAw
@@ -2366,7 +2366,7 @@
pub BFFC9B54721244AD
sub 788E173C196BC673
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBFRRGVMBDADAQcmG+x0mHZwJ3uKgODjUZXkGRkuz7aP/qRmuQVn93tl8DmA1
lgvXndvChUjzYt4DJnQhRsapAXEmP5/YYIkWOzuk9EpXGtqUieocylvNXP9eDF9y
@@ -2402,7 +2402,7 @@
sub 3F078B16810B4EA4
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGRmS+sBEADKHnDWmf5NP1/WPGmBLTEDv/mSGZx7jpfjbaEcCFH3hiGbspbK
3wgGE1OzFf6JRBurs8GS0gD4aXoQFz8saVASPHlKK/LYc7f6vYAAWj6Tlm1j2qwe
@@ -2448,7 +2448,7 @@
sub 606CC6C4533E81A2
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGAic/4BDACtIv4a32pL+84jJNhJ1yb6GFgoWknJSJ6IELIL0Z7m+FYsymRs
lTJ/QwBgjZlgS3HS7IBhEl5o+kEt2/U5lPkz/krP8By8EvRv18PpfBzmXNT8rGqc
@@ -2485,7 +2485,7 @@
sub 97149CA7141687A7
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFwVgzkBEADF3gGO9iBXW9g7+yRjwTKuadaSW/32gDyREjKNSa7NA0HSCtnU
dKapw6AaCFpznhfjPQL+bZX/YJUdrIXrSJ9iL//2Ay/JET7UhYBsHxaMm8VURpIK
@@ -2529,7 +2529,7 @@
pub C3BAB45F4AF71FAB
sub 34FEB51E33761BEA
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFkeN88BCAC4rvR3Dc6nDYhbXUC5IQ6SJWvV98+tvZ117J/VD07el7dicryY
H3OAWl62iLjHJFP/+AEra1plpiWbPioDlzjOWC2AJjUCtqVLHdyVbY0Gv3sSRZXJ
@@ -2555,7 +2555,7 @@
pub C4C8CB73B1435348
sub EA2A558279B36E6B
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFSwGboBEADoHgtdw+OVEAIF1SiRju8QDuhePZbpSgRLrt25AmowHJhOQUI1
EP7+RWoCWW9gWAGas5mGDBxhPw8NgFv1nMUWFAsj0rkViuRD4qpJbChvlqw7YkOq
@@ -2597,7 +2597,7 @@
pub C51E6CBC7FF46F0B
sub 4006CBA6D352F1FC
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFbgSbABCADGGENSy3oWLjW7zfYMSsR0pm3l3eMA7ptyU5C0U/MoIYjbXwyX
XtlGwKnNgngATh1SMpX4WDbD8tn6vdeP4uHQsDb40t0XN7/HISFcLhV5pCgz2wNR
@@ -2623,7 +2623,7 @@
pub C71FB765CD9DE313
sub DFF2D25E2CD6139E
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGOV0eQBEADTe/ljLAoBp+z84NkWHDBqbBmEsBxcGa0VDQxGsaMMi2f6wkO2
VDkRFNzNQbmw5xFqLisZ9ywzuVc9xmZ6qoMWLJaYs9RdsJSgD9+4hL5IkmjClxc9
@@ -2667,7 +2667,7 @@
sub 29E792953D515FC5
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBF8pVB0BDADcwRGpJUDe8eVSlJ0yPQl/CyeYc0RWq2f1seUMQO0xFW1xPIeL
IE68D9VdgarA88qDLYesfBqzn57/r/ztj2aLEKt8IRunJzd0w0G2rrgSCZQ8RmzL
@@ -2705,7 +2705,7 @@
sub 96123FA2B8E17FF9
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGU//oQBDADDJ42aKLuJyYL2ZfG0ob/XxdYRZhP7/OO64Jf4WJtX7vgoVUif
iSytAikRT707EshXRMtw8tx3H/jM2O7o/gJl592IQ2gYppMh4boNO7lc7dd9F6gv
@@ -2742,7 +2742,7 @@
sub 7679164AA2590985
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBErg1IARBACVbmwMwp4p0ldolUYSkGl7XFJHwtEWmuikGcM4lp72h/YhAXpf
RVsKE3aCy6HSTt7KJrcUuOL8BB67riZXLOIZtA9kDyC+0EUbnW2EbVfJXskPLP5X
@@ -2787,7 +2787,7 @@
sub 64863FF4D1BF1809
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBEdUhrIRBADCU9cuKc92CWQlZxwtRuSIV/36Qmj264YD+Lix+r1Qe1PqRr1I
/MObOo83ulorWigSkx1k81Mnr56NwmIeo2bMhjmgRgf7EG6XEbKdRKfJcJRR1lDV
@@ -2820,7 +2820,7 @@
sub AFF3E378166B1F0F
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFeWvEwBCAC7oSQ7XqcGDc6YL4KAGvDVZYigcJmv0y5hWT4wv9ABP4Jhzr1H
NDmmGyWzhzTeMxwuZnc9vhxCQRwyxj3gGI5lYPEARswbi2fWk//78/3Wk+YMHJw3
@@ -2849,7 +2849,7 @@
sub B2D8461AB7A7DF27
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mDMEYo/OhhYJKwYBBAHaRw8BAQdAStj5losyChV0W0clNh6HwDgaGgmypqqVICtN
K+Vy0oy0JlBpZXJyZSBZdmVzIFJpY2F1IDxweS5yaWNhdUBnbWFpbC5jb20+uDgE
@@ -2863,7 +2863,7 @@
pub CB43338E060CF9FA
sub C59D5D06CF8D0E01
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBE0NT+kBEAD1hzO+dXStXYJj8M6FBn9fxw+grddjM9rqaEgJ2omSdpZZOPBs
DRor7v0Rm23Ec17y/7Dd6oR1CvyAeQwhJvNBaAW4LQmUcvvqep4hfkWDhlRvh/QS
@@ -2907,7 +2907,7 @@
sub BE04F93C75A3B493
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFRIQyEBCADYOc8Y4bOkLGh5NFwQ1JJwGzPY/mV9kndWy2tudEs89Poo4cQD
A/wndJqO2PrdvDvt+kxKQGra0RzUNW3Te5gaePo7+3H297BAWar8+KiX8RRu3uB1
@@ -2934,7 +2934,7 @@
pub CE8B1D1D2530EDC5
sub 7ECBD740FF06AEB5
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFuX5CkBEADkTgn4nzuq0lWR+7kFGYLKvmPLjes4j2nmygIafUjVbNmD70gY
DPpbSP02HxgicM6xSSqzZuBVxpbcffqjMPXf8LkVX4iWKZtyzLpf34yaojigU3qF
@@ -2986,7 +2986,7 @@
pub CF9F3090CE4CB752
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBE7E4m4BCADCkqre+MJRRn+yBa8PqDHFIpfxOk8lQeueZTrU0Hw14wMkkOW6
XFBb4hDeezStNNP6s2TS7bf5YRXZwqOwwgg33WYVVH4jPldaP1m+Z3GtYSLKEjTl
@@ -3002,7 +3002,7 @@
sub 57CE36BB68F1BC57
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mDMEYYx3eRYJKwYBBAHaRw8BAQdAV7zh1T+xL7mD2O63rTIvRfQ9kwL2Gvq/Q6PD
9apCe2K0LkpldEJyYWlucyBDb21wb3NlIFRlYW0gPGNvbXBvc2VAamV0YnJhaW5z
@@ -3018,7 +3018,7 @@
sub 5199F3DAE89C332D
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGCtdhoBDADdopjDt4eUNEqLJSw1ZICSR0oq09SOVtJSaSYdF8UiXjBfL1Ds
fhTDqSv5pT2a2gLj0OU3tFhWHvINLaKKCjQnHVcFXi2LTxt+XBOjRYkFjHVisbaZ
@@ -3053,7 +3053,7 @@
pub D364ABAA39A47320
sub 3F606403DCA455C8
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGH0NlsBEACnLJ3vl/aV+4ytkJ6QSfDFHrwzSo1eEXyuFZ85mLijvgGuaKRr
c9/lKed0MuyhLJ7YD752kcFCEIyPbjeqEFsBcgU/RWa1AEfaay4eMLBzLSOwCvhD
@@ -3097,7 +3097,7 @@
sub D826E3935EE9DC71
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGETEF0BEADoVhSwI5d3PZTca1W/1HvIf5UiTJrSlZby9xRdSbfJ0dj7V0QG
aY1tsOcLLuIkj+/wDJuATokYx6IiGnntorQcLg3b0XMoPqzTVDl4lnKcNIsh/kxD
@@ -3140,7 +3140,7 @@
pub D57506CD188FD842
sub 63F72A7A8658D653
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFj2NXwBCADPJcGfWz4Zsfa/UEUF6a4aAIjqCy+rNmLf9Vs3HD6B5p1r7VkC
e0HhxrfbkDkQu6aEmAwV6GwYiwWBf/LQYNdKm1FYZFhKLhyuTPiirFqIouEFqiK2
@@ -3174,7 +3174,7 @@
sub 9D49CFE20A7A3EE7
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBF7rvIMBEACkH8bOlnIXAH9nQYFcihkcJvv73pw66YMz4aMPJe5PzaJU6kkV
2lbEgEOnfoFLqgnJVY/KsPf00BXaP5uMzqNfJTK+HO9I7m3BTqmjLBgUegQig4K/
@@ -3219,7 +3219,7 @@
sub A23FC45C6F9E2F57
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBF0uFrIBDADbJkwrWs0qPrv4bNmPZMWHcryANAwodvFF4f51Z6S3pBkuBxx0
vW8ZKC9/scJiAzSqJRf4im70GPNE3MZjNyfuRdaedXw2rFc4Ip7lBsCtklYmTWmC
@@ -3256,7 +3256,7 @@
sub B4C70893B62BABE8
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFMvQKsBEAC3/wuVMv4ia132SA1Y/KnuZYkSNDaRH/Ie1WTAX9X0KrWA5fx2
WmzKfaLNyBHU5aI0BjoE9DW3zkZcLEcL/cxRzoXoavUGRhRsaHbj4PhQkEqV35L1
@@ -3357,7 +3357,7 @@
pub D945E643368FEF62
sub A8D88140C35897AD
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBFzJyTIBDADO8siKg1NQb8jNPo2DPC5CpPwYDPUjlX7Nq/FMBYeY51JlxKLD
jmH/R5u6LuY0v7gSodrJqE0FUjz8LgN9+Yp1f1szqxeYHLsAVahO4cafG/sITYvr
@@ -3393,7 +3393,7 @@
sub 9121AD263441EEDD
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBFrjUQUBDADTMQL/4d9EyVhsO4XBH9wbGWxcEJvsu/HvppN5fY8hpMV0+Cr9
wjAeJ7d9zdFJVB8vPLN7bb5dm6SNyK3KiOugqVgZrQ+ZPTvCCgFbFyEXuZwDiOa1
@@ -3431,7 +3431,7 @@
sub 66A2CBDE49E8A25D
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBGAwdRsBCADCXfWdHhywp8Rcgt834W/Z3MFEAxYdxjAJOTQhc/In1SJfIqi/
xD7OKHA2fbwzRnS/UmXkmElTK7JI3/1gWRm8kEaaHTnlI63Z9MZV0DHMpJMgvpFM
@@ -3460,7 +3460,7 @@
sub 50C6CC55C6F24FB1
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBF8tnmMBCADROe7j1ZvgiMgfsQKqCSuSqgMkfMT2DEXwZKdHqkj0gfx8MPQg
OP1pmMgpIwIXKr5kZ9KMGiGULNnS+WU2SNqjyKeq3MlnSYW5Di52MoAD7W4cHmry
@@ -3489,7 +3489,7 @@
sub A947A3FCB1697B4F
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBF7H/6gBCACbEuIbxWAfHEYViPqdpwxDYauxsYwk6FgA9sSO1nS95KRwx+Cs
X6F8nRGnfLtbo6Ffcp6r58fNi9RvY7ueRGiL0kQd6c5GYx6dH1b91Q1qrdVOeEdj
@@ -3518,7 +3518,7 @@
sub E8D0C72FC5A02B28
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGAlt80BEACftpFzUCGm2u5sV4UgAysobdqZywkUKP147toek4ULQRYpADig
AI9J3BCmHbcApLek1U7vj8geB6T7V0c4ELLFPQ+4lQlCPC8Siv5c2gDaZvoMzTlw
@@ -3561,7 +3561,7 @@
pub DEE12B9896F97E34
sub 9A716F957BC42546
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFAxQKwBCADJGPv6pmFEq0SDwAKESEgCdnXycbR0bNXpNa/3VGboNto1xKgd
AQ/sI5x+CmN0hpUjklEwff6QIt3MlofEMkAzSfRmTobhJTK9W7r4+p5DuhJpi5Wz
@@ -3593,7 +3593,7 @@
pub E0130A3ED5A2079E
sub 0AE7BBD7FEE66E0C
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFlMSXMBCADcgN0/57D/gU5cDobPiRuDT6qAxb/NWhQiqwAocKd274r4gPJm
RbffUEZEgKhjH6l0CQfilC4R4x2QtU9sNC9kB/D6zumoS1uI0Hmx1pC4UseUy55r
@@ -3621,7 +3621,7 @@
sub F3DBCE882C3A01AA
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFsNoY0BCADIvRrJEX3k7UeuT6zt+F4++xH+5Qo7QzdicjFhhyb22PLPyIsI
Ema+T4QqiPDegUv8yKKTTBmHNw/vSUHTPX9ZUpglckopuOgdfnuQjTKEOEzrN7V/
@@ -3650,7 +3650,7 @@
sub 5A34A5E06B936F93
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFF/4bYBCADTeOLZiVGNbjlPrwG7UcMl+yXmEqpf9dB1A9cuicH3PWXj0WOb
LSzHjzoRvRekEqSUmgoveey1lPuA2qjOUkXY6Kiyx+oLiG0/ObJHUQW2O+tjSQ0R
@@ -3680,7 +3680,7 @@
sub 60EB70DDAAC2EC21
sub 3D5839A2262CBBFB
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBF/RX/MBDADSqelDQKobURExWUKALq86yTPMxMasxmDlccKFpk5xjWrryL7z
qg4Fnb7IK5fKDtcnTANtOv2hlIli1h131+SmjJdD3qhfly7QoszOpr5izDS+FOCj
@@ -3761,7 +3761,7 @@
sub 4697DFC8F2696A57
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBEzdTPIBEADki1HMFzssqhU2l3jJr0zNE/gyPohjzI5ugw1dNWUd/ht6oUnm
2StYcsRnFHlY7aIp56v6cZtAKYDZTlEArIurH5xyQXQ3PLfxQZPVS6HDUghaa0rJ
@@ -3806,7 +3806,7 @@
sub 52410ED7B05AD2E9
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGKRNiwBDAC56nNMaU1QEHCpOnvOHK1rjDKGDolxSyx9rgoTTWpaI9y7JbUT
iajEkzrtTsqjrabCltAY6QGQUz/TdS9MikCPUZM+l9EYKoBACDeKrYMcApHj4eVw
@@ -3842,7 +3842,7 @@
uid Rolf Lear (JDOM) (Used to sign JDOM Packages) <[email protected]>
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFCPD00BCAC4tY8wMQTsCKyII/mMkUDAkXA2cLM47fY1Wn+iohtgtalUdA0v
AhGvTdFU6/St35rOKNoyLC7Sy30FBYpAEfMB/x9j/CaQtdtGhaQU0hCvtWGhhS3J
@@ -3859,7 +3859,7 @@
sub BB09D73166EEF1AD
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFEqVnEBEADZhnnAV62dwYvq5CxvEO9N7m7vrYMosc8PCEafxJqrDMbWWfv2
tD3EaHAERt/UFVEo2U5FV1hELUvFISPhh/DpOWYuc7pwA75do7ul6dhwgi5FcyjR
@@ -3913,7 +3913,7 @@
pub ECDFEA3CB4493B94
sub 3BD211F725778C36
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBERFeVERBACjfASThn15ynIICr0Gu8quGCl2rSSRar8TsjrbiwYB2MTW35Rg
NjLU6MN5Nq4d5G8D5aMeoyGODstIHH8zA52sDGeHOMKfDaAraL+lGzElbpmaqP2s
@@ -3943,7 +3943,7 @@
pub EE92349AD86DE446
sub E68665C8F91BDE69
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBGO91akBCADDDpIrW/IohUSJNDu9VOUlnfEOm5VS49uqM0uucLi0BeAhy1Fo
P6Yg1cJkcK66DtnUoTM/JJLyDzJRlKnniLrYCkw8ScvtPdA5cQKJTY5ecn+9ouR2
@@ -3969,7 +3969,7 @@
pub EE9E7DC9D92FC896
sub 3B7272A25F20140F
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBE/oyDcBCACgYsHtmWmtUzqyr/JN+orfJaTl2363qiS+NJ1lt2CNxUWOqldc
VcIGyjmzokxTRpGdCFmT1Lh/hzZhcDPLjrtxf+f6njIibt80OiEbX39gjwZRIikd
@@ -3997,7 +3997,7 @@
sub AE7B5A78012824FE
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGFUnmoBDADItKvcmnwP6xsF7EnS+gKxUBU+M+x1sdzLJGyOL4laakwgUx3m
RhKwDfT6tIQjTAVpHpORa2LNYikoYYodIHshTuwN9Gba/pybeRdazWguOv4pizTx
@@ -4035,7 +4035,7 @@
sub EF375EEBBDEFD775
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFS2J+0BCADZI8RYk32YeO9gnEkY9RN+4dKb+H1AR4v+IGxmy0UYy+O8bo4m
YzkQHTlPpEPGe10/quKk1embDifEfNa9mwcSJl+XUPFlTrSA97SR31mdyK/Ua146
@@ -4064,7 +4064,7 @@
sub 1C9F436B883DCCF6
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGAhOxEBEADdB5Jy2sSOndOMCTyk8IFIJYPogjXtN7CnyIlqr4jEB5G87TJf
m7OxB95aIVS1vSA5ghCm88N1mKtW6jyYjgLFQbbyD9/X3ShVZjh8B2R4atL93SSK
@@ -4108,7 +4108,7 @@
pub F406F31BC1468EBA
sub 4BB1ED965FF68B71
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFmnALcBCAD1KazT9eswNXzML5+M72qhdIX4VlJrrOzeiQtTW9vbXj7DZUnw
U8m2bNmKHtpnyXQ3Vl7FE/e8CKGUVKmB854VJGDSyjToeAnt8A0Lg4smaSfgbEim
@@ -4136,7 +4136,7 @@
sub 6064B04A9DC688E0
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBEtsF2oRBACcai1CJgjBfgteTh61OuTg4dxFwvLSxXy8uM1ouJw5sMx+OKR9
Uq6pAZ1+NAUckUrha9J6qhQ+WQtaO5PI1Cz2f9rY+FBRx3O+jeTaCgGxM8mGUM5e
@@ -4169,7 +4169,7 @@
pub F6D4A1D411E9D1AE
sub B5CB27F94F97173B
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBE89LqsBCAC/C7QToaRF8eZgGOxcvp9aG+mFFCMjaRAb4Mh59OYdmUb6ZjfO
9388HPebGbPNR8SHYs0dBIuWY4ZJ7oUTYPswasL8vB0iPFdyHhvkCca+yk0b8ZBM
@@ -4195,7 +4195,7 @@
pub F800DD0933ECF7F7
sub 592C39141EB02A78
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQMuBEvQhhQRCADQ2MH2FpQD7pbCTDJ4uvPSeaOz0IUhkX9bK4sKvIISx8MbHhR4
k4sXi+vVkLngWCMUV4nB4WcCibk2S184SzL0TstTDrudxe4eJFVbmZw0GrgASugQ
@@ -4236,7 +4236,7 @@
sub 012F07EDD27CB2E2
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mDMEZI8otBYJKwYBBAHaRw8BAQdAL5VNS8O2NJbsTZaMphHGdxsSaLUj0tZLI6+R
/pve51q0HlNlYW4gTGVhcnkgPHN0bGVhcnlAZ21haWwuY29tPrg4BGSPKLQSCisG
@@ -4252,7 +4252,7 @@
sub 5F68B9B2F1725F16
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFFCLwoBCADxtcGi0nfolr1kGWe3jQ7n18roJFwBs4Q52nx0h4+a8ZGr7/1E
1brakrz3t/cTSZIrhfru8kirP8cJtXBxpd/nCeRrB/4ZtXPUJiGwKx6sVGr0ix6U
@@ -4279,7 +4279,7 @@
pub 012579464D01C06A
sub CB6D56B72FDDF8AA
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFgnlA8BCACVtx3oLXcanfvwtMRwal6pLQ8IVMG9+fr4xGdbSHXCRNbosDa5
agU7WeQMPhusSxJGaA3w7NOdjAwD/LeHADhDPeI6llJg1Fb3EyqH0NZaODKU/Or/
@@ -4307,7 +4307,7 @@
sub C753427AB202DB9B
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFBdqooBEADuV8IhDi4Xvs1oYAnTXQz9MW+bU5uaxQyQcFzUwxacSdgAv+pj
dZRFli8qs31HsddRmW6qCkCua/QXNQWCOcylcwAKmumct1Z/ZumYTRVGbsagneBa
@@ -4350,7 +4350,7 @@
pub 02216ED811210DAA
sub 8C40458A5F28CF7B
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGADx6IBDADoHin1LGQ8dhnlhfNCBZ3IyXS2NpR1VjmYtHSlh1hGsPcmHuwo
1mLA6JzXF7NuK3Y52pbTr6vz9bAap8Ysjq/3UJeiDbf7FvmO5xAEVUhrpc7AEY7G
@@ -4386,7 +4386,7 @@
sub 7CD1B9BD808646B7
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFqzjCgBEADfFggdskGls5KqMnhvePTtS4Bn/2t9Rl+Wg3ylXgy4IFd4bnI2
9f82dVM/nobNqAnhOp0wEaAcw+57xBx3rjjKQbrMzUweWeL3uJdTwtPWoyzzsUP0
@@ -4431,7 +4431,7 @@
sub 0181B45EA58677BC
sub 944EC8D1A08CF77A
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mDMEYiljShYJKwYBBAHaRw8BAQdA4ativA3OtR15B4YnoRwpm9rRgHdd0A0lzJ4u
6q7gsMO4MwRiKWQYFgkrBgEEAdpHDwEBB0A8fHRuwUbuvdnDexkJzQZVUg+nFrcA
@@ -4456,7 +4456,7 @@
sub F2E4DE8FA750E060
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBEmoKU8RBADEN0Q6AuEWEeddjARAzNXcjEx1WfTbLxW5abiiy7zLEht63mhF
kBlbyxEIRnHCSrPLUqY5ROWdyey8MJw+bsQn005RZmSvq2rniXz3MpcyAcYPVPWx
@@ -4489,7 +4489,7 @@
sub 9757C89E39C828B7
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mDMEZUJpBRYJKwYBBAHaRw8BAQdAMUi1X0odyTiUuXIgDEYZZ4Pf4FQifp2UgYln
s/XkBjO0KFViZXIgT3BlbiBTb3VyY2UgUHJvZ3JhbSA8b3Nwb0B1YmVyLmNvbT64
@@ -4503,7 +4503,7 @@
pub 049FE94F2D5DAD9D
sub 953E02E4F573B46F
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFxlMc4BEADbWFmOqHBqUUAcO9nPRSqtrmIdjBCzqsRosPk80n3Nd+jWc44T
/O5TObVbn4NxCmbLxklWpIU7eTEo3u5LnwhkgcsxMykWYdq6DqyzENO9PeE/McrN
@@ -4559,7 +4559,7 @@
sub DECB4AA7ECD68C0E
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBEoo3BYRBACXE2oGRA58Ml6s+kvfk6n/AJ+5OFeRT/Xelco/cpdxOVF5LkRk
yd+vR2+F9ldBlH7CSTCmrdZIN3M3zrcWndrk/OQkCxNWVnE/a1li7L3G9nYr011k
@@ -4586,7 +4586,7 @@
sub 6A0975F8B1127B83
sub 3FF44D37464BBB7E
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBFzy4ngBDAC4mz6ELMWjfJ8GZtolq3E96T7qjfp4J9FxGVxdbJxkEDnn6MTg
V8zhD7yeSZcUSvwzPiDlB/b4RYnh+5LjzKHTsrtr9ja0SupuCkVGkMGWeHhpIGV9
@@ -4667,7 +4667,7 @@
sub 11F4CE313A637CC1
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBF3HgdMBCAC3ET5ipFXdZ9GGMbtsCQ3HGT40saajsNDOdov2nMJxzKkVe3wk
sN3bpgbsqBU9ykVkIhX8zV5+v8DOBzkV0pJ2eLjFa9jBPvNjV+KoK2BAI5pzNzYg
@@ -4696,7 +4696,7 @@
sub 8118B3BCDB1A5000
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFu1EwUBEADAXapH49L1Lwt28iK737X/+4bRDE+lkMxehnUZ7QJs5zkFz5Sh
9K2rQO0PpvoMSdadGplFyhKdDP/iEUpzxTTbqMs5UjbJr0MoFfE957Vz59mNf9WY
@@ -4753,7 +4753,7 @@
sub 5FB8CE6F93DDEB23
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGAjI8sBEACtiX+/sDZDNo9M42xWgMDEUUBGkObE0opLPe9N15OHQt8Ve2yJ
VW5yW0X0hcBIkaxAG7F/NpjVRN1bLGM3J3URR+XD+Ubq2KJkKW/39RHcP0m60tL2
@@ -4798,7 +4798,7 @@
sub A9E4161147556D82
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBF+EGtgBCAC/KXNQAl1rz3VBbqm6ssjzR+5Su1QWHI7oYDS+YHCLOaqfE3jO
zQd+8iNgniVNtX2n7bt1hido5B94VmaqD+zjjSu2UV/eZoYhCOQ5NgvxIr7WZe9t
@@ -4825,7 +4825,7 @@
pub 0D3B328562A119A7
sub C45D01093DCFC371
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBE4rG7gBEADo5n849j3hlKrvFzt6y65grIxTlbLDXEB7+6sw0Xwuh4NrK/Zg
0+eF0vvCCZrl3lHE2duD2ng9ZXz8EvUSNfwKMQz+cwF0klhP92u6mykKJ3/DZ4yo
@@ -4902,7 +4902,7 @@
pub 0DA8A5EC02D11EAD
sub 71499A87DC1FF84B
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBE3LMfMRBAD90h69D8yyPWaSoAyh2mOOOZ/XH0isuBpDZCWptemlMHgImqdQ
2sXLXYT1bJKmSaMw+yKjp8J/NYk69EbmSK1C2nypLQtWhUmXXd3XVYw6hrG/dGvi
@@ -4932,7 +4932,7 @@
pub 0E91C2DE43B72BB1
sub 83552A552A0D431C
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFBIm/wBCACgqvegptBhfKbyBXZiW+7XchIJCOpwq0/9QgSehKMwELbUKqNM
sIVrywANqYn32S9hNRvBiKGm/KY7VwN9p1Cr6Ey3XuGSbRo/xN6tqfV/rV5YClL5
@@ -4958,7 +4958,7 @@
pub 0F9FE62F88E938D8
sub BF6D15D3F1BF7BCF
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGGNmd8BDADSpbdIfqzkUNAeYlP0nUw/HFU/v+/aydtjUioAi/KxYt2FOMi6
gk1LOJzHBubv8bF79mlN6sXrnq2lV/MuqvN9DrTAQ4u4Dh0pgbLK6jbxDWPGrYIo
@@ -4992,7 +4992,7 @@
pub 10066A9707090CF9
sub 2B9F5DBAEAB53FE8
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFGKp5ABCADTyMhDq+7Kcv2wXOpOmZgp++JNO1erNUjVqFX7n9bT77DciEML
LNxWVF1tkNqgkn0ughZTXK5EGdjUfZaJaDDfG4BIsox/ng4nDvIp4CtXqHbWqrlc
@@ -5024,7 +5024,7 @@
pub 102E05D8DA6C286D
sub 7680B2343D1CF013
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFOZyw0BCADj6eDnIjaug0RJQCi/HLw5jJ2kORPaegxFuE5IhpN9pZCPASax
aTROfUSnys7cbxZxh3Sri3spQ0j+ejod0MhVX9ajTg508YAJUaCBbM7CGZJZtVFL
@@ -5051,7 +5051,7 @@
uid Thai Duong <[email protected]>
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBFrY3D4BDADSiDX16IC+236IeUiqi7Nbt2wlsBS0zqqaXi43QwXwcf7aYn4+
qrn+4JvsyMrDgkRgOElz134B1i5OSzP/32w2JCnj90XUjO5N1KD0QqoSops7NLhZ
@@ -5070,7 +5070,7 @@
sub DEDF3A7EB400D53A
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFnu01oBEADvITy7wT3dfEh6GKbW58giiB+JM3ikYNsK6LWaOa9Pi4/ZPpBT
ZxNfY90xp7U8lklmiOZ80XzXfKdnQySdW0GlGkRnzL8c3FayN97TlmMeRouRo64q
@@ -5115,7 +5115,7 @@
sub 0888B86856F9D71A
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBF2hcBkBCAC2H5WcFoeByKBUAjRDjmP+5P6FRsZjLe6c1wy7G1ha6/EQUVK4
gZUZYE9W7l/4QKHvAu4PvFWdD+5FXGZuB/2kw348CtabAlJTehm1QlPt5//ODkxB
@@ -5143,7 +5143,7 @@
sub F2EA967B5B8FD0FC
sub F860F86A8AA8521B
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFolWewBCACurWoOCed1W8Ut0tmqkSTpaz1AvPrYvZxmNqSVbxGjd8S/Bpxm
uypKQ/KIF88a8QbePYa6e/I9g8HiuA2Bg91T9khc1mztXvutkiFNaldecg2rFHZK
@@ -5201,7 +5201,7 @@
sub 8B794AD8CE1926C6
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBF8LXXYBCACuy3HnrpWl7boi98G4wG1ZrhBiYImyfQd1M+dvH3GF3Vqt2NYv
Nv8vryhUkMi8uu233KrYx2/kVK0RomMYWtUrSbQIdykytd0/VsoEk82ysN21ld9P
@@ -5230,7 +5230,7 @@
sub 0190A8A50D88C2C9
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBF1wCjUBDADAQDQrGd1ul3QLVj5zbl72zNWVNsRVF98JLSXYMmxsY/A0YNzT
B8LR58QCNF/xcjDyFt6+9jDEVjkKnJTHduzxddF/cQ9pw+0BOOwyfIkC2ryHzGUH
@@ -5265,7 +5265,7 @@
pub 15C71C0A4E0B8EDD
sub 891E4C2D471515FE
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFcyNOoBEACj0zTN3GkRNAY3jihHZdGvi70i4R8mUfcQUwWGRsGGlzSwyJfe
20qNOHqwHaxVCAIp4e5paNf9cEKepOv5IqMkmaRdiC2W+BHDxcJgBot/IrC81ube
@@ -5314,7 +5314,7 @@
sub D101F7899D41F3C3
sub E074D16EB6FF4DE3
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFf0j5oBEADS6cItqCbf4lOLICohq2aHqM5I1jsz3DC4ddIU5ONbKXP1t0wk
FEUPRzd6m80cTo7Q02Bw7enh4J6HvM5XVBSSGKENP6XAsiOZnY9nkXlcQAPFRnCn
@@ -5865,7 +5865,7 @@
sub EFE8086F9E93774E
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFPU8TIBCADGNvExYTJpVuNGCF9NuWw+IkitjAD7WzF7QkvFCSw9VftzgTUZ
3PYrThRiaDdmHQAke4Sp+nYyAJ7iUcQqg/5/ONiMdzXEv5Kwy5WJN8+o2aXSunIT
@@ -5900,7 +5900,7 @@
sub 5F6BA89D4B0869B9
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBF3TQCcBDAD177B+Btl8XBEkBQ5jFSezFrpEl4arwCEa7htCp6T3h55HvYwz
P7Y9zWYXfhAC8XJlPQJYpqaQiiYtdlmOrOS4wbp5Lr+z/0XpFlJFzdKglxKYcdfP
@@ -5935,7 +5935,7 @@
pub 1861C322C56014B2
sub 9A347756830C4541
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBEGVK0ERBADwhGhmOMvSgvGaqHW3ial0NS80ZXyE1EeNL6ke/WrXHB4dT6if
inoAuUgRz3v9Na4rjSQ8YVFjn3NaZq1i8RM2KJOUU8ZkJ2AsrH6fqStjofLTd5ng
@@ -5967,7 +5967,7 @@
sub D068F0D7B6A63980
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFHNxM8BCADYmt+HKkEwu89KQbwV7XIbgwZSfWc7y1HvA2YJpJRXJQsU/Pzv
BhsHnm9ZIScBLIlgE5OUnMNz8ktPDdsFg3j/L0HREXOAqkOFxWx2kANsRo2HmkM3
@@ -5996,7 +5996,7 @@
sub A3F393B5D034A0A3
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBEzxj6sBCADGV4szLvjBwrAOKYWw3efASDI2yo5Aq4oevm9cUB4G9G/D/fuR
XhodLaG2smZLd8sNafWTSbPHswsZtMAjHGzka9Uj4Ow0etl3+kTh0DE6Loezkj7s
@@ -6079,7 +6079,7 @@
pub 1B2718089CE964B8
sub A182F48D9C2C0825
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQMuBE4CPoURCACWBMGV/j1pioJPWkD9K9NdeRvld8sBorFBZo99DF3mcJvrXo/t
We7gmvcx2n/8P5lL27sYPuj6WSRgtVBlSMXllJm3NL3Hu/7XRILfJEKVeLLTdxc/
@@ -6123,7 +6123,7 @@
sub 7999BEFBA1039E8B
sub A7E989B0634097AC
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBF3Ep5QBEADZfs6o1IpZbZ1qlBkoJ7oWL0vFCcdPUgF/PRFXWKlsuFHVVV/N
oZF9SDiCJxfvsVXmI+IHTVMR2SszU2xDF2SlScRfZQwrLhBsDP9nv9N1eGIoA5Ny
@@ -6202,7 +6202,7 @@
sub C2148900BCD3C2AF
sub CFF46EE3C17E53E9
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGBP58sBDADYRZmxLOkqrz0QZ/yESRpv7IeHGLqDE1a8QfFtFb14MJCLSAAS
3nMD6Szi9mEjEqYdJURRcMjbUBhePgbhzGa3FYkjAB8lj6IKbu+ogCwVm1S8+caZ
@@ -6283,7 +6283,7 @@
sub B7D9C5C3EEC4A9A9
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFkyw7oBCACtGFos6g11ycruiWMuXwrE4+XbU85+1jR99AN5PcKjgXo/J3T9
XaZLjJ+oTWCVgEHu5PTxAftbkq9+lmDAUEWZ1Q8dKrnVgBLsFNn+G2pcvVschorz
@@ -6312,7 +6312,7 @@
sub 8DC6F3D0ABDBD017
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFAJOeEBEACn8aGYTnhyLS9SNi+SAdRU+pMPiqxdpxDMZczVee50y3LiRnCX
biWqZyhzuHZTccgV9IMYFwxD490BioH8M80escHrMh2C50FCFglVYsZQG93jYJJR
@@ -6368,7 +6368,7 @@
sub AD9CEBA0521B1945
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBEry8yoBEADnhvT3m/zzzuiUKyAeIfnN9CeN0ilQx4P0kFMhyZchRR4Ekb41
iKw7tDL9q+g7xSo3yUT9dKjDWJ3yhDpdAhp6d4y8GAuWqlOu8CQdEHJOKK0yxTzX
@@ -6413,7 +6413,7 @@
sub B4A1D8D630480593
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFI6WiwBEAD+kkswnsY8eaqvYkS+ZB5MJr7juWrv9Lw9OGsIXFlTvD1XK01c
E8k4+uA2sOtaXQ5wTMdc5N3YzAXqFxplWuafQgEvhyTTq37M5YCxvtYEZy/EHQYT
@@ -6456,7 +6456,7 @@
pub 218FA0F6A941A037
sub 9FF24F51B06DCC19
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFjDR2UBCADDfzBacBg664xalF55ghVkhxxwLaTNUdvTJ9o0IH+cGTRj7EYr
7QvLVa68PKigj0q7SVVwhPT7fzBLDosGjHef0UWap10ynqACBoAYSFocT9m3Www1
@@ -6484,7 +6484,7 @@
sub A98BD25BE464EA45
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFxmwqABEADNTTxqFiBcLLQwARbc0bmPUlxFl0A0Di9dTycUEjn0wTGS2xgF
dFxWomZd8R4b/lVb9jHf0r+AEul7U7sBoKinjwk0EuPDAZK5PEy3P8ILcAulwQqW
@@ -6529,7 +6529,7 @@
sub D658968EFD5E9F85
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQQNBFMPOkYBIACdXZi+34dvl+8q0IGIjLzFP7JvUH8ail4vrf2zwliW/QZskB/7
pFXCpV2/hX+0n+kJz0eqenl1l/+lT6p0MQ1TMCtiMccnX7WseQM+xSv4ug82nAwa
@@ -6607,7 +6607,7 @@
sub BFE9E301CD277BAF
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFpqN94BCACaAb8Afmng1QPu5k5uzLoA1FJnF6Wf31ZU1FzDxHFHLNUYSWN2
Bg6k95QH5ruZ+Z/QOJSoIB+b3htDklyxd8m+G2KsMIqnQs0BaTN18hb3PFyMIknM
@@ -6636,7 +6636,7 @@
sub EDB3D937B0C94C3E
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBDwczzwRBADyR8BVt1SUMHxjSG1AAekABO0YQHJG/XwEHYk7zPH3aU14/ocf
g6M8gxZXumM2f3oCCkmOpnW6uKxqTclQX44GyaMDETcAU5/bjWenWNj4INDlTjFS
@@ -6668,7 +6668,7 @@
sub B4E75C15C3C701AE
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFPsGJIBCADOxQoTLxpZVRIbLaRfsHa2y/TEIGvxLP7TgqTwspZYnwBd0cOW
OHAvF8yGfdk5gvkGTlQ/xchwu2Ix05FO2c+fBoOgIG1Gn2Q+PwheZklS7S+V+GFk
@@ -6694,7 +6694,7 @@
pub 280D66A55F5316C5
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFOOGVgBCACiDwUZOc6943aBGUrxikkfUnsyZfHtF9jihYmA1pSgfsye+JxR
oG9QWW9+3qx4L/d4ZEqBftTWpsjyrY7NyMaeXtJEjE0vhiWNehgXB1z4XTJ66zCX
@@ -6708,7 +6708,7 @@
pub 29579F18FA8FD93B
sub 9DF7F2349731D55B
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFYFiMABCADYpblWssqGxbjTwsyroPh48BwdSKl59zbFKoEHDw87NeWq7fik
h95RkbdeWsQSvduXWgQZsUDq9cLOkuS/ChAMkAAd3MPp1NMdFmAqS7BX5wU5s5I7
@@ -6736,7 +6736,7 @@
sub D95ECEC170500D9F
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBFsHC7gBDADlkoJglNVbX9MShcAm6jvS5atCZwWT63gSasObXFxswsJQd1NK
qryHNcj9tKBfLbSpMOoHeyyIKDdwdxN+6+N9Hi4hf0j1Ub6deJyI8ace8VERWaxF
@@ -6773,7 +6773,7 @@
sub 74C249541619FF0B
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQMuBGJIi4URCADFspeHyziASBuPXpLpikWjmC3D6VtTaDT17ogOyGLf6/sjsQUz
0KS3PzWBuPoqRGRpTtZxJ5yr10apr8mJF9Po5LFkrtcexaiYmUWAZAik894OhKt1
@@ -6815,7 +6815,7 @@
sub 673B436865B87E35
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGKVyb8BEAC+qG+3tDrZkCJlJwiU72OrX/R+cKQ8Jvp2lzwgJg2Sw/S0xXAz
KqoxvfkcM/egEWbxUsbuYVVXlAuGwTJeg8QtiuqIVXyoEEmUoWIqjOsCcNDbQ8Of
@@ -6859,7 +6859,7 @@
pub 2BE5D98F751F4136
sub C7FDDD147FA73F44
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBEwMV84BCAD0u42clJ3hghKlMGwFA8PPlPgSEZjyvs2dRCF+dKWBaPUnR88K
kGfWB66jX6PBtHzeiVRa078lL002S1lSth2A+s1UfYGS5wVbE938wO6PCMwgoXJ6
@@ -6885,7 +6885,7 @@
pub 2C7B12F2A511E325
sub 10DA72CD7FBFA159
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBE+ZO+EBCAC3fZOOuYKthr0GcUge0PH2bh18sbM9XUmPKQz/W15l1NA/2ARS
2gUXM0R+SunMlun9KsqjnojJ2ObVPvbm1Hg/66JSRgR3JWfIpSlJxLicpfu8rCfN
@@ -6911,7 +6911,7 @@
pub 2D0E1FB8FE4B68B4
sub FCF74AFDF5947ABA
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFYVT4EBEACqm1qKc6Twp2Iw0tjUqr3hrZ7mjZMWg5MemH9ZiQ9iVIqV4Lee
KmgjVWk5jnTslriymDilDIMk0YaT67JokhgSdqMIavI29tJ6quOp0K7Rj/rNBc6p
@@ -6953,7 +6953,7 @@
pub 2E2010F8A7FF4A41
sub E4D15F24364C7906
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBEnOgPURBADYutfvXAtNgf67BQ2gWTI6+nKfILIwMPzCbQPMd7pykzF5nPMu
Nswt3E7efo5IP1Zsv6DRrLafAW0OJSmL/oo8/ta0AfqcxCCbJ6CUyViifRZ5T4nU
@@ -6985,7 +6985,7 @@
sub C4725C965E0455E9
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFPSiQsBCADcgBiaKkIG5jVFbQ0NyG//y18S84/OT1X1I82OwtTryxNqxT9A
q6HuTJqRPi5Qd0BwmQB6dG0mug9AEp58L8W5udiDysHeUvBKY6zTOprSSFvFg/Y8
@@ -7012,7 +7012,7 @@
pub 30E6F80434A72A7F
sub C30F4CB428DDFC28
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBEaNL+QRBACYhfwZdDNXVeU9G5/XsxrUgQGKkhfOaB1CyPHAd02Jyc5oHR0a
nu7dHb6QBlY8b47pX8ii+uTCOX2yyFlJt2cuKYqN1TwHrMspDTC9K1x8WJMmKdM5
@@ -7044,7 +7044,7 @@
sub D79E291A1BF549DC
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFZ1ptUBEADVzx4LjDmWHK4gY03VBGRh/A+1CAjwdDtcrHPnoFYCYC0uoe8m
z/iESYlAHRqVo0nMItZgjqGTPD6GhQvJn/fzXTjIpYIDLZgPMXxImHCSRAFnduI6
@@ -7089,7 +7089,7 @@
sub 5CE9BCD2ED28F793
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBF0vfHYBDADEDPY9ub98c7jQe4yMbPke3A/sxNHnn0WuA9JN880DPs3L7lrv
9VHTOlFXslDNBPYSbgFXH5YlMGg8ZY8bhngjc+Z3dtrCX1cAjUXOnibi7fBFomLB
@@ -7126,7 +7126,7 @@
sub 7494750BDF4F8FAE
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBE1/v9YBCADaUiBecDzwU5g9Gmn3T9pAa17OlUl2iH0zn8tNTUg++bW/A9m3
lWykQBlvPOi32lqZ5q7yewSNBGHl/pHRRVsIE6hhkVigNQbMztRFPshKCU/0RvKu
@@ -7155,7 +7155,7 @@
sub B8EB751F2C19011D
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGA2h7oBDADtWcow8HEnabHf+poCBJR+MG8JybFpgOQ5ns1e6b3xnD51kzqv
0I1orkmIfhCVU4nPGp2jy0JHQUvf3NDIDobt/O/C7+3BvNanfw7sJeHXrCy90o3I
@@ -7192,7 +7192,7 @@
sub FE694B892910DD22
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBErygmoBEADbs8zVUn5ZwbsG3tqT4x6U7SZYOtd3WXOtHjuu9Cyp74rZ19Pi
XNbYwIAoCgOI/nXVWwuOrNJH0pHaQ73slbNzLxo2ahQIkw9PbK4V3YXLai1r/W6T
@@ -7232,10 +7232,47 @@
=n4Zz
-----END PGP PUBLIC KEY BLOCK-----
+pub 36AB7ACFFF2027B1
+uid krzema12 (Piotr Krzeminski) <[email protected]>
+
+sub 958D552911BCFB32
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v@RELEASE_NAME@
+
+mQGNBGBM2rMBDAC/vuwIsVD0yQhNK5GF6eFRSxQxfG/XPXBQldx/+xTJaUtuItZe
+qxBTjmkwDP8m8jMm4iNtjHNOyfbN698nRC7QRRIheAw6MvqcTJNw+6WJku2vsxh4
+jceeGogIgAXRVQmOGApcLkD1+tYxbTcSU7wXrD8dxspAy14Fh1ZmyxhmhrbhxdKp
+4Z5rQPnQdDmzu/CuaN7qQyz5JDcwql5DGvXbbBVxBdYb34rkF2WvxZMEMuHIYoLP
+BWcHWsmNrY07+46O6oudiKk0f6mIyMbHbqAyskarmeXUQrWPMmHT7Ni6KDANRZLB
+m49VJV/3mYpmIZZq4QmWrIM/kg2lw94nwtn1WCseeH4kt2TSB1UX0FnukRYVoSM4
+d2SwDoBsW/HedO8A4nlNCYqrURTLkcgoy1EyegdlmRn2q/FAnCIUjjm4hIeoy2vk
+QeaaQg7lCljHAZtvzWjtN7WBRG34XwNGAXiBXdITeMo335tzqxruDCKGdkRnWFIF
+URf4O+WsZ3cc/kcAEQEAAbQ0a3J6ZW1hMTIgKFBpb3RyIEtyemVtaW5za2kpIDxz
+b25hdHlwZUBrcnplbWluc2tpLml0PrkBjQRgTNqzAQwArw0OrUg0it4NxA4JqEQ3
+Y4cVwc9FPSpCpR79k+CUXPmmrwlO35Q2VvZ/nxGYL4M1CnLfrt46nnylUL+oX5Gy
+dAu2cyqCL1rH3OLrk+q0KEexkulWhLJfaOUXV3PZBkYoFRleZHvFn8Qfhlo7eK67
+LQ6vWLTBCqWwNbE5kbmZTiwBLkL0ajZOXP4rMP0o3aBkiMj0Qiq5DEzrYjpjGtUm
+CZZwf4X9c86tkbSS+i/VgHQfR7MUwg2zXswJu/t8cc+PPW7qpyfmyRhnKBFK4DqH
+PsqDVFAnWUqyV+Mp51wiUbTH8AvlKCk+xNAGhNH93/m2pBWLHjDFQFNwqiTwLdj7
+OVLaokE4kjRXYow7TrjbeSfY4VI1ZWkIFw5MdDQ4VnzRS7lRGPn3Y9Ortpu0wRFR
+gygnRF75AWqBtoqSjtsdUL2X2kx8UleOE0NAVF7MwdlsUruBbnh/TFcudJFjAEdU
+YmfJhoL2PyUpsK2itHZTArXFLooEwRCQGWWxUYqFZ7NzABEBAAGJAbYEGAEKACAW
+IQSCR/iJM8LVb4MK9zQ2q3rP/yAnsQUCYEzaswIbDAAKCRA2q3rP/yAnsf/qC/wO
+Ou/ZIQGNZe7ebemklst9m2PHngB9TGljixu3M8QHhDaH1eIYOhzhMMNwtJQmDBHy
+EWuYurwEJVV62vgyl2/S7SmxXDE7Kp4DRuwJAvMI08vCKLNpwHBp2HogCON6nVAv
+BOedrDhFNfeBXMx5Ig5CPcGb3DWBKCnZiebtyRMQyXq9z0wC9ArS6FNmzXOfZgBA
+xSAuoIgGOHBOF6nOQAoQdAqHpIKJWa0kvBEyRU5vzTJHwVCNtrCDMtgfgYWXouVs
+cljqKyXTSeO0wdiIfDelnZXS0gU3kNgxW8kFumlHiubOnzn8ClLdL8piGFxOXbcm
+cMWh0+SndYzZ9SbnUbUKEbR7GDH39uqrZ8r3YugBwwUut71uWtu6EEhvLAS+jRo7
+qKx+doc5xpCPMyHPpo539DTLeowcp5O/tdkaqypfeSPf+ZOzLWBbMj9zsq48mSWV
+4NzrdvDj2rIvs02a2YMsdpNyt/kQbDbZiOrPuFWwdIw7pNuwCpt66Zu3V6d9dCQ=
+=B5lt
+-----END PGP PUBLIC KEY BLOCK-----
+
pub 36D4E9618F3ADAB5
sub C4935FA8AC763C70
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGGiftwBDAC94Yhhh/5yO8jYFkg01MPnooXKZEPwxAbAg9wn5iM0tHxhEpkU
zJVYZ+JYq013+Ldp8Of7A/d6hKTtZ0xwSeY7S/WFykIk6tc0P5j0sfFS3pGPDk+W
@@ -7269,7 +7306,7 @@
pub 379CE192D401AB61
sub 0CFE993CDBE1D0A2
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFTi8JIBEACcN1ucQ1uCOZ1owTELQV/6i4q7NbYdJ5wf7yPYfEugSo3yfbo3
Pw/XEvlnpDZmT155sGNOkteZtZMdcm5XhFbdtquLlrkjAcUGatq5rAt3eLAlvU7u
@@ -7313,7 +7350,7 @@
sub D908A43FB7EC07AC
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFu07rsBEADYizNlY0FYNZ6q2wx7AmWLw6PHje55uFhYM8Saqtwg/rm1tl78
j28E/coP2zMFf/ec+zqKsfYi4DMmLZ9ESIngMUOIE7mY0Pp4WN7oYFRtvU0ARWyp
@@ -7370,7 +7407,7 @@
sub 9B2A1B698A113AAD
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFzwo60BEACg1rgL5jUtKkFE5DiwqJwxzJyJDH00TBSN6ZT+nXh1UxgC9q2h
olF9V+2+LV1Jcmnc946xzIMiWLG33QB0NKVCdU5jNuLahOcViQQjNfGXwNzYoNCR
@@ -7413,7 +7450,7 @@
pub 3C0A8F4744F37328
sub D17266C6E05F9993
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFSQ6LEBCADnoAfQsg2uDYMnEPqt7tlnZxzyLVKiHXdJzT6OHA0FUdsB9H/9
vWI863v20dsk4+tf1pXLa1AWBusInf7FM1JBCQBc/By3fR3JRhJU0QSoEcwtOQSa
@@ -7439,7 +7476,7 @@
pub 3C27D97B0C83A85C
sub 4BC7B9A81C39EBA0
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGEdX1MBDACuRDzoPMh3CyUHQydFo363R6OdXqMZ8mJQMdysIJCXOXZGRwUC
uyPOUfH6uSG24RU2zvD72D2SGAehQKLXLQeN6XCt9PRAszP18dJADm10xgkXJm+G
@@ -7475,7 +7512,7 @@
sub 575D6C921D84AC76
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGL4BxIBEAC+lX44fd/zrVQPzdKygarBd/X0bBpGakT++Kfk4UBGl3q+wd2G
R9puB9R377ds8hU7U3To8sHguUZo6DbD9Gb/is/WajSb9g92z+rMow3KbqfCYqWr
@@ -7521,7 +7558,7 @@
sub 7ECD484BE871E4BC
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFhV2aoBCACyHcEuTUn5nVo1ODvWvgBgV8b6Aju4cVAhMNIvAdcOYf+N9Rgo
Y/669/P371uN2hc4SxJeORBjHyzkAX2sJZQj+FwdvGl60YX9Zv/NQaTzC1WFMRp2
@@ -7549,7 +7586,7 @@
sub 6B7EF7B18190F4A9
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBF2KLsIBEADgVw/j0Loslv+pBDEfYemeObeKCWBhEdAiGznT23XFb4eOa4oL
Yk8FTL5SYV+Ylm5Pv4zUGV1JUggzb4mS5+/k0kl2OHzZpJTLz45E9Qe4KI5vk6jT
@@ -7592,7 +7629,7 @@
pub 3F36885C24DF4B75
sub 97859F2FE8EAEB26
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFmfSwYBCADdZEuR8cs2ejLLW3+Glxiq15rVbHbxaWmmZApGNijFro/LzFrR
z+99N1mnA5+Ar/yKmn8lsCiTWukGQzWbdH/QSRUdyHtzxbCSeONdMhdKl3sJY1h2
@@ -7618,7 +7655,7 @@
pub 3FAAD2CD5ECBB314
sub 3260CB2DEF74135B
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFhqdSMBEACmveOOsQrTky8b5M+Cq6lbhqRB4+INnfigxr7+EMpswo4AxYuA
Op/YG+G7NU5h6EK6Tj2dVfXga90GYFkehtFRZgOUJUGKPU/53upsbnsWS8qjJD8g
@@ -7662,7 +7699,7 @@
sub C0B9C2CC3DD97C16
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBE5zrtcBCADFfU0ugIGUCM44fqPJKrsB3TaDu5EpauvFfYqUfyookzMHSKtB
4YqBSKzBEiZ1rFB/KCn7XJTh5epoCau4DsG4U0XZjsx+esDR4ZtL42LEzeMTuluV
@@ -7691,7 +7728,7 @@
sub 01F3A913FB698736
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFLmWO0BCADfxkkFnwj5uOALP07g8yArQpu6zbNr+5dtDvJe8Y51V1leb74U
Eh4U1IeosCRdKUCg0XlAjDmjrUkG6W/5AMUZM676JVHL5tVG1F+dsKhCrFOZoMHj
@@ -7721,7 +7758,7 @@
sub 47624A56526BF2F2
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFAZ8yMBCAD+elPZR4dx7RHLErbQadUXmxxh15JTZ7A/OmARW0ZA1kbkRven
4b3rXQKtWhZqxHh9Vb1FMgOnrbOi9984J3REJzLWEFM+REB6GJ3/ZAQvaAmrjDtV
@@ -7750,7 +7787,7 @@
sub 8A57131A07E0911E
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGB980QBEADLBOfY981RbUf7zI9AoXcxGignXkYbeSvxIMML9vAbnhmuHwa6
h+81ZTY2XK7Rz211y129YidPykkiLX9mY+OWvJsj7dTyVTcIm6MU5ETDvovfmKWg
@@ -7795,7 +7832,7 @@
sub 1364C5E2DF3E99C5
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBF1Vn08BEADgfOupXhJxyb3t1kzDNa595spJptjF5ViyXuEJtlMQlmobPP9L
2gZH83gNe7Ro1TsLesgWTtin3hGANSKITdi/wVH4ET6lPInv1k/8hXe0zlF11Zmi
@@ -7840,7 +7877,7 @@
sub 2C8E4A350000730C
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFQMn6cBEACn5RegEd6pYnrIwFMpf/SKP1aIp+rF657o4zP2eQtCyU2Kxiyd
VXyvUqIN9kv8exnNUOHnjQzUyVFmcaYaQTxf6D+DVkSlusHk4yq+6I4K7g42Ghvw
@@ -7883,7 +7920,7 @@
pub 44CE7BF2825EA2CD
sub E01173141D06B1BF
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBEzQQMUBCACbwbw7tuTWgwPsDAdQTWGO355jP75oBLHwGgEwV+OCKtxkNXNw
wrJqXst83vmD1dEJyHflQww+d+Olj90IefQGfR+K7O005C2nky7eNGIomxaP52Y/
@@ -7910,7 +7947,7 @@
sub 8067ECAA8D58321C
sub 750F9A735EECF640
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFkgff4BEADQW10I1gEirYflEkNU9ukvBD/UFzsNxtKKxiDB58O1j9/o8bJN
uM56B/skfFg1V4Gkpmnf9sJyakI8jHIvZ720dPHB8nVRBKV+sUD7hoI2QYVJMJMV
@@ -7977,7 +8014,7 @@
sub D74B959DFA1D84F2
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFkW7RoBCAC7NMtr/e27nrUuIcEZEJBZS3TbZYId80UNQXgYmqPhy/sfCyMc
87eKzOalauwLbr5+VGuKqhvKrihV1WCt2+FUjOtnCf1GutpAUH9plfSs8IpRog0h
@@ -8006,7 +8043,7 @@
sub 4CE6E05D128BCFAD
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFE0soEBCADAy/iIRT/lpb+vfDjWs/k1XQNU3mzXoMm1mmS/Z8VOc0jF7sVB
A5z2pC6u2OmEr1oJkhWefX+mU//7kXs6VvUCReE4uheGBlisg/ELEXkTm342TcwS
@@ -8035,7 +8072,7 @@
sub 868FF6CCEF26A83C
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBF5CDMYBCADC1/aWU6ZbGZEphRbmjUPNfqh3N5goSnDCou97mmQ9Uq8iBuKS
UXJnGSOHudXK56f+Drx5lGZdLAzveZdqaqb1o3yLFO3PJxwj3Ulhab3O3uTG2eR0
@@ -8064,7 +8101,7 @@
sub FCB1A11865F6A17A
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFVB0KABCAC8YRgcTIomAMw865DHxS/tbFgqN9i7M+tgpih1ETJbb4enhIBj
Upeq+MoFCtxN86zGu2gsA4DOMEXVCReJ4O5n0F8E03+NUraCnJjbXLW9eEyRQRaU
@@ -8093,7 +8130,7 @@
sub 3EA98BD451E4B457
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBF36fYEBCADU+1b8TH2AhJuJXebg5D3UbR9rk8/9kEfiF7ifbb3nCB9tnF5M
7NnNocEdAq3XezNuSj9LtEpWUu6P4JdpXcfZiQO6wrobzSJRUWDc7X8D8NyhGpd8
@@ -8122,7 +8159,7 @@
sub 726F4E5C34CFD750
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBF8QwXwBDADKNLAHhjWUqnLYiO+ws3Hy1du6tMvkR3nfsnIDqpCvSjb+3/rI
OHSyq8TbaGLLuHOM4K/KvrKgjhTbXQxvx1WR5IpoylcINzI959yAbaywBj6gVQB3
@@ -8159,7 +8196,7 @@
sub 5686B45C142551D3
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBF1EtnUBCAChtyYd/4eMAxTz5DVmO+8QOrTA1cf9bprQhtXD5pVbw8/IGKN+
EqXmvt7AGy+4O633g7ec5iyirwCfEP+4YDv8k1LOvY9C5+tOwfK+FxAPRVc1AAB5
@@ -8188,7 +8225,7 @@
sub A568CCD291175902
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFUpCooBCACj6t0qOEGqMpX3puhk5W1TXZ1ewSXPS1yaoiFD2rysxjVWmXxG
wvon31ed6PaZqtv+CUCIjbCjJN50dQF6g1I4FLvDcpF8LuLGriYtFW43lJ/GW//G
@@ -8218,7 +8255,7 @@
sub 25EB2A6CB1459233
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFBm+zUBCACsrBpO6mOsZ/B6PdPPV/Hj87m2GHeEYEHt2o2l8X2BdbZKbVW1
FIKnpYe3+TsFCe/qNxlR6vk0Jpy3ChD3nW/J0rmU0ju1SZnS7rdSMj3AI5M5xxpy
@@ -8247,7 +8284,7 @@
sub BE0F021FCB5F68A0
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGBFmccBDADIusjFY82nMHFXYxY1b5eIWtyaXTQxv/bXfjR2Yb16dURgFjai
OeuYzapF7vVqNV8/H7Sya0W9z4OWf0ZttWhtQFcmhF90586OArXEikKcFgO8EL+l
@@ -8281,7 +8318,7 @@
pub 55C7E5E701832382
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mI0EVdDLQQEEAJMtYCaTA56YsP5RzQzPvqVTaR2nZ27qRk36blHB9WmXK+NHpGeH
PHgq59mLPVueo2/M5k/fFrCe36jHePP31gYpFtueeYDfsofHwod0WhsHyC7JfG8d
@@ -8294,7 +8331,7 @@
sub D89D05374952262B
sub B5681E477AD61C38
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBF+7lwIBDACcXIXAwFDoWvCCWn+OImyyJQvSnnte93Mc1ZJtlArkrjeGU7Mu
5giUH+FOyiXlj7CU4G9RTnAzDgM8XPncWOERgRG2dXtO03Li7iUEX4Z8PCUGsTxP
@@ -8372,7 +8409,7 @@
pub 571A5291E827E1C7
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBE9iFawRBACJb4OMk3zqMDNvSJKYZ8fGYrPq7yCcf/ykKDkGb2dtPnAZGkSp
3mmNlTsU6s9ARn7BtkhIuM5TdbLs+z+okX62h3F0WW3h+CpfIXyKSgl7uWbhZ5G8
@@ -8389,7 +8426,7 @@
pub 5796E91EE6619C69
sub 153E7A3C2B4E5118
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFri3Q8BEAC90D8TTu6C05m/eq6HbU8gOHFc+2VJriVmnoyODTlEk/LAsT6h
BRok7nzY0LpNUzUREjJy/w80YTOjLs25IFhnqA6mq8BGLjFwjhBPA4piCyhW/Elh
@@ -8444,7 +8481,7 @@
sub 2E74CACB6918A897
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBE1VSkkRBACkCgvt26sMi+0X+EOJDMqdK0Sziy06k47LJf1jOg4tTZ2T9QtP
OZ8fD+va/O5+q8Kna993jzcO5n0Nv+R/K3+MvUqSmdITshCIjBt3cC0n6FWndGyl
@@ -8477,7 +8514,7 @@
sub 92BD2D0B5B21ABA2
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFUBG7QBCADRWXf0Fw05qRhM4cRnGKlOW1ecue1DCxHAtFwoqmAXyTCO+tI0
MEW5SyXUkX6FsWLl6A2y+KgOs669ogzfQ0rnZMEt4HisRp8wpgk3GWR1/9aKYz/c
@@ -8505,7 +8542,7 @@
sub 990B862E2E89C087
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBGGugdABCACl8r69cybA9Xv4mVUXH2UErQutkOsCxXJCb6KcL5Ucn7+0RGxc
PRdw+sw2VBJsBctdUeCvNA6o1126O0gSgbQPTojvdyYtq4J9OAjS11RoiiQ269Zc
@@ -8534,7 +8571,7 @@
sub 8857595B73BFD468
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mDMEYuRVGhYJKwYBBAHaRw8BAQdA2Dp4m1Yhtb1g94pQzzL24FuP6b9KXF8lP9Dh
hZnynhe0M1Rhcm8gTC4gU2FpdG8gKEZvciBHaXRIdWIgQWN0aW9ucykgPGxlb0B4
@@ -8550,7 +8587,7 @@
sub 4E5C59DBFF7DACF9
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGPcwwABEADTw/gqmHh4LTSDsBP0KMoXFtFQnv7xmVPPrPjt0NxGn3w2WIou
7UaLUTViKkgm92h72gyM7N9JfNBLcYrqVf9ed75MPdGQgzIhkVg3SLWZGFoIQUJ4
@@ -8594,7 +8631,7 @@
pub 5B05CCDE140C2876
sub 9D29AE4A6B50E01F
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQMuBEwVZOURCADNnKQzSjFuI9/IGj3WTJcPU2B/H8NbZaTsz5WE91WumgZulK2q
YeD4u6zdOyFK7DEScgxk7dicox9cNEgYKQnQXctDhfqER9bnvA2iJ+AFxjRAWyvs
@@ -8633,7 +8670,7 @@
pub 5D67BFFCBA1F9A39
sub DBE749136BF76809
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFUHdtcBCAC5xFdAcSc5qQsPkujcRdzeldrESZBo1/SfGwFV0T+lgp99QJuI
LDwZ1OEG/lQck59J0JRdAgxlUj1um5LzNYexIJSdxRz2DffQ/z9R+hw4DF2h0fyP
@@ -8661,7 +8698,7 @@
sub A7CC6488427379A4
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFgRFtYBCADud9fmvTI8Dbs+9GcZUIVzxkL84QYHSDxI9fF+sxfAviq1U+YJ
a+ZLIW7HsXx8vpn3hqIqAbDxHjrb6MEJ3OWD5Ks7O9Lq7HOhtqAT/mpV3fZmf6pF
@@ -8689,7 +8726,7 @@
uid Sebastiano Vigna <[email protected]>
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFijpzMBCACxAT3jijwXbI6b7LIF/k8oSGyM8ZNJpb6AQvPqKIqCzxNFXzow
EBCasKMhIWgGy+293Tpt/DY4btJie4u+igMBS86iXrF8CUnOLPgTlAIyil/oREGJ
@@ -8706,7 +8743,7 @@
sub 91A4BA316974A467
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGHvIbQBDACpPdbz5UIVIWR4cfXyyZEMOG0ayCzJQPsT4eq8XR0o5Y9egfAq
dRXC8paInsaF/iVL8BJY6CNq4B3dUfJwKDcJiCiPbiQgknqF1HDBqQtCb4akW8f4
@@ -8741,7 +8778,7 @@
pub 5ED22F661BBF0ACC
sub 31ADCD8BFCB760B4
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBExyNhsRBAC/W5cMapoP7NUn8S22iWG5bPw0bconApJHP4kQdT17gT2JgNJz
BmuGWV59ZOGQkc6woeFKc1s6twlsgIL51jMeVOtgLJRGTS4So2hthNqDcgO4j8Lm
@@ -8773,7 +8810,7 @@
sub 0440006D577EAE4B
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBE7JURcBCADO+9Dc4/JnB+wX+fq+Fr2zUGSPOT6/qjE5kXL4FEbJKsqDSAKG
VnbtRrsIUdmNIFQmz71bBDFhRBbrSrkz927k8eUPhYtxE2NmmWSuKgrjF4qviPQv
@@ -8802,7 +8839,7 @@
sub 73F7734B17EC71F4
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGEVsM0BEADiZwFLiyjeOLeGS0jAso0pOwUigT9PpwQq7JFAuJP2i9C4Eunc
J2HWRdMhnAY12C2MVetSwhI/4QID+rIreB7ooC4xv8sz1PIC30t2oSYtXF4w5DYh
@@ -8847,7 +8884,7 @@
sub FD2D3AEF63B97A64
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBF/kpOgBDADKuK/xrCb39AAmyzVkFTP03ZNCAVhDnmx/1bSHTwvXFWQ2topE
IgqlMpKmjuEH03gfOP2ibbgeJ3WOJcijqfeHNZ7wGDcslbKOnFVrcN7DuJx9LDYc
@@ -8882,7 +8919,7 @@
pub 62C82E50836EB3EE
sub 2AC7BF2F3349DE80
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFeOGY0BEADIr99yL4ahwgM3KB7zMVzDk/PEkzUWpm1BSxqUxuQtzWArFj13
Y3Zi6g1tw5jKESfxtmpXx7j7xR3qVdJbsYJMU0zQi+FehwnKox3Go3UnIKt7kydz
@@ -8926,7 +8963,7 @@
sub D547B4A01F74AC1E
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBE3XFIUBCADcj1zw8m1evCgEMqxgOfl6L8y1tsYWsX7tVPvHEkYlXHrdcpkB
fGuWPrauvhBmB9sBkFfxzU98Ilz3Xk9pfISYiaMUk9Mk1ZxsCoYPVhxvOSvk5LgS
@@ -8954,7 +8991,7 @@
sub D3DBC823BE4819ED
sub 0162FE0CF6E18BD4
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBE7sdQQBEACsLaqrIiSlsJIWpalL9i+i6x8Yg6l+bw8qaH/i7kjZKFLf6Xrq
PFHo9dpF3LPOguvPLP5fs04KIShl0IhJuArSxvwfH8GnqPAaM0TZpfJQ9uqAcvxk
@@ -9033,7 +9070,7 @@
sub 1E8F1D57A4450BCB
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFbqsT0BCADwERe1Rc9qNWwXOvwZHsjauVDy0TpqNVY8I3S+OYm4rX1dkjyh
+6bTEH1ys6bKevvR+PLhYzTGKboHnMT0RIINY/DQQSzHr/GRyCiiRlRvULbt9Fnz
@@ -9062,7 +9099,7 @@
sub DCF4B49B4D5845D2
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBEmhev8RBACz56FVQ9l701+PE7Nr6+6Lsoy5tK6wmV89pEvUDgDjT0VTs4EI
dupAk4a0dLn8Lu87AloEYuSzbCxv5cH5vyDcvLDK6g3/sRC1LPQPydD+UlCvG8LI
@@ -9095,7 +9132,7 @@
sub 0AC07D0BBD11498C
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGBVUWMBDACXALXWXSrB2V95lR1L+i+sQsTQt8tCIgX0iX9UZ7Vw2K/lLnLw
WYtM3oTxYox4OdgkK9tK6771EdCH5wQtRdUQJjlsBfZDPMiGqmh1jrAxAugEkFyC
@@ -9129,7 +9166,7 @@
pub 66B50994442D2D40
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBGDoYisBEACqUDZnT4h6ma6XIzdC6KR++uDbR2VKdhCuv0Og/sHEKkm6ZbG0
OFB8tAaQx/WlsoQyf3DlLfUEOGDai875Aqor3fbM+E1hrZbQNfsOySKEE52k7PYe
@@ -9150,7 +9187,7 @@
sub A1766BE5F812AC2E
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mJMEYvEGpBMFK4EEACMEIwQA6knc/2gtbqDhPh5EzrymR4Hwi1Xf2S0aqMopA1zg
IeZzBgSfL+4fEfpXL4eAzvrk29jIXSizDEOgFpw3PW3Om1gASxub4Jo6EQrRgOdd
@@ -9171,7 +9208,7 @@
sub CA7AE93399B1ED99
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFqHCi8BCACgRpCaVCiJ2MccCN01SbHYowmM255nSYKOnfItBmXYAMtc4rL9
n1y1qFtc4LBbkIrPH8CO2zpEImUTZel4W93BQkluPOO3EX/hLCTCFfXrO89L1u4V
@@ -9200,7 +9237,7 @@
sub C0058C509A81C102
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGAofm8BDADhvXfCdHebhi2I1nd+n+1cTk0Kfv8bq4BQ1T2O85XlFpp1jaIR
70GAm2MOt8+eEXt/TuPkVBWnJovDpBbkUfYWxSIpPxJzcxWV+4WJi/25fBOq2EuP
@@ -9237,7 +9274,7 @@
sub 4083687620E57086
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFfAYzkBCADRFsmQXLC6UbFNjCKrwy+AwiMNUchJdsPkbFrvueWKq0nPB6Rh
D2YGNdCdLlkeybHHaSjYi/Xdxv7Vgfp4d32tzoqQJe8Q8oYYW7KbTkfzwH7TNLcw
@@ -9267,7 +9304,7 @@
sub EA8543C570FAF804
sub CA890A5FA09CFD80
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFgMcBMBEAC/xcIVVOOh+F7S0OTzBlFH34s5fDbi6Zto469tZyW1peyWtXAZ
m+2jzFfeTCHaUQO3YjoTy2fPygS4tVD+ew4EAzMG5Uti4kwWZw0PYKz2JO/gl1JY
@@ -9333,7 +9370,7 @@
pub 6A97BB242496B68A
sub 374A2ECC99F4A7A0
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGK88f8BDACqAfnTaZazrzbO9vM+3nAdmcW1QR84zwUKneFML/I45kihIW2t
zhcx5JIwl7gK6q9kzRGClMCkSGhq0y9Q8UGR+wAmLJ8bexS998c3rtFfg2/c1zBC
@@ -9369,7 +9406,7 @@
sub FA6831EE37606774
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFTDM4oBCAC9cUMAjkP1dD7tt0JUI5kVORKagn4/zG6+Y2MUwGgJs481xsFC
jXPuNZMucAVtXmw5Sl7FbsfSxR/9jJ2pnbXL918eRFbUqY4LnuOTZjcgNWo8PWPc
@@ -9396,7 +9433,7 @@
pub 6CCC36CC6C69FC17
sub C694465FAACEE66F
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBEtrDFABCADLXGAhjPxdh+naC6XU5kficZYEVAURNRa8MTnaMKr+31v2zcAk
nyqyjihcXGQBCeaNsz2mQkc/MrKdnFNVSwp715JcmcqDJGfR9aIDMUs9PvoNkkqv
@@ -9422,7 +9459,7 @@
pub 6ED0F678B90EB06E
sub 3605922A9B0C4A82
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFTCZ/YBCAC/AX5Uve8E97kKLWumtArjcouxHzENzNez3/wjiuIdCTchm/G6
fUKHNTqo9sdcnvAO4mfJbysXh1tqXl6zxjw1QdQGCyy8klGRlpEiper0eS5heDhV
@@ -9457,7 +9494,7 @@
sub E2F840B227D3C024
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBF2ClL8BCADs2bbaF1ZMiMkTUUb59NTlyAbOOVWoIh7cnKeNjMWBUTP0kLFI
XpoKiyccQLP4rFdbP2yI6h+LJR0Kj/lJmKpCaAooNlooxfIyPUX5TMvDTRutzwBO
@@ -9484,7 +9521,7 @@
pub 72385FF0AF338D52
sub 458AAC45B5189772
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBEr8kngBEACvK2oDnKTCGQWUEMxCgQPYTTaWVHzaRFZCn8po/DnKMh8llPuU
GRdi5O7ChLjsg7qlNJKhi//ZoSnNBdPfT7EGNaKxUO13BVNBvXDiNNbUTWGBY2W7
@@ -9526,7 +9563,7 @@
pub 7457CA33C3CE9E15
sub ABE9F3126BB741C1
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFIXyRQBCADe285y3Pu7KzoKyP6wqeNXtvvuwMatAmPm5x/i+S8MlryqzsYa
x6twUmXV1yKjjtGrO+9fHvTOWBfSSP+fP9KTaTQYSasoJq2Mw4cQDy1i0zrxNZUw
@@ -9562,7 +9599,7 @@
sub 6494C6D6997C215E
sub E88979FB9B30ACF2
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBFcMjNMBEAC6Wr5QuLIFgz1V1EFPlg8ty2TsjQEl4VWftUAqWlMevJFWvYEx
BOsOZ6kNFfBfjAxgJNWTkxZrHzDl74R7KW/nUx6X57bpFjUyRaB8F3/NpWKSeIGS
@@ -9755,7 +9792,7 @@
sub FA84183FDD6A6B98
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBF6RvwcBCADIVU7oxOiljoWxNTkZ00PKVwyqhahYpN/4lamULtECCS+HAF+J
DsNy/6QCl7lKAGrSyn9dvsI56KEkGvUJfpQrpRlg+uIQDMxS8JF7p9n49DNc8Q88
@@ -9784,7 +9821,7 @@
sub DBC5123E2E98FEFE
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBGSsZCsBDADJZoPoHGJNAB3sn/kFQ3zlj+vZ7OY5aWoH2nL3tHQYZvN/pJRs
8wu4Cw1ApatqLIaur6S6LR+s4xB7HxnMvpiF3NMwr6ZeZBUUTGEJbRgFhY9TqZam
@@ -9821,7 +9858,7 @@
sub AC9F6F1991913E30
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBEqXMWkRBACnsxVroe9ojc2AnRn/85KJi/Ntsbku5iJ5z72B6I+VGn/b1Xln
kuvRJ41RLG13lKVmHtSTq2pajjmAr9jY5gS8nJ3JUES9bG3yKNN1IDswXExfAUJp
@@ -9864,7 +9901,7 @@
sub 9F7335D63326E7F9
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFWdcSoBCADK8j+0eVZKUGctZo/VaJ/K2Wppx4jEFgih8xiIWREQ9B3QEugJ
mJMWZHhrnHB+sjVx5No482ch6sVhYmC+VMyTdzepItZ8beYa0pnNGJnrFT+HcTOS
@@ -9893,7 +9930,7 @@
sub C3E640F38D845FA2
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFZUsiQBCADGmoidvh3VvXWGdwbAtHPtDPKEebE/MfFVO+QTRbjJxphzKwAt
mxHruikafaSTnC9FWizj99e/Yc45YZHcnt5Htmy0a7DSOQXL37rrnieZxg86tYmC
@@ -9922,7 +9959,7 @@
sub C189C86B813330C4
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBEvxja8BEADAzZOup1X0B12zJsNsDvXVIwmM6bB+uhEsUHoFTvmsEVwRoZtn
i7Q0WSFoY+LDxbvC4Bg1+urCrUrstRJYRyF/pMqPYq/HokRlPjtrli/i3mUSd0zN
@@ -9967,7 +10004,7 @@
sub 926DFB2EDB329089
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBEPonucRBACtbhYckAoyz1tuSXYX4XiqGa5390gIMcxe2hJ+Ncx9o3zX09Im
f8PW27BnMrz7EIydgB2wphhjfK4vkNNtm5ZDWH/zJStsk1Fe7lNuuxs8XorX1+8D
@@ -10001,7 +10038,7 @@
sub 3967D4EDA591B991
sub 0588BC69A286FF16
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGNBF+TCd4BDACbIA94MfIWL0SpvZwBddXgx36Lp9GYOWNgGoQCWSvk9vaMrLaI
rEll0xnoP98CfBQYrVSAmHDMhSLBCjNB3V1Sdz8GRdOG7HUffF7Cqwbm3Fxo3H/h
@@ -10083,7 +10120,7 @@
sub 9842FE565AA0601E
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQGiBEvsZw4RBADH20nX+H1xvMBYmXRj1Aae4dRr6Y6qI7QRWHO6Z7/dxr9bk/NN
Yjq5KsVOQxZzloVdtqx75rznT7fZq98g7Nq9IeEtB6k4tnh6XQLhljJMk0a3mzdt
@@ -10116,7 +10153,7 @@
sub D7913335BFA51814
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQINBF99kCMBEADE22W+h7w4R9fHyqEQV1NpgY2qI1S1HlwSQLQnf1pt1k3ZvMzX
Eh2q9lNAn96O5TqpiMhHm2ZEDORqXqAx6hEVpA5iy+7NrIaSpxcbM0crUxSqoceW
@@ -10156,12 +10193,82 @@
=oANj
-----END PGP PUBLIC KEY BLOCK-----
+pub 7FAC222BF1FC0C12
+uid Erik C. Thauvin <[email protected]>
+
+sub 776702A6A2DA330E
+sub 3565C12F190E4CB2
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v@RELEASE_NAME@
+
+mQGiBDZlGC4RBAD71K/vOcX69tHOv4e6nuIGqbqooKxURWJhe1OvoK9Z1pgadt5D
+Ct0HDuSGydkA91cAM3a2QzZmGepGS8r3s3ldTf83uBfsriYM+yXmalmHYNdMVrPL
+CDFUNMH7A+2rnKvhQ7phmMVBdeEtRcrncfXMHmFvfq0h2y2V/NQ6VfJq5wCg/xd4
+gARVHy7gIE9VtVMg3W9D6xsEAOxmB+PxSjD0y5/NN81EMeOYc3INakbQtjkQ0KoO
+jlkWot6OidFBpUA/ssLRUpmBGN4nTztXiESjz4n79cW3bRAG3b1XtOxbCkNltlrD
+DKqpy4FwBsnslKFEAOIwttWJKXkebtWIw5Jm+HHIENH+IzoE43JFmKhtTS/427Rn
+FIi6A/4l/WesI6bUZDVXuicWh4KYrArARpuK59Cycz17lZmAES7nramBCK7uNEi1
+XlZwWhKz/tYL8o2y2cwuUv45EOl+mv7sF88z65Vt60N9u6HXu1STFyl85q09g2Vs
+il+/P8Hlh71t53yrTtmAwkugBB4mbdi6uvVFlWN0kkxQhtXGlrQiRXJpayBDLiBU
+aGF1dmluIDxlcmlrQHRoYXV2aW4ubmV0PrkCDQRZDPiYARAAtzgweuKZHqTHK8mZ
+2TC0ySaUYTRskl4L8JetdXpnnYVXb6qcyYHSyyzxyR2SrSj2sjdyK2svU3FSxYbc
+OswyFlBjJsapQydE0ZcWrzhQbMgm/3hhQdzzCB49W3nl6m7pjFxtZTZvrxvjxPIF
+iLQUIbphrMyH8ZoxjS/nfMc/k0q29B4qT6heICZQJoOEZ/kgPC8SNKoFRhuVSp5A
+fBRMGe04dQYqxUYGnub/ik0dKXKJLapk6OVDgTr0daDH/awpc2RWM17Ct4qzmnWF
+BFDHcUNlkvYoQYf3xEoLA0hiTJc/gT0IB22Bush4cSoPv+JHV8QnY56Nnliv3Kdl
+4YYdiFLIAfFwjP41QsNbEJsY74e9sDuh1ZofGQh7g5ejDBDGo7frHSEM5QC/nIWZ
+b5XzPO6uL027NwqHn6ILQVlSo6pZcBH3I8wPGX/lXX0oVELBUJiCGSvDFaK6qzkQ
+QdljUiNiq7RW+EuNr3ut+8zviruzTFWXs1psnQK5TenxIjS6ewb57byJv3jhq/G9
+hHqNlmaaSA2FQU4oN0lgyAVC8N6dIgIQt0nNJQKfIgAhDFgpmBhCk1WgNL554jr3
+CkHdmjFf+hFTJzGdE8g2pIDWh5QTBL0sy47mfInfnA0TDhUnpb/luZHBPrKa6arU
+c2Gk0Axdski8T3mQDqmGY8cGC8EAEQEAAYkCaAQYEQIACQUCWQz4mAIbAgIpCRB/
+rCIr8fwMEsFdIAQZAQgABgUCWQz4mAAKCRB3ZwKmotozDoOjEACpSTVTZKmuEcOJ
+tD1RQIKIxVIPu4tatD5nU0IzhsLYPB98CZJTbDm1zuHpjEB5qRRVwsbBDvQ2cZnt
+Sz4PKqALT+xx+kxrEjO+Ubv7JLaA+z/InwiMVppipJ9F7WpRRqhSOCcgkFjzKtsq
+Qa4V5SUq4fy5piHimj367u/aV6/Psz2mNkS6cFWYuKSwDy/uGtm3m4xvUs1y+YpX
+7zzdeWaMRcdNAbkfTk0tbc6Ij+20l/jDbADO+5KGuIlah7GZcT8k3uNzO+ZRLyQN
+mlbgupyH2NvRNGhc3loYgcwfDtmM5B0Wd6clkWH5Yc6uPt+6N5a037/4pkTngzUR
+L8rQcY7seC5um6ouVCXdVkCeazFAHNQg/Q0zqN2Gr25uDpzP3s8y+A2ucOcNSQgM
+YIPy6CAeLnE9CpnudlNGbRCVWT9B+pIjKLI3AZo/9geaW0JLc9S9+HXGQn0RO00W
+EYX7zfkuX8KtDznjw5JnyDbxRAPiKJjoIoz16UehtqzhBWJzmk9+kqH+IfQ6IIsi
+HJRqJD4veGHem9ZN9+5XdVOgsiSx0BB7UrjqXBu15JTQRwg2gmy6N5XPuV5yM36h
+paRG8FOlYCmOOnFmpzzEBJv4Z8oT5PNWdhhwTugaxv1yWlTzFDbg/va5/NFhwtDi
+v3XnqjFTesbBFwMWIhRRMpCwl+J5oM2lAJ4p7U6+8andiWS6tuMns/mDTJ3LJwCf
+dgb+Mx8gFFGCQpvy4DTYfDtXbkCJAmgEGBECAAkFAlkM+JgCGwICKQkQf6wiK/H8
+DBLBXSAEGQEIAAYFAlkM+JgACgkQd2cCpqLaMw6DoxAAqUk1U2SprhHDibQ9UUCC
+iMVSD7uLWrQ+Z1NCM4bC2DwffAmSU2w5tc7h6YxAeakUVcLGwQ70NnGZ7Us+Dyqg
+C0/scfpMaxIzvlG7+yS2gPs/yJ8IjFaaYqSfRe1qUUaoUjgnIJBY8yrbKkGuFeUl
+KuH8uaYh4po9+u7v2levz7M9pjZEunBVmLiksA8v7hrZt5uMb1LNcvmKV+883Xlm
+jEXHTQG5H05NLW3OiI/ttJf4w2wAzvuShriJWoexmXE/JN7jczvmUS8kDZpW4Lqc
+h9jb0TRoXN5aGIHMHw7ZjOQdFnenJZFh+WHOrj7fujeWtN+/+KZE54M1ES/K0HGO
+7HgubpuqLlQl3VZAnmsxQBzUIP0NM6jdhq9ubg6cz97PMvgNrnDnDUkIDGCD8ugg
+Hi5xPQqZ7nZTRm0QlVk/QfqSIyiyNwGaP/YHmltCS3PUvfh1xkJ9ETtNFhGF+835
+Ll/CrQ8548OSZ8g28UQD4iiY6CKM9elHobas4QVic5pPfpKh/iH0OiCLIhyUaiQ+
+L3hh3pvWTffuV3VToLIksdAQe1K46lwbteSU0EcINoJsujeVz7lecjN+oaWkRvBT
+pWApjjpxZqc8xASb+GfKE+TzVnYYcE7oGsb9clpU8xQ24P72ufzRYcLQ4r9156ox
+U3rGwRcDFiIUUTKQsJfieaDNpQCg2uWEeylyeb7fxS8xwZJNvwLabfcAoN9rHTJ7
+Z3ZeNKUlYL7uHGivlZh/uQINBDZlGC8QCAD2Qle3CH8IF3KiutapQvMF6PlTETlP
+tvFuuUs4INoBp1ajFOmPQFXz0AfGy0OplK33TGSGSfgMg71l6RfUodNQ+PVZX9x2
+Uk89PY3bzpnhV5JZzf24rnRPxfx2vIPFRzBhznzJZv8V+bv9kV7HAarTW56NoKVy
+OtQa8L9GAFgr5fSI/VhOSdvNILSd5JEHNmszbDgNRR0PfIizHHxbLY7288kjwEPw
+pVsYjY67VYy4XTjTNP18F1dDox0YbN4zISy1Kv884bEpQBgRjXyEpwpy1obEAxnI
+Byl6ypUM2Zafq9AKUJsCRtMIPWakXUGfnHy9iUsiGSa6q6Jew1XpMgs7AAICB/9I
+2phs8YFrOxKv9lwdRFT1DLg5AlK3aOhLeDMfe4oPdcfu2YBslX2xJaZK8tq5J0YB
+JjOz+FPGlSLFdBYgcsf+Wf7fm8xhYYoaZpFbBkBwNZUNOTg0QCpJwYUqeiwjIzqo
+FciFKL+esSRdRLur6C80Knepl4LHuOZAcjKeukHzUxjISZICIvSNzJe/FWoa4p7J
+oJuG2ScUXWhae0OF2ulWHmN0lw/c3NDkysTuwaVHl3jm9xL/8aLXAblzQqAuuOr/
+7V1/wvU92Ir31b2+BIRb6SLSjKdZfoyCQd19KBP5DjJtV3/mMLEvT0eMhMp9NdaC
+Z/DQtQ0jYYgDwvv2yGogiEYEGBECAAYFAjZlGC8ACgkQf6wiK/H8DBIhHgCdFfHu
+2whpwaX1/YaS03SxvtT8hegAnRSdQ88UIcXYj2PkFOiuFPrFBvee
+=crZ+
+-----END PGP PUBLIC KEY BLOCK-----
+
pub 7FE9900F412D622E
uid Wouter van Oortmerssen <[email protected]>
sub AE6B5325E74ED034
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
mQENBFnyVlkBCACe8zGkIlDV0dUKmk9PWe2Hw8qM9DdPbtpUOpmUOidGY5svQDL3
eqvHk85TbxqFEe3Qbjjt+R+iApFuXy5kmueXTvwCm7nAU+k/pZtPuzHyhDs3iFFH
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 2d28c739..3cbe2f3 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -121,6 +121,7 @@
<trusted-key id="1F47744C9B6E14F2049C2857F1F111AF65925306" group="io.github.classgraph"/>
<trusted-key id="1FA37FBE4453C1073E7EF61D6449005F96BC97A3" group="de.undercouch"/>
<trusted-key id="20723A6399BC060154283B37CFAE163B64AC9189" group="org.jetbrains.skiko"/>
+ <trusted-key id="217BFC8CA98788CB33198A8E7FAC222BF1FC0C12" group="net.thauvin.erik.urlencoder"/>
<trusted-key id="22B79F456B06F4E75B8B579DB57BD58EF6D0A713" group="com.google.protobuf"/>
<trusted-key id="24D04176586361FDA94EE0315F7786DF73E61F56" group="com.google.devtools.ksp"/>
<trusted-key id="26063B04869F7D235CCC057447586A1B75EF0DE5" group="com.squareup.wire"/>
@@ -310,6 +311,7 @@
<trusted-key id="7FE5E98DF3A5C0DC34663AB7C1ADD37CA0069309" group="org.spdx" name="spdx-gradle-plugin"/>
<trusted-key id="808D78B17A5A2D7C3668E31FBFFC9B54721244AD" group="org.apache.commons"/>
<trusted-key id="80F6D6B0D90C6747753344CAB5A9E81B565E89E0" group="org.tomlj"/>
+ <trusted-key id="8247F88933C2D56F830AF73436AB7ACFFF2027B1" group="it.krzeminski"/>
<trusted-key id="8254180BFC943B816E0B5E2E5E2F2B3D474EFE6B" group="it.unimi.dsi"/>
<trusted-key id="82C9EC0E52C47A936A849E0113D979595E6D01E1" group="org.apache.maven.shared" name="maven-shared-utils"/>
<trusted-key id="82F833963889D7ED06F1E4DC6525FD70CC303655" group="org.codehaus.mojo"/>
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
index 9ba4c00..86da8db 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
@@ -77,9 +77,7 @@
@Test
fun testSurfaceControlCompatBuilder_parent() {
val callbackLatch = CountDownLatch(1)
- val scenario =
- ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
+ val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
try {
scenario.onActivity {
@@ -97,7 +95,7 @@
it.addSurface(it.getSurfaceView(), callback)
}
- scenario.moveToState(Lifecycle.State.RESUMED)
+
assertTrue(callbackLatch.await(3000, TimeUnit.MILLISECONDS))
} catch (e: java.lang.IllegalArgumentException) {
fail()
@@ -110,9 +108,7 @@
@Test
fun testSurfaceControlCompatBuilder_parentSurfaceControl() {
val callbackLatch = CountDownLatch(1)
- val scenario =
- ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
+ val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
try {
scenario.onActivity {
@@ -136,7 +132,6 @@
it.addSurface(it.getSurfaceView(), callback)
}
- scenario.moveToState(Lifecycle.State.RESUMED)
assertTrue(callbackLatch.await(3000, TimeUnit.MILLISECONDS))
} catch (e: java.lang.IllegalArgumentException) {
fail()
@@ -170,9 +165,7 @@
fun testSurfaceTransactionOnCommitCallback() {
val listener = TransactionOnCommitListener()
- val scenario =
- ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
+ val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
try {
scenario.onActivity {
@@ -180,7 +173,6 @@
.addTransactionCommittedListener(executor!!, listener)
.commit()
}
- scenario.moveToState(Lifecycle.State.RESUMED)
listener.mLatch.await(3, TimeUnit.SECONDS)
assertEquals(0, listener.mLatch.count)
@@ -197,9 +189,7 @@
val listener = TransactionOnCommitListener()
val listener2 = TransactionOnCommitListener()
- val scenario =
- ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
+ val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
try {
scenario.onActivity {
@@ -209,8 +199,6 @@
.commit()
}
- scenario.moveToState(Lifecycle.State.RESUMED)
-
listener.mLatch.await(3, TimeUnit.SECONDS)
listener2.mLatch.await(3, TimeUnit.SECONDS)
@@ -228,9 +216,7 @@
@Test
fun testSurfaceControlIsValid_valid() {
val callbackLatch = CountDownLatch(1)
- val scenario =
- ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
+ val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
try {
scenario.onActivity {
val callback =
@@ -250,7 +236,6 @@
it.addSurface(it.mSurfaceView, callback)
}
- scenario.moveToState(Lifecycle.State.RESUMED)
assertTrue(callbackLatch.await(3000, TimeUnit.MILLISECONDS))
} catch (e: java.lang.IllegalArgumentException) {
fail()
@@ -263,9 +248,7 @@
@Test
fun testSurfaceControlIsValid_validNotValid() {
val callbackLatch = CountDownLatch(1)
- val scenario =
- ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
+ val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
try {
scenario.onActivity {
val callback =
@@ -287,8 +270,6 @@
it.addSurface(it.mSurfaceView, callback)
}
-
- scenario.moveToState(Lifecycle.State.RESUMED)
assertTrue(callbackLatch.await(3000, TimeUnit.MILLISECONDS))
} catch (e: java.lang.IllegalArgumentException) {
fail()
@@ -301,9 +282,7 @@
@Test
fun testSurfaceControlIsValid_multipleReleases() {
val callbackLatch = CountDownLatch(1)
- val scenario =
- ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
+ val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
try {
scenario.onActivity {
val callback =
@@ -327,7 +306,6 @@
it.addSurface(it.mSurfaceView, callback)
}
- scenario.moveToState(Lifecycle.State.RESUMED)
assertTrue(callbackLatch.await(3000, TimeUnit.MILLISECONDS))
} catch (e: java.lang.IllegalArgumentException) {
fail()
@@ -1220,41 +1198,37 @@
var scCompat: SurfaceControlCompat? = null
var surfaceView: SurfaceView? = null
val scenario =
- ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
- .onActivity {
- it.setDestroyCallback { destroyLatch.countDown() }
- surfaceView = it.mSurfaceView
- val callback =
- object : SurfaceHolderCallback() {
- override fun surfaceCreated(sh: SurfaceHolder) {
- scCompat =
- SurfaceControlCompat.Builder()
- .setParent(it.getSurfaceView())
- .setName("SurfaceControlCompatTest")
- .build()
+ ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java).onActivity {
+ it.setDestroyCallback { destroyLatch.countDown() }
+ surfaceView = it.mSurfaceView
+ val callback =
+ object : SurfaceHolderCallback() {
+ override fun surfaceCreated(sh: SurfaceHolder) {
+ scCompat =
+ SurfaceControlCompat.Builder()
+ .setParent(it.getSurfaceView())
+ .setName("SurfaceControlCompatTest")
+ .build()
- // Buffer colorspace is RGBA, so Color.BLUE will be visually Red
- val buffer =
- SurfaceControlUtils.getSolidBuffer(
- SurfaceControlWrapperTestActivity.DEFAULT_WIDTH,
- SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT,
- Color.BLUE
- )
+ // Buffer colorspace is RGBA, so Color.BLUE will be visually Red
+ val buffer =
+ SurfaceControlUtils.getSolidBuffer(
+ SurfaceControlWrapperTestActivity.DEFAULT_WIDTH,
+ SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT,
+ Color.BLUE
+ )
- SurfaceControlCompat.Transaction()
- .addTransactionCommittedListener(executor!!, listener)
- .setBuffer(scCompat!!, buffer)
- .setVisibility(scCompat!!, true)
- .setCrop(scCompat!!, Rect(20, 30, 90, 60))
- .commit()
- }
+ SurfaceControlCompat.Transaction()
+ .addTransactionCommittedListener(executor!!, listener)
+ .setBuffer(scCompat!!, buffer)
+ .setVisibility(scCompat!!, true)
+ .setCrop(scCompat!!, Rect(20, 30, 90, 60))
+ .commit()
}
+ }
- it.addSurface(it.mSurfaceView, callback)
- }
-
- scenario.moveToState(Lifecycle.State.RESUMED)
+ it.addSurface(it.mSurfaceView, callback)
+ }
assertTrue(listener.mLatch.await(3000, TimeUnit.MILLISECONDS))
@@ -1568,49 +1542,43 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
@Test
fun testClearFrameRate() {
- ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
- .onActivity {
- val callback =
- object : SurfaceHolderCallback() {
- override fun surfaceCreated(sh: SurfaceHolder) {
+ ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java).onActivity {
+ val callback =
+ object : SurfaceHolderCallback() {
+ override fun surfaceCreated(sh: SurfaceHolder) {
- val surfaceControl =
- SurfaceControlCompat.Builder()
- .setName("testSurfaceControl")
- .setParent(it.mSurfaceView)
- .build()
- SurfaceControlCompat.Transaction()
- .clearFrameRate(surfaceControl)
- .commit()
- }
+ val surfaceControl =
+ SurfaceControlCompat.Builder()
+ .setName("testSurfaceControl")
+ .setParent(it.mSurfaceView)
+ .build()
+ SurfaceControlCompat.Transaction().clearFrameRate(surfaceControl).commit()
}
+ }
- it.addSurface(it.mSurfaceView, callback)
- }
+ it.addSurface(it.mSurfaceView, callback)
+ }
}
private fun testFrameRate(frameRate: Float, compatibility: Int, strategy: Int) {
- ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
- .onActivity {
- val callback =
- object : SurfaceHolderCallback() {
- override fun surfaceCreated(sh: SurfaceHolder) {
+ ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java).onActivity {
+ val callback =
+ object : SurfaceHolderCallback() {
+ override fun surfaceCreated(sh: SurfaceHolder) {
- val surfaceControl =
- SurfaceControlCompat.Builder()
- .setName("testSurfaceControl")
- .setParent(it.mSurfaceView)
- .build()
- SurfaceControlCompat.Transaction()
- .setFrameRate(surfaceControl, frameRate, compatibility, strategy)
- .commit()
- }
+ val surfaceControl =
+ SurfaceControlCompat.Builder()
+ .setName("testSurfaceControl")
+ .setParent(it.mSurfaceView)
+ .build()
+ SurfaceControlCompat.Transaction()
+ .setFrameRate(surfaceControl, frameRate, compatibility, strategy)
+ .commit()
}
+ }
- it.addSurface(it.mSurfaceView, callback)
- }
+ it.addSurface(it.mSurfaceView, callback)
+ }
}
@SuppressLint("NewApi")
@@ -1653,86 +1621,84 @@
fun testSetExtendedRangeBrightness() {
val destroyLatch = CountDownLatch(1)
val scenario =
- ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
- .onActivity {
- it.setDestroyCallback { destroyLatch.countDown() }
- val display = it.display
- assertNotNull(display)
- if (display!!.isHdrSdrRatioAvailable) {
- assertEquals(1.0f, display.hdrSdrRatio, .0001f)
- }
+ ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java).onActivity {
+ it.setDestroyCallback { destroyLatch.countDown() }
+ val display = it.display
+ assertNotNull(display)
+ if (display!!.isHdrSdrRatioAvailable) {
+ assertEquals(1.0f, display.hdrSdrRatio, .0001f)
+ }
- it.window.attributes.screenBrightness = 0.01f
- val hdrReady = CountDownLatch(1)
- val listenerErrors = arrayOfNulls<Exception>(1)
- if (display.isHdrSdrRatioAvailable) {
- display.registerHdrSdrRatioChangedListener(
- executor!!,
- object : Consumer<Display?> {
- var mIsRegistered = true
+ it.window.attributes.screenBrightness = 0.01f
+ val hdrReady = CountDownLatch(1)
+ val listenerErrors = arrayOfNulls<Exception>(1)
+ if (display.isHdrSdrRatioAvailable) {
+ display.registerHdrSdrRatioChangedListener(
+ executor!!,
+ object : Consumer<Display?> {
+ var mIsRegistered = true
- override fun accept(updatedDisplay: Display?) {
- try {
- assertEquals(display.displayId, updatedDisplay!!.displayId)
- assertTrue(mIsRegistered)
- if (display.hdrSdrRatio > 2f) {
- hdrReady.countDown()
- display.unregisterHdrSdrRatioChangedListener(this)
- mIsRegistered = false
- }
- } catch (e: Exception) {
- synchronized(it) {
- listenerErrors[0] = e
- hdrReady.countDown()
- }
+ override fun accept(updatedDisplay: Display?) {
+ try {
+ assertEquals(display.displayId, updatedDisplay!!.displayId)
+ assertTrue(mIsRegistered)
+ if (display.hdrSdrRatio > 2f) {
+ hdrReady.countDown()
+ display.unregisterHdrSdrRatioChangedListener(this)
+ mIsRegistered = false
+ }
+ } catch (e: Exception) {
+ synchronized(it) {
+ listenerErrors[0] = e
+ hdrReady.countDown()
}
}
}
+ }
+ )
+ } else {
+ assertThrows(IllegalStateException::class.java) {
+ display.registerHdrSdrRatioChangedListener(
+ executor!!,
+ Consumer { _: Display? -> }
)
- } else {
- assertThrows(IllegalStateException::class.java) {
- display.registerHdrSdrRatioChangedListener(
- executor!!,
- Consumer { _: Display? -> }
- )
+ }
+ }
+ val extendedDataspace =
+ DataSpace.pack(
+ DataSpace.STANDARD_BT709,
+ DataSpace.TRANSFER_SRGB,
+ DataSpace.RANGE_EXTENDED
+ )
+ val buffer =
+ getSolidBuffer(
+ SurfaceControlWrapperTestActivity.DEFAULT_WIDTH,
+ SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT,
+ Color.RED
+ )
+ val callback =
+ object : SurfaceHolderCallback() {
+ override fun surfaceCreated(sh: SurfaceHolder) {
+ val scCompat =
+ SurfaceControlCompat.Builder()
+ .setParent(it.getSurfaceView())
+ .setName("SurfaceControlCompatTest")
+ .build()
+
+ SurfaceControlCompat.Transaction()
+ .setBuffer(scCompat, buffer)
+ .setDataSpace(scCompat, extendedDataspace)
+ .setExtendedRangeBrightness(scCompat, 1.0f, 3.0f)
+ .setVisibility(scCompat, true)
+ .commit()
}
}
- val extendedDataspace =
- DataSpace.pack(
- DataSpace.STANDARD_BT709,
- DataSpace.TRANSFER_SRGB,
- DataSpace.RANGE_EXTENDED
- )
- val buffer =
- getSolidBuffer(
- SurfaceControlWrapperTestActivity.DEFAULT_WIDTH,
- SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT,
- Color.RED
- )
- val callback =
- object : SurfaceHolderCallback() {
- override fun surfaceCreated(sh: SurfaceHolder) {
- val scCompat =
- SurfaceControlCompat.Builder()
- .setParent(it.getSurfaceView())
- .setName("SurfaceControlCompatTest")
- .build()
- SurfaceControlCompat.Transaction()
- .setBuffer(scCompat, buffer)
- .setDataSpace(scCompat, extendedDataspace)
- .setExtendedRangeBrightness(scCompat, 1.0f, 3.0f)
- .setVisibility(scCompat, true)
- .commit()
- }
- }
-
- it.addSurface(it.mSurfaceView, callback)
- }
+ it.addSurface(it.mSurfaceView, callback)
+ }
try {
- scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+ scenario.onActivity {
SurfaceControlUtils.validateOutput(it.window) { bitmap ->
val coord = intArrayOf(0, 0)
it.mSurfaceView.getLocationInWindow(coord)
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt
index 598c67e..2110acd 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt
@@ -70,13 +70,15 @@
@JvmOverloads
constructor(
surfaceView: SurfaceView,
- private val callback: Callback<T>,
+ callback: Callback<T>,
@HardwareBufferFormat val bufferFormat: Int = HardwareBuffer.RGBA_8888
) {
/** Target SurfaceView for rendering */
private var mSurfaceView: SurfaceView? = null
+ private var mCallback: Callback<T>? = null
+
/**
* Executor used to deliver callbacks for rendering as well as issuing surface control
* transactions
@@ -185,6 +187,7 @@
init {
mSurfaceView = surfaceView
+ mCallback = callback
surfaceView.holder.addCallback(mHolderCallback)
with(surfaceView.holder) {
if (surface != null && surface.isValid) {
@@ -253,7 +256,7 @@
}
canvas.drawColor(Color.BLACK, BlendMode.CLEAR)
}
- callback.onDrawFrontBufferedLayer(canvas, width, height, param)
+ mCallback?.onDrawFrontBufferedLayer(canvas, width, height, param)
}
@SuppressLint("WrongConstant")
@@ -293,7 +296,7 @@
transformHint
)
}
- callback.onFrontBufferedLayerRenderComplete(
+ mCallback?.onFrontBufferedLayerRenderComplete(
frontBufferSurfaceControl,
transaction
)
@@ -460,7 +463,7 @@
if (transform != BufferTransformHintResolver.UNKNOWN_TRANSFORM) {
transaction.setBufferTransform(parentSurfaceControl, transform)
}
- callback.onMultiBufferedLayerRenderComplete(
+ mCallback?.onMultiBufferedLayerRenderComplete(
frontBufferSurfaceControl,
parentSurfaceControl,
transaction
@@ -566,7 +569,7 @@
with(multiBufferedRenderer) {
mMultiBufferedRenderNode?.let { renderNode ->
val canvas = renderNode.beginRecording()
- callback.onDrawMultiBufferedLayer(canvas, width, height, params)
+ mCallback?.onDrawMultiBufferedLayer(canvas, width, height, params)
renderNode.endRecording()
}
@@ -671,6 +674,7 @@
mSurfaceView?.holder?.removeCallback(mHolderCallback)
mSurfaceView = null
releaseInternal(cancelPending) {
+ mCallback = null
onReleaseComplete?.invoke()
mHandlerThread.quit()
}
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
index b06c22c..182f845 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
@@ -530,12 +530,13 @@
@ChangeFrameRateStrategy changeFrameRateStrategy: Int
): Transaction {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- mImpl.setFrameRate(
- surfaceControl.scImpl,
- frameRate,
- compatibility,
- changeFrameRateStrategy
- )
+ val strategy =
+ when (changeFrameRateStrategy) {
+ CHANGE_FRAME_RATE_ALWAYS -> changeFrameRateStrategy
+ CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS -> changeFrameRateStrategy
+ else -> CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS
+ }
+ mImpl.setFrameRate(surfaceControl.scImpl, frameRate, compatibility, strategy)
}
return this
}
diff --git a/health/connect/connect-client/build.gradle b/health/connect/connect-client/build.gradle
index 9b1779f..b212af5 100644
--- a/health/connect/connect-client/build.gradle
+++ b/health/connect/connect-client/build.gradle
@@ -77,8 +77,7 @@
}
testOptions.unitTests.includeAndroidResources = true
namespace "androidx.health.connect.client"
- compileSdk = 34
- compileSdkExtension = 10
+ compileSdk = 35
// TODO(b/352609562): Typedef with `toLong()`
experimentalProperties["android.lint.useK2Uast"] = false
}
diff --git a/health/connect/connect-client/samples/build.gradle b/health/connect/connect-client/samples/build.gradle
index 08b7839..fb7bcef 100644
--- a/health/connect/connect-client/samples/build.gradle
+++ b/health/connect/connect-client/samples/build.gradle
@@ -51,4 +51,5 @@
defaultConfig {
minSdkVersion 26
}
+ compileSdk = 35
}
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
index 14f303c..ee854d9 100644
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
@@ -95,7 +95,7 @@
context.packageName,
PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong())
)
- .requestedPermissions
+ .requestedPermissions!!
.filter { it.startsWith(PERMISSION_PREFIX) }
.toTypedArray()
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt
index 05fe60c..653ea0c 100644
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt
@@ -78,7 +78,7 @@
context.packageName,
PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong())
)
- .requestedPermissions
+ .requestedPermissions!!
.filter { it.startsWith(PERMISSION_PREFIX) }
.toTypedArray()
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
index a03c963..e38be6e 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
@@ -312,7 +312,7 @@
@Suppress("Deprecation")
packageInfo.versionCode = versionCode
packageInfo.applicationInfo = ApplicationInfo()
- packageInfo.applicationInfo.enabled = enabled
+ packageInfo.applicationInfo!!.enabled = enabled
val packageManager = context.packageManager
Shadows.shadowOf(packageManager).installPackage(packageInfo)
}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/feature/HealthConnectApkImplTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/feature/HealthConnectApkImplTest.kt
index dea6c6b..47d6fb9 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/feature/HealthConnectApkImplTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/feature/HealthConnectApkImplTest.kt
@@ -121,7 +121,7 @@
@Suppress("Deprecation")
packageInfo.versionCode = versionCode
packageInfo.applicationInfo = ApplicationInfo()
- packageInfo.applicationInfo.enabled = true
+ packageInfo.applicationInfo!!.enabled = true
val packageManager = context.packageManager
Shadows.shadowOf(packageManager).installPackage(packageInfo)
}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
index f1480f8..2bb9f0c 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
@@ -841,7 +841,7 @@
val packageInfo = PackageInfo()
packageInfo.packageName = packageName
packageInfo.applicationInfo = ApplicationInfo()
- packageInfo.applicationInfo.enabled = enabled
+ packageInfo.applicationInfo!!.enabled = enabled
val packageManager = context.packageManager
Shadows.shadowOf(packageManager).installPackage(packageInfo)
}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt
index 609c83a5..ff2ea5c 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt
@@ -186,7 +186,7 @@
val packageInfo = PackageInfo()
packageInfo.packageName = packageName
packageInfo.applicationInfo = ApplicationInfo()
- packageInfo.applicationInfo.enabled = enabled
+ packageInfo.applicationInfo!!.enabled = enabled
val packageManager = context.packageManager
shadowOf(packageManager).installPackage(packageInfo)
}
diff --git a/health/connect/connect-testing/build.gradle b/health/connect/connect-testing/build.gradle
index 9439dd3..1c1c2b5 100644
--- a/health/connect/connect-testing/build.gradle
+++ b/health/connect/connect-testing/build.gradle
@@ -47,8 +47,7 @@
}
namespace "androidx.health.connect.testing"
testOptions.unitTests.includeAndroidResources = true
- compileSdk = 34
- compileSdkExtension = 10
+ compileSdk = 35
}
androidx {
diff --git a/health/connect/connect-testing/samples/build.gradle b/health/connect/connect-testing/samples/build.gradle
index c93f7cc..a9cb0e5 100644
--- a/health/connect/connect-testing/samples/build.gradle
+++ b/health/connect/connect-testing/samples/build.gradle
@@ -51,6 +51,7 @@
defaultConfig {
minSdkVersion 26
}
+ compileSdk = 35
}
tasks.withType(KotlinCompile).configureEach {
diff --git a/ink/ink-brush/api/current.txt b/ink/ink-brush/api/current.txt
index 7582fcf..7599e2b 100644
--- a/ink/ink-brush/api/current.txt
+++ b/ink/ink-brush/api/current.txt
@@ -57,6 +57,16 @@
public static final class BrushFamily.Companion {
}
+ public final class BrushUtil {
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush copyWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color, optional androidx.ink.brush.BrushFamily family, optional float size, optional float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static android.graphics.Color createAndroidColor(androidx.ink.brush.Brush);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder createBuilderWithAndroidColor(android.graphics.Color color);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.Brush.Companion, androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder setAndroidColor(androidx.ink.brush.Brush.Builder, android.graphics.Color color);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder toBuilderWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color);
+ }
+
@SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ExperimentalInkCustomBrushApi {
}
diff --git a/ink/ink-brush/api/restricted_current.txt b/ink/ink-brush/api/restricted_current.txt
index 7582fcf..7599e2b 100644
--- a/ink/ink-brush/api/restricted_current.txt
+++ b/ink/ink-brush/api/restricted_current.txt
@@ -57,6 +57,16 @@
public static final class BrushFamily.Companion {
}
+ public final class BrushUtil {
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush copyWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color, optional androidx.ink.brush.BrushFamily family, optional float size, optional float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static android.graphics.Color createAndroidColor(androidx.ink.brush.Brush);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder createBuilderWithAndroidColor(android.graphics.Color color);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.Brush.Companion, androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder setAndroidColor(androidx.ink.brush.Brush.Builder, android.graphics.Color color);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder toBuilderWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color);
+ }
+
@SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ExperimentalInkCustomBrushApi {
}
diff --git a/ink/ink-brush/build.gradle b/ink/ink-brush/build.gradle
index 2483290..4679548 100644
--- a/ink/ink-brush/build.gradle
+++ b/ink/ink-brush/build.gradle
@@ -100,5 +100,4 @@
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2024"
description = "Define brushes for freehand input."
- metalavaK2UastEnabled = false
}
diff --git a/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt b/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
index b23e689..1a277fe 100644
--- a/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
+++ b/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
@@ -41,7 +41,7 @@
private val testFamily = BrushFamily(uri = "/brush-family:pencil")
@Test
- fun brushGetAndroidColor_getsCorrectColor() {
+ fun brushCreateAndroidColor_getsCorrectColor() {
val brush = Brush.createWithColorLong(testFamily, testColorLong, 1f, 1f)
// Note that expectedColor is not necessarily the same as testColor, because of precision
@@ -50,7 +50,7 @@
// the
// color internally as a ColorLong anyway).
val expectedColor = AndroidColor.valueOf(testColorLong)
- assertThat(brush.getAndroidColor()).isEqualTo(expectedColor)
+ assertThat(brush.createAndroidColor()).isEqualTo(expectedColor)
}
@Test
@@ -97,7 +97,7 @@
}
@Test
- fun brushBuilderAndroidColor_setsColor() {
+ fun brushBuilderSetAndroidColor_setsColor() {
val brush =
Brush.builder()
.setFamily(testFamily)
@@ -110,7 +110,7 @@
}
@Test
- fun brushBuilderAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+ fun brushBuilderSetAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
val brush =
Brush.builder()
@@ -126,13 +126,13 @@
}
@Test
- fun brushWithAndroidColor_createsBrushWithColor() {
+ fun brushCreateWithAndroidColor_createsBrushWithColor() {
val brush = Brush.createWithAndroidColor(testFamily, testColor, 1f, 1f)
assertThat(brush.colorLong).isEqualTo(testColorLong)
}
@Test
- fun brushWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
+ fun brushCreateWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
val brush = Brush.createWithAndroidColor(testFamily, unsupportedColor, 1f, 1f)
@@ -142,21 +142,10 @@
}
@Test
- fun brushUtilGetAndroidColor_getsCorrectColor() {
- val brush = Brush.createWithColorLong(testFamily, testColorLong, 1f, 1f)
-
- // Note that expectedColor is not necessarily the same as testColor, because of precision
- // loss
- // when converting from testColor to testColorLong.
- val expectedColor = AndroidColor.valueOf(testColorLong)
- assertThat(BrushUtil.getAndroidColor(brush)).isEqualTo(expectedColor)
- }
-
- @Test
- fun brushUtilToBuilderWithAndroidColor_setsColor() {
+ fun brushToBuilderWithAndroidColor_setsColor() {
val brush = Brush.createWithColorIntArgb(testFamily, 0x4499bb66, 2f, 0.2f)
- val newBrush = BrushUtil.toBuilderWithAndroidColor(brush, testColor).build()
+ val newBrush = brush.toBuilderWithAndroidColor(testColor).build()
assertThat(newBrush.colorLong).isEqualTo(testColorLong)
assertThat(brush.family).isEqualTo(testFamily)
@@ -165,11 +154,11 @@
}
@Test
- fun brushUtilToBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+ fun brushToBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
val brush = Brush.createWithColorIntArgb(testFamily, 0x4499bb66, 2f, 0.2f)
val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
- val newBrush = BrushUtil.toBuilderWithAndroidColor(brush, unsupportedColor).build()
+ val newBrush = brush.toBuilderWithAndroidColor(unsupportedColor).build()
// unsupportedColor gets converted to ColorLong (losing precision) and then to Display P3.
val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
@@ -181,9 +170,9 @@
}
@Test
- fun brushUtilMakeBuilderWithAndroidColor_setsColor() {
+ fun createBrushBuilderWithAndroidColor_setsColor() {
val brush =
- BrushUtil.createBuilderWithAndroidColor(testColor)
+ createBrushBuilderWithAndroidColor(testColor)
.setFamily(testFamily)
.setSize(2f)
.setEpsilon(0.2f)
@@ -196,10 +185,10 @@
}
@Test
- fun brushUtilMakeBuilderAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+ fun createBrushBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
val brush =
- BrushUtil.createBuilderWithAndroidColor(unsupportedColor)
+ createBrushBuilderWithAndroidColor(unsupportedColor)
.setFamily(testFamily)
.setSize(2f)
.setEpsilon(0.2f)
@@ -209,20 +198,4 @@
val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
assertThat(brush.colorLong).isEqualTo(expectedColor.pack())
}
-
- @Test
- fun brushUtilMakeBrushWithAndroidColor_createsBrushWithColor() {
- val brush = BrushUtil.createWithAndroidColor(testFamily, testColor, 1f, 1f)
- assertThat(brush.colorLong).isEqualTo(testColorLong)
- }
-
- @Test
- fun brushUtilMakeBrushWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
- val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
- val brush = BrushUtil.createWithAndroidColor(testFamily, unsupportedColor, 1f, 1f)
-
- // unsupportedColor gets converted to ColorLong (losing precision) and then to Display P3.
- val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
- assertThat(brush.colorLong).isEqualTo(expectedColor.pack())
- }
}
diff --git a/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt b/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
index a1a5bdc..ff89d66 100644
--- a/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
+++ b/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+@file:JvmName("BrushUtil")
package androidx.ink.brush
@@ -22,17 +22,20 @@
import android.os.Build
import androidx.annotation.CheckResult
import androidx.annotation.RequiresApi
-import androidx.annotation.RestrictTo
/**
* The brush color as an [android.graphics.Color] instance, which can express colors in several
* different color spaces. sRGB and Display P3 are supported; a color in any other color space will
* be converted to Display P3.
+ *
+ * Unless an instance of [android.graphics.Color] is actually needed, prefer to use
+ * [Brush.colorLong] to get the color without causing an allocation, especially in
+ * performance-sensitive code. [Brush.colorLong] is fully compatible with the [Long] representation
+ * of [android.graphics.Color].
*/
-@JvmSynthetic
@CheckResult
@RequiresApi(Build.VERSION_CODES.O)
-public fun Brush.getAndroidColor(): AndroidColor = BrushUtil.getAndroidColor(this)
+public fun Brush.createAndroidColor(): AndroidColor = AndroidColor.valueOf(colorLong)
/**
* Creates a copy of `this` [Brush] and allows named properties to be altered while keeping the rest
@@ -40,7 +43,6 @@
* several different color spaces. sRGB and Display P3 are supported; a color in any other color
* space will be converted to Display P3.
*/
-@JvmSynthetic
@CheckResult
@RequiresApi(Build.VERSION_CODES.O)
public fun Brush.copyWithAndroidColor(
@@ -53,19 +55,53 @@
/**
* Set the color on a [Brush.Builder] as an [android.graphics.Color] instance. sRGB and Display P3
* are supported; a color in any other color space will be converted to Display P3.
+ *
+ * Java callers should prefer [toBuilderWithAndroidColor] or [createBrushBuilderWithAndroidColor] as
+ * a more fluent API.
*/
-@JvmSynthetic
@CheckResult
@RequiresApi(Build.VERSION_CODES.O)
public fun Brush.Builder.setAndroidColor(color: AndroidColor): Brush.Builder =
setColorLong(color.pack())
/**
+ * Returns a [Brush.Builder] with values set equivalent to the [Brush] and the color specified by an
+ * [android.graphics.Color] instance, which can encode several different color spaces. sRGB and
+ * Display P3 are supported; a color in any other color space will be converted to Display P3. Java
+ * developers, use the returned builder to build a copy of a Brush. Kotlin developers, see
+ * [copyWithAndroidColor] method.
+ *
+ * In Kotlin, calling this is equivalent to calling [Brush.toBuilder] followed by
+ * [Brush.Builder.setAndroidColor]. For Java callers, this function allows more fluent call
+ * chaining.
+ */
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun Brush.toBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
+ toBuilder().setAndroidColor(color)
+
+/**
+ * Returns a new, blank [Brush.Builder] with the color specified by an [android.graphics.Color]
+ * instance, which can encode several different color spaces. sRGB and Display P3 are supported; a
+ * color in any other color space will be converted to Display P3.
+ *
+ * In Kotlin, calling this is equivalent to calling [Brush.builder] followed by
+ * [Brush.Builder.setAndroidColor]. For Java callers, this function allows more fluent call
+ * chaining.
+ */
+@JvmName("createBuilderWithAndroidColor")
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun createBrushBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
+ Brush.Builder().setAndroidColor(color)
+
+/**
* Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which can
* encode several different color spaces. sRGB and Display P3 are supported; a color in any other
* color space will be converted to Display P3.
+ *
+ * Java callers should prefer `BrushUtil.createWithAndroidColor` ([createBrushWithAndroidColor]).
*/
-@JvmSynthetic
@CheckResult
@RequiresApi(Build.VERSION_CODES.O)
public fun Brush.Companion.createWithAndroidColor(
@@ -73,57 +109,21 @@
color: AndroidColor,
size: Float,
epsilon: Float,
-): Brush = BrushUtil.createWithAndroidColor(family, color, size, epsilon)
+): Brush = createWithColorLong(family, color.pack(), size, epsilon)
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public object BrushUtil {
-
- /**
- * The brush color as an [android.graphics.Color] instance, which can express colors in several
- * different color spaces. sRGB and Display P3 are supported; a color in any other color space
- * will be converted to Display P3.
- */
- @JvmStatic
- @CheckResult
- @RequiresApi(Build.VERSION_CODES.O)
- public fun getAndroidColor(brush: Brush): AndroidColor = AndroidColor.valueOf(brush.colorLong)
-
- /**
- * Returns a [Brush.Builder] with values set equivalent to [brush] and the color specified by an
- * [android.graphics.Color] instance, which can encode several different color spaces. sRGB and
- * Display P3 are supported; a color in any other color space will be converted to Display P3.
- * Java developers, use the returned builder to build a copy of a Brush. Kotlin developers, see
- * [copyWithAndroidColor] method.
- */
- @JvmStatic
- @CheckResult
- @RequiresApi(Build.VERSION_CODES.O)
- public fun toBuilderWithAndroidColor(brush: Brush, color: AndroidColor): Brush.Builder =
- brush.toBuilder().setAndroidColor(color)
-
- /**
- * Returns a new [Brush.Builder] with the color specified by an [android.graphics.Color]
- * instance, which can encode several different color spaces. sRGB and Display P3 are supported;
- * a color in any other color space will be converted to Display P3.
- */
- @JvmStatic
- @CheckResult
- @RequiresApi(Build.VERSION_CODES.O)
- public fun createBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
- Brush.Builder().setAndroidColor(color)
-
- /**
- * Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which
- * can encode several different color spaces. sRGB and Display P3 are supported; a color in any
- * other color space will be converted to Display P3.
- */
- @JvmStatic
- @CheckResult
- @RequiresApi(Build.VERSION_CODES.O)
- public fun createWithAndroidColor(
- family: BrushFamily,
- color: AndroidColor,
- size: Float,
- epsilon: Float,
- ): Brush = Brush.createWithColorLong(family, color.pack(), size, epsilon)
-}
+/**
+ * Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which can
+ * encode several different color spaces. sRGB and Display P3 are supported; a color in any other
+ * color space will be converted to Display P3.
+ *
+ * Kotlin callers should prefer [Brush.Companion.createWithAndroidColor].
+ */
+@JvmName("createWithAndroidColor")
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun createBrushWithAndroidColor(
+ family: BrushFamily,
+ color: AndroidColor,
+ size: Float,
+ epsilon: Float,
+): Brush = Brush.createWithAndroidColor(family, color, size, epsilon)
diff --git a/ink/ink-geometry/api/current.txt b/ink/ink-geometry/api/current.txt
index 7d352f46..8901f35 100644
--- a/ink/ink-geometry/api/current.txt
+++ b/ink/ink-geometry/api/current.txt
@@ -65,6 +65,7 @@
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Box? box);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.BoxAccumulator? other);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Parallelogram parallelogram);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.PartitionedMesh mesh);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Segment segment);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Triangle triangle);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Vec point);
@@ -172,26 +173,37 @@
public final class Intersection {
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Box other);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToBox);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Parallelogram other);
+ method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToParallelogram);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Vec point);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Box box, androidx.ink.geometry.AffineTransform meshToBox);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Parallelogram parallelogram, androidx.ink.geometry.AffineTransform meshToParallelogram);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.PartitionedMesh other, androidx.ink.geometry.AffineTransform thisToCommonTransForm, androidx.ink.geometry.AffineTransform otherToCommonTransform);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Segment segment, androidx.ink.geometry.AffineTransform meshToSegment);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Triangle triangle, androidx.ink.geometry.AffineTransform meshToTriangle);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Vec point, androidx.ink.geometry.AffineTransform meshToPoint);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToSegment);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Segment other);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToTriangle);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Triangle other);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToPoint);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Vec other);
@@ -313,6 +325,36 @@
public static final class Parallelogram.Companion {
}
+ public final class PartitionedMesh {
+ method public androidx.ink.geometry.Box? computeBoundingBox();
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box, optional androidx.ink.geometry.AffineTransform boxToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle, optional androidx.ink.geometry.AffineTransform triangleToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold, optional androidx.ink.geometry.AffineTransform boxToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold, optional androidx.ink.geometry.AffineTransform triangleToThis);
+ method protected void finalize();
+ method @IntRange(from=0L) public int getOutlineCount(@IntRange(from=0L) int groupIndex);
+ method @IntRange(from=0L) public int getOutlineVertexCount(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex);
+ method @IntRange(from=0L) public int getRenderGroupCount();
+ method public void initializeSpatialIndex();
+ method public androidx.ink.geometry.MutableVec populateOutlinePosition(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex, @IntRange(from=0L) int outlineVertexIndex, androidx.ink.geometry.MutableVec outPosition);
+ field public static final androidx.ink.geometry.PartitionedMesh.Companion Companion;
+ }
+
+ public static final class PartitionedMesh.Companion {
+ }
+
public abstract class Segment {
method public final androidx.ink.geometry.ImmutableBox computeBoundingBox();
method public final androidx.ink.geometry.MutableBox computeBoundingBox(androidx.ink.geometry.MutableBox outBox);
diff --git a/ink/ink-geometry/api/restricted_current.txt b/ink/ink-geometry/api/restricted_current.txt
index 7d352f46..8901f35 100644
--- a/ink/ink-geometry/api/restricted_current.txt
+++ b/ink/ink-geometry/api/restricted_current.txt
@@ -65,6 +65,7 @@
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Box? box);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.BoxAccumulator? other);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Parallelogram parallelogram);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.PartitionedMesh mesh);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Segment segment);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Triangle triangle);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Vec point);
@@ -172,26 +173,37 @@
public final class Intersection {
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Box other);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToBox);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Parallelogram other);
+ method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToParallelogram);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Vec point);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Box box, androidx.ink.geometry.AffineTransform meshToBox);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Parallelogram parallelogram, androidx.ink.geometry.AffineTransform meshToParallelogram);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.PartitionedMesh other, androidx.ink.geometry.AffineTransform thisToCommonTransForm, androidx.ink.geometry.AffineTransform otherToCommonTransform);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Segment segment, androidx.ink.geometry.AffineTransform meshToSegment);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Triangle triangle, androidx.ink.geometry.AffineTransform meshToTriangle);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Vec point, androidx.ink.geometry.AffineTransform meshToPoint);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToSegment);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Segment other);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToTriangle);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Triangle other);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToPoint);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Vec other);
@@ -313,6 +325,36 @@
public static final class Parallelogram.Companion {
}
+ public final class PartitionedMesh {
+ method public androidx.ink.geometry.Box? computeBoundingBox();
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box, optional androidx.ink.geometry.AffineTransform boxToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle, optional androidx.ink.geometry.AffineTransform triangleToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold, optional androidx.ink.geometry.AffineTransform boxToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold, optional androidx.ink.geometry.AffineTransform triangleToThis);
+ method protected void finalize();
+ method @IntRange(from=0L) public int getOutlineCount(@IntRange(from=0L) int groupIndex);
+ method @IntRange(from=0L) public int getOutlineVertexCount(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex);
+ method @IntRange(from=0L) public int getRenderGroupCount();
+ method public void initializeSpatialIndex();
+ method public androidx.ink.geometry.MutableVec populateOutlinePosition(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex, @IntRange(from=0L) int outlineVertexIndex, androidx.ink.geometry.MutableVec outPosition);
+ field public static final androidx.ink.geometry.PartitionedMesh.Companion Companion;
+ }
+
+ public static final class PartitionedMesh.Companion {
+ }
+
public abstract class Segment {
method public final androidx.ink.geometry.ImmutableBox computeBoundingBox();
method public final androidx.ink.geometry.MutableBox computeBoundingBox(androidx.ink.geometry.MutableBox outBox);
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
index 79501d8..dde4d23 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
@@ -227,8 +227,7 @@
*
* @return `this`
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
- public fun add(mesh: PartitionedMesh): BoxAccumulator = this.add(mesh.bounds)
+ public fun add(mesh: PartitionedMesh): BoxAccumulator = this.add(mesh.computeBoundingBox())
/**
* Compares this [BoxAccumulator] with [other], and returns true if either: Both this and
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
index 1ee275d..fcdf2f5 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
@@ -16,7 +16,6 @@
package androidx.ink.geometry
-import androidx.annotation.RestrictTo
import androidx.ink.nativeloader.NativeLoader
/**
@@ -116,7 +115,6 @@
* intersection of the point in [mesh]’s object coordinates.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Vec.intersects(mesh: PartitionedMesh, meshToPoint: AffineTransform): Boolean {
return nativeMeshVecIntersects(
nativeMeshAddress = mesh.getNativeAddress(),
@@ -218,7 +216,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Segment.intersects(mesh: PartitionedMesh, meshToSegment: AffineTransform): Boolean {
return nativeMeshSegmentIntersects(
nativeMeshAddress = mesh.getNativeAddress(),
@@ -311,7 +308,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Triangle.intersects(
mesh: PartitionedMesh,
meshToTriangle: AffineTransform
@@ -382,7 +378,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Box.intersects(mesh: PartitionedMesh, meshToBox: AffineTransform): Boolean {
return nativeMeshBoxIntersects(
nativeMeshAddress = mesh.getNativeAddress(),
@@ -436,7 +431,6 @@
* checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Parallelogram.intersects(
mesh: PartitionedMesh,
meshToParallelogram: AffineTransform,
@@ -467,7 +461,6 @@
* coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(
other: PartitionedMesh,
thisToCommonTransForm: AffineTransform,
@@ -566,7 +559,6 @@
* intersection of the point in [mesh]’s object coordinates.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(point: Vec, meshToPoint: AffineTransform): Boolean =
point.intersects(this, meshToPoint)
@@ -578,7 +570,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(
segment: Segment,
meshToSegment: AffineTransform
@@ -592,7 +583,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(
triangle: Triangle,
meshToTriangle: AffineTransform,
@@ -606,7 +596,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(box: Box, meshToBox: AffineTransform): Boolean =
box.intersects(this, meshToBox)
@@ -619,7 +608,6 @@
* checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(
parallelogram: Parallelogram,
meshToParallelogram: AffineTransform,
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
index d279937..6f186f6 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
@@ -25,23 +25,22 @@
import androidx.ink.nativeloader.NativeLoader
/**
- * An immutable† complex shape expressed as a set of triangles. This is used to represent the shape
- * of a stroke or other complex objects see [MeshCreation]. The mesh may be divided into multiple
- * partitions, which enables certain brush effects (e.g. "multi-coat"), and allows ink to create
- * strokes requiring greater than 216 triangles (which must be rendered in multiple passes).
+ * An immutable** complex shape expressed as a set of triangles. This is used to represent the shape
+ * of a stroke or other complex objects. The mesh may be divided into multiple partitions, which
+ * enables certain brush effects (e.g. "multi-coat"), and allows strokes to be created using greater
+ * than 2^16 triangles (which must be rendered in multiple passes).
*
- * A PartitionedMesh may optionally have one or more "outlines", which are polylines that traverse
+ * A [PartitionedMesh] may optionally have one or more "outlines", which are polylines that traverse
* some or all of the vertices in the mesh; these are used for path-based rendering of strokes. This
* supports disjoint meshes such as dashed lines.
*
- * PartitionedMesh provides fast intersection and coverage testing by use of an internal spatial
+ * [PartitionedMesh] provides fast intersection and coverage testing by use of an internal spatial
* index.
*
- * † PartitionedMesh is technically not immutable, as the spatial index is lazily instantiated;
+ * ** [PartitionedMesh] is technically not immutable, as the spatial index is lazily instantiated;
* however, from the perspective of a caller, its properties do not change over the course of its
* lifetime. The entire object is thread-safe.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
@Suppress("NotCloseable") // Finalize is only used to free the native peer.
public class PartitionedMesh
/** Only for use within the ink library. Constructs a [PartitionedMesh] from native pointer. */
@@ -73,42 +72,53 @@
@VisibleForTesting internal constructor() : this(ModeledShapeNative.alloc())
/**
- * The number of render groups in this mesh. Each outline in the [PartitionedMesh] belongs to
- * exactly one render group, which are numbered in z-order: the group with index zero should be
- * rendered on bottom; the group with the highest index should be rendered on top.
+ * Returns the number of render groups in this mesh. Each outline in the [PartitionedMesh]
+ * belongs to exactly one render group, which are numbered in z-order: the group with index zero
+ * should be rendered on bottom; the group with the highest index should be rendered on top.
*/
@IntRange(from = 0)
- public val renderGroupCount: Int =
+ public fun getRenderGroupCount(): Int =
ModeledShapeNative.getRenderGroupCount(nativeAddress).also { check(it >= 0) }
/** The [Mesh] objects that make up this shape. */
private val meshesByGroup: List<List<Mesh>> = buildList {
- for (groupIndex in 0 until renderGroupCount) {
+ for (groupIndex in 0 until getRenderGroupCount()) {
val nativeAddressesOfMeshes =
ModeledShapeNative.getNativeAddressesOfMeshes(nativeAddress, groupIndex)
add(nativeAddressesOfMeshes.map(::Mesh))
}
}
+ private var _bounds: Box? = null
+
/**
- * The minimum bounding box of the [PartitionedMesh]. This will be null if the [PartitionedMesh]
- * is empty.
+ * Returns the minimum bounding box of the [PartitionedMesh]. This will be null if the
+ * [PartitionedMesh] is empty.
*/
- public val bounds: Box? = run {
+ public fun computeBoundingBox(): Box? {
+ // If we've already computed the bounding box, re-use it -- it won't change over the
+ // lifetime of
+ // this object.
+ if (_bounds != null) return _bounds
+
+ // If we have no meshes, then the bounding box is null.
+ if (meshesByGroup.isEmpty()) return null
+
val envelope = BoxAccumulator()
for (meshes in meshesByGroup) {
for (mesh in meshes) {
envelope.add(mesh.bounds)
}
}
- envelope.box
+ _bounds = envelope.box
+ return envelope.box
}
/** Returns the [MeshFormat] used for each [Mesh] in the specified render group. */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
public fun renderGroupFormat(@IntRange(from = 0) groupIndex: Int): MeshFormat {
- require(groupIndex >= 0 && groupIndex < renderGroupCount) {
- "groupIndex=$groupIndex must be between 0 and renderGroupCount=${renderGroupCount}"
+ require(groupIndex >= 0 && groupIndex < getRenderGroupCount()) {
+ "groupIndex=$groupIndex must be between 0 and renderGroupCount=${getRenderGroupCount()}"
}
return MeshFormat(ModeledShapeNative.getRenderGroupFormat(nativeAddress, groupIndex))
}
@@ -119,51 +129,51 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
public fun renderGroupMeshes(@IntRange(from = 0) groupIndex: Int): List<Mesh> {
- require(groupIndex >= 0 && groupIndex < renderGroupCount) {
- "groupIndex=$groupIndex must be between 0 and renderGroupCount=${renderGroupCount}"
+ require(groupIndex >= 0 && groupIndex < getRenderGroupCount()) {
+ "groupIndex=$groupIndex must be between 0 and renderGroupCount=${getRenderGroupCount()}"
}
return meshesByGroup[groupIndex]
}
- /** The number of outlines that comprise this shape. */
+ /** Returns the number of outlines that comprise this shape. */
@IntRange(from = 0)
- public fun outlineCount(@IntRange(from = 0) groupIndex: Int): Int {
- require(groupIndex >= 0 && groupIndex < renderGroupCount) {
- "groupIndex=$groupIndex must be between 0 and renderGroupCount=${renderGroupCount}"
+ public fun getOutlineCount(@IntRange(from = 0) groupIndex: Int): Int {
+ require(groupIndex >= 0 && groupIndex < getRenderGroupCount()) {
+ "groupIndex=$groupIndex must be between 0 and renderGroupCount=${getRenderGroupCount()}"
}
return ModeledShapeNative.getOutlineCount(nativeAddress, groupIndex).also { check(it >= 0) }
}
/**
- * The number of vertices that are in the outline at index [outlineIndex], and within the render
- * group at [groupIndex].
+ * Returns the number of vertices that are in the outline at index [outlineIndex], and within
+ * the render group at [groupIndex].
*/
@IntRange(from = 0)
- public fun outlineVertexCount(
+ public fun getOutlineVertexCount(
@IntRange(from = 0) groupIndex: Int,
@IntRange(from = 0) outlineIndex: Int,
): Int {
- require(outlineIndex >= 0 && outlineIndex < outlineCount(groupIndex)) {
- "outlineIndex=$outlineIndex must be between 0 and outlineCount=${outlineCount(groupIndex)}"
+ require(outlineIndex >= 0 && outlineIndex < getOutlineCount(groupIndex)) {
+ "outlineIndex=$outlineIndex must be between 0 and outlineCount=${getOutlineCount(groupIndex)}"
}
return ModeledShapeNative.getOutlineVertexCount(nativeAddress, groupIndex, outlineIndex)
.also { check(it >= 0) }
}
/**
- * Retrieve the outline vertex position from the outline at index [outlineIndex] (which can be
- * up to, but not including, [outlineCount]), and the vertex from within that outline at index
- * [outlineVertexIndex] (which can be up to, but not including, the result of calling
- * [outlineVertexCount] with [outlineIndex]). The resulting x/y position of that outline vertex
- * will be put into [outPosition], which can be pre-allocated and reused to avoid allocations.
+ * Populates [outPosition] with the position of the outline vertex at [outlineVertexIndex] in
+ * the outline at [outlineIndex] in the render group at [groupIndex], and returns [outPosition].
+ * [groupIndex] must be less than [getRenderGroupCount], [outlineIndex] must be less
+ * [getOutlineVertexCount] for [groupIndex], and [outlineVertexIndex] must be less than
+ * [getOutlineVertexCount] for [groupIndex] and [outlineIndex].
*/
public fun populateOutlinePosition(
@IntRange(from = 0) groupIndex: Int,
@IntRange(from = 0) outlineIndex: Int,
@IntRange(from = 0) outlineVertexIndex: Int,
outPosition: MutableVec,
- ) {
- val outlineVertexCount = outlineVertexCount(groupIndex, outlineIndex)
+ ): MutableVec {
+ val outlineVertexCount = getOutlineVertexCount(groupIndex, outlineIndex)
require(outlineVertexIndex >= 0 && outlineVertexIndex < outlineVertexCount) {
"outlineVertexIndex=$outlineVertexIndex must be between 0 and " +
"outlineVertexCount($outlineVertexIndex)=$outlineVertexCount"
@@ -178,6 +188,7 @@
val (meshIndex, meshVertexIndex) = scratchIntArray
val mesh = meshesByGroup[groupIndex][meshIndex]
mesh.fillPosition(meshVertexIndex, outPosition)
+ return outPosition
}
/**
@@ -187,18 +198,18 @@
* triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
* Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
* loops back over itself) are counted individually. Note that, if any triangles have negative
- * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
- * value of their area will be used instead.
+ * area (due to winding, see [Triangle.computeSignedArea]), the absolute value of their area
+ * will be used instead.
*
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [triangleToThis] contains the transform that maps from [triangle]'s
- * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
- * [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+ * [AffineTransform.IDENTITY].
*/
@JvmOverloads
@FloatRange(from = 0.0, to = 1.0)
- public fun coverage(
+ public fun computeCoverage(
triangle: Triangle,
triangleToThis: AffineTransform = AffineTransform.IDENTITY,
): Float =
@@ -225,17 +236,20 @@
* [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space. Triangles in the
* [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that loops back over
* itself) are counted individually. Note that, if any triangles have negative area (due to
- * winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute value of their
- * area will be used instead.
+ * winding, see [Triangle.computeSignedArea]), the absolute value of their area will be used
+ * instead.
*
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [boxToThis] contains the transform that maps from [box]'s coordinate space
- * to this [PartitionedMesh]'s coordinate space, which defaults to the [IDENTITY].
+ * to this [PartitionedMesh]'s coordinate space, which defaults to [AffineTransform.IDENTITY].
*/
@JvmOverloads
@FloatRange(from = 0.0, to = 1.0)
- public fun coverage(box: Box, boxToThis: AffineTransform = AffineTransform.IDENTITY): Float =
+ public fun computeCoverage(
+ box: Box,
+ boxToThis: AffineTransform = AffineTransform.IDENTITY
+ ): Float =
ModeledShapeNative.modeledShapeBoxCoverage(
nativeAddress = nativeAddress,
boxXMin = box.xMin,
@@ -257,18 +271,18 @@
* of all triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
* Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
* loops back over itself) are counted individually. Note that, if any triangles have negative
- * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
- * value of their area will be used instead.
+ * area (due to winding, see [Triangle.computeSignedArea]), the absolute value of their area
+ * will be used instead.
*
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [parallelogramToThis] contains the transform that maps from
* [parallelogram]'s coordinate space to this [PartitionedMesh]'s coordinate space, which
- * defaults to the [IDENTITY].
+ * defaults to [AffineTransform.IDENTITY].
*/
@JvmOverloads
@FloatRange(from = 0.0, to = 1.0)
- public fun coverage(
+ public fun computeCoverage(
parallelogram: Parallelogram,
parallelogramToThis: AffineTransform = AffineTransform.IDENTITY,
): Float =
@@ -295,18 +309,18 @@
* triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
* Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
* loops back over itself) are counted individually. Note that, if any triangles have negative
- * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
- * value of their area will be used instead.t
+ * area (due to winding, see [Triangle.computeSignedArea]), the absolute value of their area
+ * will be used instead.
*
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [otherShapeToThis] contains the transform that maps from [other]'s
- * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
- * [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+ * [AffineTransform.IDENTITY].
*/
@JvmOverloads
@FloatRange(from = 0.0, to = 1.0)
- public fun coverage(
+ public fun computeCoverage(
other: PartitionedMesh,
otherShapeToThis: AffineTransform = AffineTransform.IDENTITY,
): Float =
@@ -327,7 +341,7 @@
*
* This is equivalent to:
* ```
- * this.coverage(triangle, triangleToThis) > coverageThreshold
+ * computeCoverage(triangle, triangleToThis) > coverageThreshold
* ```
*
* but may be faster.
@@ -335,11 +349,11 @@
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [triangleToThis] contains the transform that maps from [triangle]'s
- * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
- * [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+ * [AffineTransform.IDENTITY].
*/
@JvmOverloads
- public fun coverageIsGreaterThan(
+ public fun computeCoverageIsGreaterThan(
triangle: Triangle,
coverageThreshold: Float,
triangleToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -375,10 +389,10 @@
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [boxToThis] contains the transform that maps from [box]'s coordinate space
- * to this [PartitionedMesh]'s coordinate space, which defaults to the [IDENTITY].
+ * to this [PartitionedMesh]'s coordinate space, which defaults to [AffineTransform.IDENTITY].
*/
@JvmOverloads
- public fun coverageIsGreaterThan(
+ public fun computeCoverageIsGreaterThan(
box: Box,
coverageThreshold: Float,
boxToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -413,10 +427,10 @@
*
* Optional argument [parallelogramToThis] contains the transform that maps from
* [parallelogram]'s coordinate space to this [PartitionedMesh]'s coordinate space, which
- * defaults to the [IDENTITY].
+ * defaults to [AffineTransform.IDENTITY].
*/
@JvmOverloads
- public fun coverageIsGreaterThan(
+ public fun computeCoverageIsGreaterThan(
parallelogram: Parallelogram,
coverageThreshold: Float,
parallelogramToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -452,11 +466,11 @@
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [otherShapeToThis] contains the transform that maps from [other]'s
- * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
- * [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+ * [AffineTransform.IDENTITY].
*/
@JvmOverloads
- public fun coverageIsGreaterThan(
+ public fun computeCoverageIsGreaterThan(
other: PartitionedMesh,
coverageThreshold: Float,
otherShapeToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -488,7 +502,7 @@
override fun toString(): String {
val address = java.lang.Long.toHexString(nativeAddress)
- return "PartitionedMesh(bounds=$bounds, meshesByGroup=$meshesByGroup, nativeAddress=$address)"
+ return "PartitionedMesh(bounds=${computeBoundingBox()}, meshesByGroup=$meshesByGroup, nativeAddress=$address)"
}
protected fun finalize() {
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
index 10bf22b..50106fe 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
@@ -31,35 +31,35 @@
class PartitionedMeshTest {
@Test
- fun bounds_shouldBeEmpty() {
+ fun computeBoundingBox_shouldBeEmpty() {
val partitionedMesh = PartitionedMesh()
- assertThat(partitionedMesh.bounds).isNull()
+ assertThat(partitionedMesh.computeBoundingBox()).isNull()
}
@Test
- fun renderGroupCount_whenEmptyShape_shouldBeZero() {
+ fun getRenderGroupCount_whenEmptyShape_shouldBeZero() {
val partitionedMesh = PartitionedMesh()
- assertThat(partitionedMesh.renderGroupCount).isEqualTo(0)
+ assertThat(partitionedMesh.getRenderGroupCount()).isEqualTo(0)
}
@Test
- fun outlineCount_whenEmptyShape_shouldThrow() {
+ fun getOutlineCount_whenEmptyShape_shouldThrow() {
val partitionedMesh = PartitionedMesh()
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(-1) }
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(0) }
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(1) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(-1) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(1) }
}
@Test
- fun outlineVertexCount_whenEmptyShape_shouldThrow() {
+ fun getOutlineVertexCount_whenEmptyShape_shouldThrow() {
val partitionedMesh = PartitionedMesh()
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(-1, 0) }
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(0, 0) }
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(1, 0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(-1, 0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(0, 0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(1, 0) }
}
@Test
@@ -92,14 +92,14 @@
fun populateOutlinePosition_withStrokeShape_shouldBeWithinBounds() {
val shape = buildTestStrokeShape()
- assertThat(shape.renderGroupCount).isEqualTo(1)
- assertThat(shape.outlineCount(0)).isEqualTo(1)
- assertThat(shape.outlineVertexCount(0, 0)).isGreaterThan(2)
+ assertThat(shape.getRenderGroupCount()).isEqualTo(1)
+ assertThat(shape.getOutlineCount(0)).isEqualTo(1)
+ assertThat(shape.getOutlineVertexCount(0, 0)).isGreaterThan(2)
- val bounds = assertNotNull(shape.bounds)
+ val bounds = assertNotNull(shape.computeBoundingBox())
val p = MutableVec()
- for (outlineVertexIndex in 0 until shape.outlineVertexCount(0, 0)) {
+ for (outlineVertexIndex in 0 until shape.getOutlineVertexCount(0, 0)) {
shape.populateOutlinePosition(groupIndex = 0, outlineIndex = 0, outlineVertexIndex, p)
assertThat(p.x).isAtLeast(bounds.xMin)
assertThat(p.y).isAtLeast(bounds.yMin)
@@ -124,7 +124,7 @@
@Test
fun meshFormat_forTestShape_isEquivalentToMeshFormatOfFirstMesh() {
val partitionedMesh = buildTestStrokeShape()
- assertThat(partitionedMesh.renderGroupCount).isEqualTo(1)
+ assertThat(partitionedMesh.getRenderGroupCount()).isEqualTo(1)
val shapeFormat = partitionedMesh.renderGroupFormat(0)
val meshes = partitionedMesh.renderGroupMeshes(0)
assertThat(meshes).isNotEmpty()
@@ -152,9 +152,9 @@
p2 = ImmutableVec(100f, 700f),
)
- assertThat(partitionedMesh.coverage(intersectingTriangle)).isGreaterThan(0f)
- assertThat(partitionedMesh.coverage(externalTriangle)).isEqualTo(0f)
- assertThat(partitionedMesh.coverage(externalTriangle, SCALE_TRANSFORM)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(intersectingTriangle)).isGreaterThan(0f)
+ assertThat(partitionedMesh.computeCoverage(externalTriangle)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(externalTriangle, SCALE_TRANSFORM)).isEqualTo(0f)
}
/**
@@ -169,9 +169,9 @@
val externalBox =
ImmutableBox.fromTwoPoints(ImmutableVec(100f, 200f), ImmutableVec(300f, 400f))
- assertThat(partitionedMesh.coverage(intersectingBox)).isGreaterThan(0f)
- assertThat(partitionedMesh.coverage(externalBox)).isEqualTo(0f)
- assertThat(partitionedMesh.coverage(externalBox, SCALE_TRANSFORM)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(intersectingBox)).isGreaterThan(0f)
+ assertThat(partitionedMesh.computeCoverage(externalBox)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(externalBox, SCALE_TRANSFORM)).isEqualTo(0f)
}
/**
@@ -196,9 +196,10 @@
shearFactor = 2f,
)
- assertThat(partitionedMesh.coverage(intersectingParallelogram)).isGreaterThan(0f)
- assertThat(partitionedMesh.coverage(externalParallelogram)).isEqualTo(0f)
- assertThat(partitionedMesh.coverage(externalParallelogram, SCALE_TRANSFORM)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(intersectingParallelogram)).isGreaterThan(0f)
+ assertThat(partitionedMesh.computeCoverage(externalParallelogram)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(externalParallelogram, SCALE_TRANSFORM))
+ .isEqualTo(0f)
}
/**
@@ -221,9 +222,9 @@
)
.shape
- assertThat(partitionedMesh.coverage(intersectingShape)).isGreaterThan(0f)
- assertThat(partitionedMesh.coverage(externalShape)).isEqualTo(0f)
- assertThat(partitionedMesh.coverage(externalShape, SCALE_TRANSFORM)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(intersectingShape)).isGreaterThan(0f)
+ assertThat(partitionedMesh.computeCoverage(externalShape)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(externalShape, SCALE_TRANSFORM)).isEqualTo(0f)
}
/**
@@ -246,9 +247,11 @@
p2 = ImmutableVec(100f, 700f),
)
- assertThat(partitionedMesh.coverageIsGreaterThan(intersectingTriangle, 0f)).isTrue()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalTriangle, 0f)).isFalse()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalTriangle, 0f, SCALE_TRANSFORM))
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingTriangle, 0f)).isTrue()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalTriangle, 0f)).isFalse()
+ assertThat(
+ partitionedMesh.computeCoverageIsGreaterThan(externalTriangle, 0f, SCALE_TRANSFORM)
+ )
.isFalse()
}
@@ -270,9 +273,9 @@
val externalBox =
ImmutableBox.fromTwoPoints(ImmutableVec(100f, 200f), ImmutableVec(300f, 400f))
- assertThat(partitionedMesh.coverageIsGreaterThan(intersectingBox, 0f)).isTrue()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalBox, 0f)).isFalse()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalBox, 0f, SCALE_TRANSFORM))
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingBox, 0f)).isTrue()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalBox, 0f)).isFalse()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalBox, 0f, SCALE_TRANSFORM))
.isFalse()
}
@@ -298,10 +301,16 @@
shearFactor = 2f,
)
- assertThat(partitionedMesh.coverageIsGreaterThan(intersectingParallelogram, 0f)).isTrue()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalParallelogram, 0f)).isFalse()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingParallelogram, 0f))
+ .isTrue()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalParallelogram, 0f))
+ .isFalse()
assertThat(
- partitionedMesh.coverageIsGreaterThan(externalParallelogram, 0f, SCALE_TRANSFORM)
+ partitionedMesh.computeCoverageIsGreaterThan(
+ externalParallelogram,
+ 0f,
+ SCALE_TRANSFORM
+ )
)
.isFalse()
}
@@ -333,9 +342,9 @@
)
.shape
- assertThat(partitionedMesh.coverageIsGreaterThan(intersectingShape, 0f)).isTrue()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalShape, 0f)).isFalse()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalShape, 0f, SCALE_TRANSFORM))
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingShape, 0f)).isTrue()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalShape, 0f)).isFalse()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalShape, 0f, SCALE_TRANSFORM))
.isFalse()
}
@@ -360,7 +369,7 @@
)
assertThat(partitionedMesh.isSpatialIndexInitialized()).isFalse()
- assertThat(partitionedMesh.coverage(triangle)).isNotNaN()
+ assertThat(partitionedMesh.computeCoverage(triangle)).isNotNaN()
assertThat(partitionedMesh.isSpatialIndexInitialized()).isTrue()
}
diff --git a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt
index 050e442..5b262d7 100644
--- a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt
+++ b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt
@@ -125,8 +125,8 @@
*/
internal constructor(nativeAddress: Long, brush: Brush) {
val shape = PartitionedMesh(StrokeJni.allocShallowCopyOfShape(nativeAddress))
- require(shape.renderGroupCount == brush.family.coats.size) {
- "The shape must have one render group per brush coat, but found ${shape.renderGroupCount} render groups in shape and ${brush.family.coats.size} brush coats in brush."
+ require(shape.getRenderGroupCount() == brush.family.coats.size) {
+ "The shape must have one render group per brush coat, but found ${shape.getRenderGroupCount()} render groups in shape and ${brush.family.coats.size} brush coats in brush."
}
this.nativeAddress = nativeAddress
this.brush = brush
@@ -142,8 +142,8 @@
* [PartitionedMesh] is being stored in addition to the [Brush] and [StrokeInputBatch].
*/
public constructor(brush: Brush, inputs: StrokeInputBatch, shape: PartitionedMesh) {
- require(shape.renderGroupCount == brush.family.coats.size) {
- "The shape must have one render group per brush coat, but found ${shape.renderGroupCount} render groups in shape and ${brush.family.coats.size} brush coats in brush."
+ require(shape.getRenderGroupCount() == brush.family.coats.size) {
+ "The shape must have one render group per brush coat, but found ${shape.getRenderGroupCount()} render groups in shape and ${brush.family.coats.size} brush coats in brush."
}
this.brush = brush
this.shape = shape
diff --git a/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt b/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt
index c730120..2155aff 100644
--- a/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt
+++ b/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt
@@ -68,7 +68,7 @@
// Create a [ModeledShape] with render group.
val inputs = makeTestInputs()
val shape = Stroke(buildTestBrush(), inputs).shape
- assertThat(shape.renderGroupCount).isEqualTo(1)
+ assertThat(shape.getRenderGroupCount()).isEqualTo(1)
// Create a brush with two brush coats.
val coat = BrushCoat(BrushTip(), BrushPaint())
diff --git a/inspection/inspection-gradle-plugin/lint-baseline.xml b/inspection/inspection-gradle-plugin/lint-baseline.xml
index 603e7bd..ef0ffef 100644
--- a/inspection/inspection-gradle-plugin/lint-baseline.xml
+++ b/inspection/inspection-gradle-plugin/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.6.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.6.0-beta01)" variant="all" version="8.6.0-beta01">
+<issues format="6" by="lint 8.7.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.0-alpha02)" variant="all" version="8.7.0-alpha02">
<issue
id="EagerGradleConfiguration"
@@ -38,6 +38,24 @@
</issue>
<issue
+ id="GradleProjectIsolation"
+ message="Avoid using method findProject"
+ errorLine1=" val inspectorProject = libraryProject.rootProject.findProject(inspectorProjectPath)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val inspectorProject = libraryProject.rootProject.findProject(inspectorProjectPath)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt"/>
+ </issue>
+
+ <issue
id="WithPluginClasspathUsage"
message="Avoid usage of GradleRunner#withPluginClasspath, which is broken. Instead use something like https://github.com/autonomousapps/dependency-analysis-gradle-plugin/tree/main/testkit#gradle-testkit-support-plugin"
errorLine1=" GradleRunner.create().withProjectDir(projectSetup.rootDir).withPluginClasspath()"
diff --git a/libraryversions.toml b/libraryversions.toml
index 3980454..75a598d 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,6 +1,6 @@
[versions]
ACTIVITY = "1.10.0-alpha02"
-ANNOTATION = "1.9.0-alpha02"
+ANNOTATION = "1.9.0-alpha03"
ANNOTATION_EXPERIMENTAL = "1.5.0-alpha01"
APPCOMPAT = "1.8.0-alpha01"
APPSEARCH = "1.1.0-alpha05"
@@ -89,9 +89,9 @@
LEANBACK_TAB = "1.1.0-beta01"
LEGACY = "1.1.0-alpha01"
LIBYUV = "0.1.0-dev01"
-LIFECYCLE = "2.9.0-alpha01"
+LIFECYCLE = "2.9.0-alpha02"
LIFECYCLE_EXTENSIONS = "2.2.0"
-LINT = "1.0.0-alpha01"
+LINT = "1.0.0-alpha02"
LOADER = "1.2.0-alpha01"
MEDIA = "1.7.0-rc01"
MEDIAROUTER = "1.8.0-alpha01"
diff --git a/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt b/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
index e8680ae..3f129b8 100644
--- a/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
+++ b/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
@@ -140,8 +140,11 @@
Replacement(NAMED_DOMAIN_OBJECT_COLLECTION, null, EAGER_CONFIGURATION_ISSUE),
"findByName" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
"findByPath" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
+ "findProject" to Replacement(PROJECT, null, PROJECT_ISOLATION_ISSUE),
"findProperty" to
Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
+ "hasProperty" to
+ Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
"property" to
Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
"iterator" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
@@ -149,8 +152,10 @@
"getAt" to Replacement(TASK_COLLECTION, "named", EAGER_CONFIGURATION_ISSUE),
"getByPath" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
"getByName" to Replacement(TASK_CONTAINER, "named", EAGER_CONFIGURATION_ISSUE),
+ "getParent" to Replacement(PROJECT, null, PROJECT_ISOLATION_ISSUE),
"getProperties" to
Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
+ "getRootProject" to Replacement(PROJECT, null, PROJECT_ISOLATION_ISSUE),
"matching" to Replacement(TASK_COLLECTION, null, EAGER_CONFIGURATION_ISSUE),
"replace" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
"remove" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
diff --git a/navigation/navigation-common/api/current.txt b/navigation/navigation-common/api/current.txt
index bb6ad19..cabfeb1 100644
--- a/navigation/navigation-common/api/current.txt
+++ b/navigation/navigation-common/api/current.txt
@@ -590,5 +590,8 @@
method public static inline <reified T> T toRoute(androidx.lifecycle.SavedStateHandle, optional java.util.Map<kotlin.reflect.KType,androidx.navigation.NavType<? extends java.lang.Object?>> typeMap);
}
+ public interface SupportingPane {
+ }
+
}
diff --git a/navigation/navigation-common/api/restricted_current.txt b/navigation/navigation-common/api/restricted_current.txt
index bb6ad19..cabfeb1 100644
--- a/navigation/navigation-common/api/restricted_current.txt
+++ b/navigation/navigation-common/api/restricted_current.txt
@@ -590,5 +590,8 @@
method public static inline <reified T> T toRoute(androidx.lifecycle.SavedStateHandle, optional java.util.Map<kotlin.reflect.KType,androidx.navigation.NavType<? extends java.lang.Object?>> typeMap);
}
+ public interface SupportingPane {
+ }
+
}
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/SupportingPane.kt b/navigation/navigation-common/src/main/java/androidx/navigation/SupportingPane.kt
new file mode 100644
index 0000000..a6fd4ff
--- /dev/null
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/SupportingPane.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.navigation
+
+/**
+ * A marker interface for [NavDestination] subclasses that sit alongside the view of other
+ * destinations.
+ *
+ * Supporting pane destinations have the same lifecycle as the other visible destinations (e.g., a
+ * non-SupportingPane destination will continue to be resumed when a supporting pane is added to the
+ * back stack).
+ *
+ * [androidx.navigation.NavController.OnDestinationChangedListener] instances can also customize
+ * their behavior based on whether the destination is a SupportingPane.
+ */
+public interface SupportingPane
diff --git a/navigation/navigation-compose/samples/build.gradle b/navigation/navigation-compose/samples/build.gradle
index b672a68..eb7498d 100644
--- a/navigation/navigation-compose/samples/build.gradle
+++ b/navigation/navigation-compose/samples/build.gradle
@@ -57,10 +57,6 @@
kotlinTarget = KotlinTarget.KOTLIN_2_0
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
android {
compileSdk 35
namespace "androidx.navigation.compose.samples"
diff --git a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt
index 838929a6..bfb1c33 100644
--- a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt
+++ b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt
@@ -103,65 +103,77 @@
SharedTransitionLayout {
val selectFirst = mutableStateOf(true)
NavHost(navController, startDestination = RedBox) {
- composable<RedBox> { RedBox(this, selectFirst) { navController.navigate(BlueBox) } }
- composable<BlueBox> { BlueBox(this, selectFirst) { navController.popBackStack() } }
+ composable<RedBox> {
+ RedBox(this@SharedTransitionLayout, this, selectFirst) {
+ navController.navigate(BlueBox)
+ }
+ }
+ composable<BlueBox> {
+ BlueBox(this@SharedTransitionLayout, this, selectFirst) {
+ navController.popBackStack()
+ }
+ }
}
}
}
-context(SharedTransitionScope)
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun RedBox(
+ sharedScope: SharedTransitionScope,
scope: AnimatedContentScope,
selectFirst: MutableState<Boolean>,
onNavigate: () -> Unit
) {
- Box(
- Modifier.sharedBounds(
- rememberSharedContentState("name"),
- scope,
- renderInOverlayDuringTransition = selectFirst.value
- )
- .clickable(
- onClick = {
- selectFirst.value = !selectFirst.value
- onNavigate()
- }
- )
- .background(Color.Red)
- .size(100.dp)
- ) {
- Text("start", color = Color.White)
+ with(sharedScope) {
+ Box(
+ Modifier.sharedBounds(
+ rememberSharedContentState("name"),
+ scope,
+ renderInOverlayDuringTransition = selectFirst.value
+ )
+ .clickable(
+ onClick = {
+ selectFirst.value = !selectFirst.value
+ onNavigate()
+ }
+ )
+ .background(Color.Red)
+ .size(100.dp)
+ ) {
+ Text("start", color = Color.White)
+ }
}
}
-context(SharedTransitionScope)
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun BlueBox(
+ sharedScope: SharedTransitionScope,
scope: AnimatedContentScope,
selectFirst: MutableState<Boolean>,
onPopBack: () -> Unit
) {
- Box(
- Modifier.offset(180.dp, 180.dp)
- .sharedBounds(
- rememberSharedContentState("name"),
- scope,
- renderInOverlayDuringTransition = !selectFirst.value
- )
- .clickable(
- onClick = {
- selectFirst.value = !selectFirst.value
- onPopBack()
- }
- )
- .alpha(0.5f)
- .background(Color.Blue)
- .size(180.dp)
- ) {
- Text("finish", color = Color.White)
+ with(sharedScope) {
+ Box(
+ Modifier.offset(180.dp, 180.dp)
+ .sharedBounds(
+ rememberSharedContentState("name"),
+ scope,
+ renderInOverlayDuringTransition = !selectFirst.value
+ )
+ .clickable(
+ onClick = {
+ selectFirst.value = !selectFirst.value
+ onPopBack()
+ }
+ )
+ .alpha(0.5f)
+ .background(Color.Blue)
+ .size(180.dp)
+ ) {
+ Text("finish", color = Color.White)
+ }
}
}
diff --git a/navigation/navigation-runtime/lint-baseline.xml b/navigation/navigation-runtime/lint-baseline.xml
index e8b9764..287e223 100644
--- a/navigation/navigation-runtime/lint-baseline.xml
+++ b/navigation/navigation-runtime/lint-baseline.xml
@@ -40,6 +40,15 @@
<issue
id="NewApi"
message="This Kotlin extension function will be hidden by `java.util.SequencedCollection` starting in API 35"
+ errorLine1=" nextResumed.removeFirstKt()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/navigation/NavController.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="This Kotlin extension function will be hidden by `java.util.SequencedCollection` starting in API 35"
errorLine1=" val started = nextStarted.removeFirstKt()"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt
index b393c43..71559e7 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt
@@ -23,7 +23,11 @@
import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.navigation.test.FloatingTestNavigator
import androidx.navigation.test.R
+import androidx.navigation.test.SupportingFloatingTestNavigator
+import androidx.navigation.test.SupportingTestNavigator
import androidx.navigation.test.dialog
+import androidx.navigation.test.supportingDialog
+import androidx.navigation.test.supportingPane
import androidx.test.annotation.UiThreadTest
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -164,6 +168,79 @@
}
/**
+ * Test that navigating from a sibling to a SupportingPane sibling leaves the previous
+ * destination resumed.
+ */
+ @UiThreadTest
+ @Test
+ fun testLifecycleWithSupportingPane() {
+ val navController = createNavController()
+ val navGraph =
+ navController.navigatorProvider.navigation(
+ route = "graph",
+ startDestination = "start"
+ ) {
+ test("start")
+ test("second")
+ supportingPane("supportingPane")
+ }
+ navController.graph = navGraph
+
+ val graphBackStackEntry = navController.getBackStackEntry("graph")
+ assertWithMessage("The parent graph should be resumed when its child is resumed")
+ .that(graphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ val startBackStackEntry = navController.getBackStackEntry("start")
+ assertWithMessage("The start destination should be resumed")
+ .that(startBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+
+ navController.navigate("second")
+
+ assertWithMessage("The parent graph should be resumed when its child is resumed")
+ .that(graphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ assertWithMessage("The start destination should be created when not visible")
+ .that(startBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.CREATED)
+ val secondBackStackEntry = navController.getBackStackEntry("second")
+ assertWithMessage("The second destination should be resumed")
+ .that(secondBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+
+ navController.navigate("supportingPane")
+
+ assertWithMessage("The parent graph should be resumed when its child is resumed")
+ .that(graphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ assertWithMessage("The start destination should still be in created")
+ .that(startBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.CREATED)
+ assertWithMessage("The second destination should be resumed when a SupportingPane is open")
+ .that(secondBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ val supportingPaneBackStackEntry = navController.getBackStackEntry("supportingPane")
+ assertWithMessage("The supporting pane destination should be resumed")
+ .that(supportingPaneBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+
+ navController.popBackStack()
+
+ assertWithMessage("The parent graph should be resumed when its child is resumed")
+ .that(graphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ assertWithMessage("The start destination should still be in created")
+ .that(startBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.CREATED)
+ assertWithMessage("The second destination should be resumed after pop")
+ .that(secondBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ assertWithMessage("The popped destination should be destroyed")
+ .that(supportingPaneBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.DESTROYED)
+ }
+
+ /**
* Test that navigating from a sibling to a FloatingWindow sibling leaves the previous
* destination started.
*/
@@ -224,6 +301,78 @@
.isEqualTo(Lifecycle.State.DESTROYED)
}
+ /**
+ * Test that navigating from a sibling + SupportingPane sibling to a dialog leaves both started.
+ */
+ @UiThreadTest
+ @Test
+ fun testLifecycleWithSupportingPaneAndDialog() {
+ val navController = createNavController()
+ val navGraph =
+ navController.navigatorProvider.navigation(
+ route = "graph",
+ startDestination = "start"
+ ) {
+ test("start")
+ supportingPane("supportingPane")
+ dialog("dialog")
+ }
+ navController.graph = navGraph
+
+ val graphBackStackEntry = navController.getBackStackEntry("graph")
+ assertWithMessage("The parent graph should be resumed when its child is resumed")
+ .that(graphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ val startBackStackEntry = navController.getBackStackEntry("start")
+ assertWithMessage("The start destination should be resumed")
+ .that(startBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+
+ navController.navigate("supportingPane")
+
+ assertWithMessage("The parent graph should be resumed when its child is resumed")
+ .that(graphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ assertWithMessage("The start destination should be resumed when a SupportingPane is open")
+ .that(startBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ val supportingPaneBackStackEntry = navController.getBackStackEntry("supportingPane")
+ assertWithMessage("The supporting pane destination should be resumed")
+ .that(supportingPaneBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+
+ navController.navigate("dialog")
+
+ assertWithMessage("The parent graph should be resumed when its child is resumed")
+ .that(graphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ assertWithMessage("The start destination should be started under a dialog")
+ .that(startBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.STARTED)
+ assertWithMessage("The supporting pane destination should be started under a dialog")
+ .that(supportingPaneBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.STARTED)
+ val dialogBackStackEntry = navController.getBackStackEntry("dialog")
+ assertWithMessage("The dialog destination should be resumed")
+ .that(dialogBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+
+ navController.popBackStack()
+
+ assertWithMessage("The parent graph should be resumed when its child is resumed")
+ .that(graphBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ assertWithMessage("The start destination should still be resumed after pop")
+ .that(startBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ assertWithMessage("The supporting pane destination should be resumed after pop")
+ .that(supportingPaneBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ assertWithMessage("The popped destination should be destroyed")
+ .that(dialogBackStackEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.DESTROYED)
+ }
+
/** Test that all visible floating windows underneath the top one are marked started. */
@UiThreadTest
@Test
@@ -268,6 +417,53 @@
assertThat(topDialogEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
}
+ /**
+ * Test that all visible floating windows underneath the top one are marked started unless a
+ * SupportingPane+FloatingWindow destination is above a FloatingWindow.
+ */
+ @UiThreadTest
+ @Test
+ fun testLifecycleWithSupportingDialogs() {
+ val navController = createNavController()
+ val navGraph =
+ navController.navigatorProvider.navigation(
+ route = "graph",
+ startDestination = "start"
+ ) {
+ test("start")
+ supportingDialog("bottomDialog")
+ dialog("midDialog")
+ supportingDialog("topDialog")
+ }
+ navController.graph = navGraph
+
+ val graphEntry = navController.getBackStackEntry("graph")
+ assertThat(graphEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ val startEntry = navController.getBackStackEntry("start")
+ assertThat(startEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ navController.navigate("bottomDialog")
+ assertThat(graphEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ assertThat(startEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ val bottomDialogEntry = navController.getBackStackEntry("bottomDialog")
+ assertThat(bottomDialogEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ navController.navigate("midDialog")
+ assertThat(graphEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ assertThat(startEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ assertThat(bottomDialogEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ val midDialogEntry = navController.getBackStackEntry("midDialog")
+ assertThat(midDialogEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ navController.navigate("topDialog")
+ assertThat(graphEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ assertThat(startEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ assertThat(bottomDialogEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ assertThat(midDialogEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ val topDialogEntry = navController.getBackStackEntry("topDialog")
+ assertThat(topDialogEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ }
+
@UiThreadTest
@Test
fun testLifecycleWithDialogsAndGraphs() {
@@ -1386,7 +1582,9 @@
): NavController {
val navController = NavHostController(ApplicationProvider.getApplicationContext())
navController.navigatorProvider.addNavigator(TestNavigator())
+ navController.navigatorProvider.addNavigator(SupportingTestNavigator())
navController.navigatorProvider.addNavigator(FloatingTestNavigator())
+ navController.navigatorProvider.addNavigator(SupportingFloatingTestNavigator())
navController.setLifecycleOwner(lifecycleOwner)
return navController
}
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/test/SupportingFloatingTestNavigator.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/test/SupportingFloatingTestNavigator.kt
new file mode 100644
index 0000000..9a3c82b
--- /dev/null
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/test/SupportingFloatingTestNavigator.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.navigation.test
+
+import androidx.navigation.FloatingWindow
+import androidx.navigation.NavDestinationBuilder
+import androidx.navigation.NavDestinationDsl
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.Navigator
+import androidx.navigation.SupportingPane
+import androidx.navigation.get
+import androidx.testutils.TestNavigator
+
[email protected]("supporting_dialog")
+class SupportingFloatingTestNavigator : TestNavigator() {
+ override fun createDestination(): Destination {
+ return SupportingFloatingDestination(this)
+ }
+
+ class SupportingFloatingDestination(navigator: TestNavigator) :
+ Destination(navigator), FloatingWindow, SupportingPane
+}
+
+/** Construct a new [TestNavigator.Destination] from a [SupportingFloatingTestNavigator]. */
+inline fun NavGraphBuilder.supportingDialog(
+ route: String,
+ builder: SupportingFloatingTestNavigatorDestinationBuilder.() -> Unit = {}
+) =
+ destination(
+ SupportingFloatingTestNavigatorDestinationBuilder(
+ provider[SupportingFloatingTestNavigator::class],
+ route
+ )
+ .apply(builder)
+ )
+
+/**
+ * DSL for constructing a new [TestNavigator.Destination] from a [SupportingFloatingTestNavigator].
+ */
+@NavDestinationDsl
+class SupportingFloatingTestNavigatorDestinationBuilder(
+ navigator: SupportingFloatingTestNavigator,
+ route: String
+) : NavDestinationBuilder<TestNavigator.Destination>(navigator, route)
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/test/SupportingTestNavigator.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/test/SupportingTestNavigator.kt
new file mode 100644
index 0000000..fac28ea
--- /dev/null
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/test/SupportingTestNavigator.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.test
+
+import androidx.navigation.NavDestinationBuilder
+import androidx.navigation.NavDestinationDsl
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.Navigator
+import androidx.navigation.SupportingPane
+import androidx.navigation.get
+import androidx.testutils.TestNavigator
+
[email protected]("supporting_pane")
+class SupportingTestNavigator : TestNavigator() {
+ override fun createDestination(): Destination {
+ return SupportingDestination(this)
+ }
+
+ class SupportingDestination(navigator: TestNavigator) : Destination(navigator), SupportingPane
+}
+
+/** Construct a new [TestNavigator.Destination] from a [SupportingTestNavigator]. */
+inline fun NavGraphBuilder.supportingPane(
+ route: String,
+ builder: SupportingTestNavigatorDestinationBuilder.() -> Unit = {}
+) =
+ destination(
+ SupportingTestNavigatorDestinationBuilder(provider[SupportingTestNavigator::class], route)
+ .apply(builder)
+ )
+
+/** DSL for constructing a new [TestNavigator.Destination] from a [SupportingTestNavigator]. */
+@NavDestinationDsl
+class SupportingTestNavigatorDestinationBuilder(navigator: SupportingTestNavigator, route: String) :
+ NavDestinationBuilder<TestNavigator.Destination>(navigator, route)
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index 9e36bf8..0075458 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -1105,12 +1105,58 @@
// Nothing to update
return
}
- // First determine what the current resumed destination is and, if and only if
- // the current resumed destination is a FloatingWindow, what destinations are
- // underneath it that must remain started.
- var nextResumed: NavDestination? = backStack.last().destination
+ // Lifecycle can be split into three layers:
+ // 1. Resumed - these are the topmost destination(s) that the user can interact with
+ // 2. Started - these destinations are visible, but are underneath resumed destinations
+ // 3. Created - these destinations are not visible or on the process of being animated out
+
+ // So first, we need to determine which destinations should be resumed and started
+ // This is done by looking at the two special interfaces we have:
+ // - FloatingWindow indicates a destination that is above all other destinations, leaving
+ // destinations below it visible, but not interactable. These are always only on the
+ // top of the back stack
+ // - SupportingPane indicates a destination that sits alongside the previous destination
+ // and shares the same lifecycle (e.g., both will be resumed, started, or created)
+
+ // This means no matter what, the topmost destination should be able to be resumed,
+ // then we add in all of the destinations that also need to be resumed (if the
+ // topmost screen is a SupportingPane)
+ val topmostDestination = backStack.last().destination
+ val nextResumed: MutableList<NavDestination> = mutableListOf(topmostDestination)
+ if (topmostDestination is SupportingPane) {
+ // A special note for destinations that are marked as both a FloatingWindow and a
+ // SupportingPane: a supporting floating window destination can only support other
+ // floating windows - if a supporting floating window destination is above
+ // a regular destination, the regular destination will *not* be resumed, but instead
+ // follow the normal rules between floating windows and regular destinations and only
+ // be started.
+ val onlyAllowFloatingWindows = topmostDestination is FloatingWindow
+ val iterator = backStack.reversed().drop(1).iterator()
+ while (iterator.hasNext()) {
+ val destination = iterator.next().destination
+ if (
+ onlyAllowFloatingWindows &&
+ destination !is FloatingWindow &&
+ destination !is NavGraph
+ ) {
+ break
+ }
+ // Add all visible destinations (e.g., SupportingDestination destinations, their
+ // NavGraphs, and the screen directly below all SupportingDestination destinations)
+ // to nextResumed
+ nextResumed.add(destination)
+ // break if we find first visible screen
+ if (destination !is SupportingPane && destination !is NavGraph) {
+ break
+ }
+ }
+ }
+
+ // Now that we've marked all of the resumed destinations, we continue to iterate
+ // through the back stack to find any destinations that should be started - ones that are
+ // below FloatingWindow destinations
val nextStarted: MutableList<NavDestination> = mutableListOf()
- if (nextResumed is FloatingWindow) {
+ if (nextResumed.last() is FloatingWindow) {
// Find all visible destinations in the back stack as they
// should still be STARTED when the FloatingWindow destination is above it.
val iterator = backStack.reversed().iterator()
@@ -1121,12 +1167,17 @@
// to nextStarted
nextStarted.add(destination)
// break if we find first visible screen
- if (destination !is FloatingWindow && destination !is NavGraph) {
+ if (
+ destination !is FloatingWindow &&
+ destination !is SupportingPane &&
+ destination !is NavGraph
+ ) {
break
}
}
}
- // First iterate downward through the stack, applying downward Lifecycle
+
+ // Now iterate downward through the stack, applying downward Lifecycle
// transitions and capturing any upward Lifecycle transitions to apply afterwards.
// This ensures proper nesting where parent navigation graphs are started before
// their children and stopped only after their children are stopped.
@@ -1136,7 +1187,7 @@
val entry = iterator.next()
val currentMaxLifecycle = entry.maxLifecycle
val destination = entry.destination
- if (nextResumed != null && destination.id == nextResumed.id) {
+ if (nextResumed.firstOrNull()?.id == destination.id) {
// Upward Lifecycle transitions need to be done afterwards so that
// the parent navigation graph is resumed before their children
if (currentMaxLifecycle != Lifecycle.State.RESUMED) {
@@ -1153,7 +1204,8 @@
}
}
if (nextStarted.firstOrNull()?.id == destination.id) nextStarted.removeFirstKt()
- nextResumed = nextResumed.parent
+ nextResumed.removeFirstKt()
+ destination.parent?.let { nextResumed.add(it) }
} else if (nextStarted.isNotEmpty() && destination.id == nextStarted.first().id) {
val started = nextStarted.removeFirstKt()
if (currentMaxLifecycle == Lifecycle.State.RESUMED) {
diff --git a/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt b/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt
index 82e6e99..4d38bfa 100644
--- a/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt
+++ b/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt
@@ -27,6 +27,7 @@
import androidx.navigation.NavDestination
import androidx.navigation.NavOptions
import androidx.navigation.Navigator
+import androidx.navigation.SupportingPane
import androidx.navigation.navOptions
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -86,6 +87,26 @@
}
@Test
+ fun testSupportingPaneLifecycle() {
+ val navigator = SupportingPaneTestNavigator()
+ navigator.onAttach(state)
+ val firstEntry = state.createBackStackEntry(navigator.createDestination(), null)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
+
+ navigator.navigate(listOf(firstEntry), null, null)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ val secondEntry = state.createBackStackEntry(navigator.createDestination(), null)
+ navigator.navigate(listOf(secondEntry), null, null)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ navigator.popBackStack(secondEntry, false)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+ }
+
+ @Test
fun testWithTransitionLifecycle() {
val navigator = TestTransitionNavigator()
navigator.onAttach(state)
@@ -129,6 +150,58 @@
}
@Test
+ fun testWithSupportingPaneTransitionLifecycle() {
+ val navigator = SupportingPaneTestTransitionNavigator()
+ navigator.onAttach(state)
+ val firstEntry = state.createBackStackEntry(navigator.createDestination(), null)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
+
+ navigator.navigate(listOf(firstEntry), null, null)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+ state.markTransitionComplete(firstEntry)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ val secondEntry = state.createBackStackEntry(navigator.createDestination(), null)
+ navigator.navigate(listOf(secondEntry), null, null)
+ // Both are started because they are SupportingPane destinations
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue()
+
+ state.markTransitionComplete(secondEntry)
+ // Even though the secondEntry has completed its transition, the firstEntry
+ // hasn't completed its transition, so it shouldn't be resumed yet
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ state.markTransitionComplete(firstEntry)
+ // Both are resumed because they are SupportingPane destinations that have finished
+ // their transitions
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ navigator.popBackStack(secondEntry, true)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+ state.markTransitionComplete(firstEntry)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ state.markTransitionComplete(secondEntry)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+
+ val restoredSecondEntry = state.restoreBackStackEntry(secondEntry)
+ navigator.navigate(listOf(restoredSecondEntry), null, null)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ assertThat(restoredSecondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue()
+
+ state.markTransitionComplete(firstEntry)
+ state.markTransitionComplete(restoredSecondEntry)
+ assertThat(restoredSecondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ }
+
+ @Test
fun testSameEntry() {
val navigator = TestTransitionNavigator()
navigator.onAttach(state)
@@ -362,6 +435,35 @@
internal class FloatingTestDestination(navigator: Navigator<out NavDestination>) :
NavDestination(navigator), FloatingWindow
+ @Navigator.Name("test")
+ internal class SupportingPaneTestNavigator : Navigator<SupportingPaneTestDestination>() {
+ override fun createDestination(): SupportingPaneTestDestination =
+ SupportingPaneTestDestination(this)
+ }
+
+ @Navigator.Name("test")
+ internal class SupportingPaneTestTransitionNavigator :
+ Navigator<SupportingPaneTestDestination>() {
+
+ override fun createDestination(): SupportingPaneTestDestination =
+ SupportingPaneTestDestination(this)
+
+ override fun navigate(
+ entries: List<NavBackStackEntry>,
+ navOptions: NavOptions?,
+ navigatorExtras: Extras?
+ ) {
+ entries.forEach { entry -> state.pushWithTransition(entry) }
+ }
+
+ override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
+ state.popWithTransition(popUpTo, savedState)
+ }
+ }
+
+ internal class SupportingPaneTestDestination(navigator: Navigator<out NavDestination>) :
+ NavDestination(navigator), SupportingPane
+
class TestViewModel : ViewModel() {
var wasCleared = false
diff --git a/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt b/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt
index beb1d0a..f26e922 100644
--- a/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt
+++ b/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt
@@ -25,6 +25,7 @@
import androidx.navigation.NavDestination
import androidx.navigation.NavViewModelStoreProvider
import androidx.navigation.NavigatorState
+import androidx.navigation.SupportingPane
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
@@ -174,6 +175,17 @@
} else {
Lifecycle.State.STARTED
}
+ previousEntry.destination is SupportingPane -> {
+ // Match the previous entry's destination, making sure
+ // a transitioning destination does not go to resumed
+ previousEntry.maxLifecycle.coerceAtMost(
+ if (!transitioning) {
+ Lifecycle.State.RESUMED
+ } else {
+ Lifecycle.State.STARTED
+ }
+ )
+ }
previousEntry.destination is FloatingWindow -> Lifecycle.State.STARTED
else -> Lifecycle.State.CREATED
}
diff --git a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
index 1e48878..db1406d 100644
--- a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
+++ b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
@@ -66,6 +66,7 @@
import androidx.pdf.widget.ZoomView
import androidx.pdf.widget.ZoomView.ZoomScroll
import com.google.android.material.floatingactionbutton.FloatingActionButton
+import java.io.IOException
import kotlinx.coroutines.launch
/**
@@ -298,7 +299,7 @@
}
}
},
- onDocumentLoadFailure = { thrown -> onLoadDocumentError(thrown) }
+ onDocumentLoadFailure = { thrown -> showLoadingErrorView(thrown) }
)
setUpEditFab()
@@ -494,11 +495,8 @@
savedState?.let { state ->
if (isFileRestoring) {
state.containsKey(KEY_LAYOUT_REACH).let {
- val layoutReach = state.getInt(KEY_LAYOUT_REACH, -1)
- if (layoutReach != -1) {
- layoutHandler?.pageLayoutReach = layoutReach
- layoutHandler?.setInitialPageLayoutReachWithMax(layoutReach)
- }
+ val layoutReach = state.getInt(KEY_LAYOUT_REACH)
+ layoutHandler?.setInitialPageLayoutReachWithMax(layoutReach)
}
// Restore page selection from saved state if it exists
@@ -584,7 +582,6 @@
pdfLoaderCallbacks?.pdfLoader = pdfLoader
layoutHandler = LayoutHandler(pdfLoader)
- paginatedView?.model?.size?.let { layoutHandler!!.pageLayoutReach = it }
val updatedSelectionModel = PdfSelectionModel(pdfLoader)
updateSelectionModel(updatedSelectionModel)
@@ -634,7 +631,7 @@
// app that owns it has been killed by the system. We will still recover,
// but log this.
viewState.set(ViewState.ERROR)
- onLoadDocumentError(e)
+ showLoadingErrorView(e)
}
}
}
@@ -658,7 +655,9 @@
}
private fun destroyContentModel() {
+
pdfLoader?.cancelAll()
+
paginationModel = null
selectionHandles?.destroy()
@@ -739,6 +738,13 @@
)
}
+ private fun showLoadingErrorView(error: Throwable) {
+ context?.resources?.getString(R.string.error_cannot_open_pdf)?.let {
+ loadingView?.showErrorView(it)
+ }
+ onLoadDocumentError(error)
+ }
+
private fun loadFile(fileUri: Uri) {
Preconditions.checkNotNull(fileUri)
Preconditions.checkArgument(
@@ -761,8 +767,13 @@
try {
validateFileUri(fileUri)
fetchFile(fileUri)
- } catch (e: SecurityException) {
- onLoadDocumentError(e)
+ } catch (error: Exception) {
+ when (error) {
+ is IOException,
+ is SecurityException,
+ is NullPointerException -> showLoadingErrorView(error)
+ else -> throw error
+ }
}
if (localUri != null && localUri != fileUri) {
annotationButton?.hide()
@@ -789,7 +800,7 @@
}
override fun failed(thrown: Throwable) {
- onLoadDocumentError(thrown)
+ showLoadingErrorView(thrown)
}
override fun progress(progress: Float) {}
diff --git a/pdf/pdf-viewer/build.gradle b/pdf/pdf-viewer/build.gradle
index 871b0a6..78bb274 100644
--- a/pdf/pdf-viewer/build.gradle
+++ b/pdf/pdf-viewer/build.gradle
@@ -21,7 +21,6 @@
id("com.android.library")
id("androidx.stableaidl")
id("kotlin-android")
- id ("kotlin-parcelize")
}
dependencies {
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
index ea847f0..3cdd35d 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
@@ -18,8 +18,6 @@
import android.content.Context;
import android.graphics.Rect;
-import android.os.Parcel;
-import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.View;
@@ -29,7 +27,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
-import androidx.core.os.ParcelCompat;
import androidx.pdf.ViewState;
import androidx.pdf.data.Range;
import androidx.pdf.util.PaginationUtils;
@@ -171,22 +168,6 @@
}
}
- @Nullable
- @Override
- protected Parcelable onSaveInstanceState() {
- Parcelable superState = super.onSaveInstanceState();
- return new SavedState(superState, mModel);
- }
-
- @Override
- protected void onRestoreInstanceState(Parcelable state) {
- SavedState savedState = (SavedState) state;
- super.onRestoreInstanceState(((SavedState) state).getSuperState());
- mModel = savedState.mModel;
- mPageRangeHandler = new PageRangeHandler(mModel);
- requestLayout();
- }
-
/**
* Returns the current viewport in content coordinates
*/
@@ -541,24 +522,4 @@
public boolean isConfigurationChanged() {
return mIsConfigurationChanged;
}
-
- static class SavedState extends View.BaseSavedState {
- final PaginationModel mModel;
-
- SavedState(Parcelable superState, PaginationModel model) {
- super(superState);
- mModel = model;
- }
-
- SavedState(Parcel source, ClassLoader loader) {
- super(source);
- mModel = ParcelCompat.readParcelable(source, loader, PaginationModel.class);
- }
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- super.writeToParcel(out, flags);
- out.writeParcelable(mModel, flags);
- }
- }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
index 6536157..8407a743 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
@@ -16,15 +16,11 @@
package androidx.pdf.viewer;
-import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Rect;
-import android.os.Parcel;
-import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
-import androidx.core.os.ParcelCompat;
import androidx.pdf.data.Range;
import androidx.pdf.models.Dimensions;
import androidx.pdf.util.PaginationUtils;
@@ -61,8 +57,7 @@
* pages are added
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("BanParcelableUsage")
-public class PaginationModel implements Parcelable {
+public class PaginationModel {
/**
* The spacing added before and after each page (the actual space between 2 consecutive pages is
* twice this distance), in pixels.
@@ -92,30 +87,6 @@
mPageSpacingPx = PaginationUtils.getPageSpacingInPixels(context);
}
- protected PaginationModel(@NonNull Parcel in) {
- PaginationModelData data = ParcelCompat.readParcelable(in, getClass().getClassLoader(),
- PaginationModelData.class);
- mPageSpacingPx = data.getPageSpacingPx();
- mMaxPages = data.getMaxPages();
- mPages = data.getPages();
- mPageStops = data.getPageStops();
- mSize = data.getSize();
- mEstimatedPageHeight = data.getEstimatedPageHeight();
- mAccumulatedPageSize = data.getAccumulatedPageSize();
- }
-
- public static final Creator<PaginationModel> CREATOR = new Creator<PaginationModel>() {
- @Override
- public PaginationModel createFromParcel(Parcel in) {
- return new PaginationModel(in);
- }
-
- @Override
- public PaginationModel[] newArray(int size) {
- return new PaginationModel[size];
- }
- };
-
/**
* Initializes the model.
*
@@ -293,6 +264,7 @@
}
+
/**
* Returns the location of the page in the model.
*
@@ -305,7 +277,7 @@
* maximizes the portion of that view that is visible on the screen
* </ul>
*
- * @param pageNum - index of requested page
+ * @param pageNum - index of requested page
* @param viewArea - the current viewport in content coordinates
* @return - coordinates of the page within this model
*/
@@ -419,16 +391,4 @@
mObservers.clear();
super.finalize();
}
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(@NonNull Parcel dest, int flags) {
- PaginationModelData data = new PaginationModelData(mPageSpacingPx, mMaxPages, mPages,
- mPageStops, mSize, mEstimatedPageHeight, mAccumulatedPageSize);
- dest.writeParcelable(data, flags);
- }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModelData.kt b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModelData.kt
deleted file mode 100644
index cfff185..0000000
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModelData.kt
+++ /dev/null
@@ -1,79 +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.viewer
-
-import android.annotation.SuppressLint
-import android.os.Parcelable
-import androidx.pdf.models.Dimensions
-import kotlinx.parcelize.Parcelize
-
-/** Parcelable representation of the data stored by [PaginationModel] */
-@Parcelize
-@SuppressLint("BanParcelableUsage")
-internal data class PaginationModelData(
- /** The space between pages in pixels */
- val pageSpacingPx: Int,
- /** The maximum number of pages in the model, i.e. the number of pages in the PDF */
- val maxPages: Int,
- /** The dimensions of all pages the model has received dimensions for, in content coordinates */
- val pages: Array<Dimensions>,
- /**
- * The bottom position of each page, in content coordinates. Derived from [pages] and
- * [pageSpacingPx], but stored separately to avoid re-computation.
- */
- val pageStops: IntArray,
- /** The number of pages for which dimensions are known */
- val size: Int,
- /**
- * The estimated height of the next PDF page, based on known dimensions. Derived from [pages]
- * and [size], but stored separately to avoid re-computation.
- */
- val estimatedPageHeight: Float,
- /**
- * The cumulative height of all pages for which dimensions are known. Derived from [pages], but
- * stored separately to avoid re-computation.
- */
- val accumulatedPageSize: Int,
-) : Parcelable {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as PaginationModelData
-
- if (pageSpacingPx != other.pageSpacingPx) return false
- if (maxPages != other.maxPages) return false
- if (!pages.contentEquals(other.pages)) return false
- if (!pageStops.contentEquals(other.pageStops)) return false
- if (size != other.size) return false
- if (estimatedPageHeight != other.estimatedPageHeight) return false
- if (accumulatedPageSize != other.accumulatedPageSize) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = pageSpacingPx
- result = 31 * result + maxPages
- result = 31 * result + pages.contentHashCode()
- result = 31 * result + pageStops.contentHashCode()
- result = 31 * result + size
- result = 31 * result + estimatedPageHeight.hashCode()
- result = 31 * result + accumulatedPageSize
- return result
- }
-}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
index 30ebfcb..6599830 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
@@ -228,9 +228,6 @@
"Document not loaded but status " + status.number
)
PdfStatus.PDF_ERROR -> {
- loadingView.showErrorView(
- context.resources.getString(R.string.error_cannot_open_pdf)
- )
handleError(status)
}
PdfStatus.FILE_ERROR,
@@ -259,7 +256,9 @@
override fun setPageDimensions(pageNum: Int, dimensions: Dimensions) {
if (viewState.get() != ViewState.NO_VIEW) {
+
paginatedView.model.addPage(pageNum, dimensions)
+
layoutHandler!!.pageLayoutReach = paginatedView.model.size
if (
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 86fed41..79fdec1 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
@@ -17,8 +17,11 @@
package androidx.privacysandbox.ui.integration.testapp
import android.app.Activity
+import android.graphics.Color
+import android.graphics.Typeface
import android.os.Bundle
import android.util.Log
+import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
@@ -59,6 +62,10 @@
}
}
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ getSandboxedSdkViews().forEach { it.addStateChangedListener() }
+ }
+
/** Returns a handle to the already loaded SDK. */
fun getSdkApi(): ISdkApi {
return sdkApi
@@ -119,6 +126,8 @@
val parent = view.parent as ViewGroup
val index = parent.indexOfChild(view)
val textView = TextView(requireActivity())
+ textView.setTypeface(null, Typeface.BOLD_ITALIC)
+ textView.setTextColor(Color.RED)
textView.text = state.throwable.message
requireActivity().runOnUiThread {
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
index 4a9ccf7..d3eba3b 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
@@ -104,6 +104,7 @@
} catch (e: Exception) {
Log.w(TAG, "Ad not loaded $e")
}
+ childSandboxedSdkView.addStateChangedListener()
sandboxedSdkViewSet.add(childSandboxedSdkView)
}
}
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt
index 3a3f32b..166d109 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt
@@ -67,7 +67,6 @@
}
private fun loadResizableBannerAd() {
- resizableBannerView.addStateChangedListener()
loadBannerAd(
currentAdType,
currentMediationOption,
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt
index e49be51..2d38fe7 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt
@@ -61,7 +61,6 @@
}
private fun loadBottomBannerAd() {
- bottomBannerView.addStateChangedListener()
bottomBannerView.layoutParams =
inflatedView.findViewById<LinearLayout>(R.id.bottom_banner_container).layoutParams
requireActivity().runOnUiThread {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt
index 5cb971c..1a1f6d9 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt
@@ -53,16 +53,19 @@
name: String,
annotations: List<XAnnotationSpec>
) = apply {
+ val paramSpec = ParameterSpec.builder(typeName.java, name, Modifier.FINAL)
actual.addParameter(
- ParameterSpec.builder(typeName.java, name, Modifier.FINAL)
- .apply {
- if (typeName.nullability == XNullability.NULLABLE) {
- addAnnotation(NULLABLE_ANNOTATION)
- } else if (typeName.nullability == XNullability.NONNULL) {
- addAnnotation(NONNULL_ANNOTATION)
- }
- }
- .build()
+ // Adding nullability annotation to primitive parameters is redundant as
+ // primitives can never be null.
+ if (typeName.isPrimitive) {
+ paramSpec.build()
+ } else {
+ when (typeName.nullability) {
+ XNullability.NULLABLE -> paramSpec.addAnnotation(NULLABLE_ANNOTATION)
+ XNullability.NONNULL -> paramSpec.addAnnotation(NONNULL_ANNOTATION)
+ else -> paramSpec
+ }.build()
+ }
)
// TODO(b/247247439): Add other annotations
}
diff --git a/room/room-compiler/build.gradle b/room/room-compiler/build.gradle
index 060f27d..ff92f34 100644
--- a/room/room-compiler/build.gradle
+++ b/room/room-compiler/build.gradle
@@ -90,6 +90,7 @@
testImplementation(libs.antlr4)
testImplementation(SdkHelperKt.getSdkDependency(project))
testImplementationAarAsJar(project(":room:room-runtime"))
+ testImplementationAarAsJar(project(":room:room-paging"))
testImplementationAarAsJar(project(":sqlite:sqlite"))
testImplementation(project(":internal-testutils-common"))
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
index 29cbd79..d34aa80 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
@@ -82,6 +82,7 @@
import org.hamcrest.CoreMatchers.notNullValue
import org.hamcrest.CoreMatchers.nullValue
import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -607,6 +608,7 @@
}
@Test
+ @Ignore("Temporarily disabling to unblock b/362512509")
fun testMissingRoomPaging() {
runProcessorTest { invocation ->
val pagingSourceElement =
diff --git a/room/room-compiler/src/test/test-data/daoWriter/input/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/input/ComplexDao.java
index ef3d537..6502b66 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/input/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/input/ComplexDao.java
@@ -16,6 +16,7 @@
package foo.bar;
import androidx.lifecycle.LiveData;
+import androidx.paging.PagingSource;
import androidx.room.*;
import androidx.sqlite.db.SupportSQLiteQuery;
import com.google.common.util.concurrent.ListenableFuture;
@@ -85,4 +86,7 @@
@RawQuery(observedEntities = User.class)
abstract public User getUserViaRawQuery(SupportSQLiteQuery rawQuery);
+
+ @Query("SELECT * FROM Child1 ORDER BY id ASC")
+ abstract public PagingSource<Integer, Child1> loadItems();
}
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/javac/withLambda/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/javac/withLambda/ComplexDao.java
index 0fa55d4..6821f7a 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/javac/withLambda/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/javac/withLambda/ComplexDao.java
@@ -3,8 +3,11 @@
import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
+import androidx.paging.PagingSource;
import androidx.room.RoomDatabase;
+import androidx.room.RoomRawQuery;
import androidx.room.guava.GuavaRoom;
+import androidx.room.paging.LimitOffsetPagingSource;
import androidx.room.util.CursorUtil;
import androidx.room.util.DBUtil;
import androidx.room.util.SQLiteStatementUtil;
@@ -627,6 +630,51 @@
}
@Override
+ public PagingSource<Integer, Child1> loadItems() {
+ final String _sql = "SELECT * FROM Child1 ORDER BY id ASC";
+ final RoomRawQuery _rawQuery = new RoomRawQuery(_sql);
+ return new LimitOffsetPagingSource<Child1>(_rawQuery, __db, "Child1") {
+ @Override
+ @NonNull
+ protected List<Child1> convertRows(@NonNull final SQLiteStatement statement,
+ final int itemCount) {
+ _rawQuery.getBindingFunction().invoke(statement);
+ final int _cursorIndexOfId = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "id");
+ final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "name");
+ final int _cursorIndexOfSerial = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "serial");
+ final int _cursorIndexOfCode = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "code");
+ final List<Child1> _result = new ArrayList<Child1>();
+ while (statement.step()) {
+ final Child1 _item;
+ final int _tmpId;
+ _tmpId = (int) (statement.getLong(_cursorIndexOfId));
+ final String _tmpName;
+ if (statement.isNull(_cursorIndexOfName)) {
+ _tmpName = null;
+ } else {
+ _tmpName = statement.getText(_cursorIndexOfName);
+ }
+ final Info _tmpInfo;
+ if (!(statement.isNull(_cursorIndexOfSerial) && statement.isNull(_cursorIndexOfCode))) {
+ _tmpInfo = new Info();
+ _tmpInfo.serial = (int) (statement.getLong(_cursorIndexOfSerial));
+ if (statement.isNull(_cursorIndexOfCode)) {
+ _tmpInfo.code = null;
+ } else {
+ _tmpInfo.code = statement.getText(_cursorIndexOfCode);
+ }
+ } else {
+ _tmpInfo = null;
+ }
+ _item = new Child1(_tmpId,_tmpName,_tmpInfo);
+ _result.add(_item);
+ }
+ return _result;
+ }
+ };
+ }
+
+ @Override
public User getUserViaRawQuery(final SupportSQLiteQuery rawQuery) {
__db.assertNotSuspendingTransaction();
final Cursor _cursor = DBUtil.query(__db, rawQuery, false, null);
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/javac/withoutLambda/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/javac/withoutLambda/ComplexDao.java
index 8074e12..b4dceb9 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/javac/withoutLambda/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/javac/withoutLambda/ComplexDao.java
@@ -4,8 +4,11 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
+import androidx.paging.PagingSource;
import androidx.room.RoomDatabase;
+import androidx.room.RoomRawQuery;
import androidx.room.guava.GuavaRoom;
+import androidx.room.paging.LimitOffsetPagingSource;
import androidx.room.util.CursorUtil;
import androidx.room.util.DBUtil;
import androidx.room.util.SQLiteStatementUtil;
@@ -690,6 +693,51 @@
}
@Override
+ public PagingSource<Integer, Child1> loadItems() {
+ final String _sql = "SELECT * FROM Child1 ORDER BY id ASC";
+ final RoomRawQuery _rawQuery = new RoomRawQuery(_sql);
+ return new LimitOffsetPagingSource<Child1>(_rawQuery, __db, "Child1") {
+ @Override
+ @NonNull
+ protected List<Child1> convertRows(@NonNull final SQLiteStatement statement,
+ final int itemCount) {
+ _rawQuery.getBindingFunction().invoke(statement);
+ final int _cursorIndexOfId = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "id");
+ final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "name");
+ final int _cursorIndexOfSerial = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "serial");
+ final int _cursorIndexOfCode = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "code");
+ final List<Child1> _result = new ArrayList<Child1>();
+ while (statement.step()) {
+ final Child1 _item;
+ final int _tmpId;
+ _tmpId = (int) (statement.getLong(_cursorIndexOfId));
+ final String _tmpName;
+ if (statement.isNull(_cursorIndexOfName)) {
+ _tmpName = null;
+ } else {
+ _tmpName = statement.getText(_cursorIndexOfName);
+ }
+ final Info _tmpInfo;
+ if (!(statement.isNull(_cursorIndexOfSerial) && statement.isNull(_cursorIndexOfCode))) {
+ _tmpInfo = new Info();
+ _tmpInfo.serial = (int) (statement.getLong(_cursorIndexOfSerial));
+ if (statement.isNull(_cursorIndexOfCode)) {
+ _tmpInfo.code = null;
+ } else {
+ _tmpInfo.code = statement.getText(_cursorIndexOfCode);
+ }
+ } else {
+ _tmpInfo = null;
+ }
+ _item = new Child1(_tmpId,_tmpName,_tmpInfo);
+ _result.add(_item);
+ }
+ return _result;
+ }
+ };
+ }
+
+ @Override
public User getUserViaRawQuery(final SupportSQLiteQuery rawQuery) {
__db.assertNotSuspendingTransaction();
final Cursor _cursor = DBUtil.query(__db, rawQuery, false, null);
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
index 54daec0..9b4ca9b 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
@@ -3,8 +3,11 @@
import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
+import androidx.paging.PagingSource;
import androidx.room.RoomDatabase;
+import androidx.room.RoomRawQuery;
import androidx.room.guava.GuavaRoom;
+import androidx.room.paging.LimitOffsetPagingSource;
import androidx.room.util.CursorUtil;
import androidx.room.util.DBUtil;
import androidx.room.util.SQLiteStatementUtil;
@@ -622,6 +625,51 @@
});
}
+ @Override
+ public PagingSource<Integer, Child1> loadItems() {
+ final String _sql = "SELECT * FROM Child1 ORDER BY id ASC";
+ final RoomRawQuery _rawQuery = new RoomRawQuery(_sql);
+ return new LimitOffsetPagingSource<Child1>(_rawQuery, __db, "Child1") {
+ @Override
+ @NonNull
+ protected List<Child1> convertRows(@NonNull final SQLiteStatement statement,
+ final int itemCount) {
+ _rawQuery.getBindingFunction().invoke(statement);
+ final int _cursorIndexOfId = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "id");
+ final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "name");
+ final int _cursorIndexOfSerial = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "serial");
+ final int _cursorIndexOfCode = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "code");
+ final List<Child1> _result = new ArrayList<Child1>();
+ while (statement.step()) {
+ final Child1 _item;
+ final int _tmpId;
+ _tmpId = (int) (statement.getLong(_cursorIndexOfId));
+ final String _tmpName;
+ if (statement.isNull(_cursorIndexOfName)) {
+ _tmpName = null;
+ } else {
+ _tmpName = statement.getText(_cursorIndexOfName);
+ }
+ final Info _tmpInfo;
+ if (!(statement.isNull(_cursorIndexOfSerial) && statement.isNull(_cursorIndexOfCode))) {
+ _tmpInfo = new Info();
+ _tmpInfo.serial = (int) (statement.getLong(_cursorIndexOfSerial));
+ if (statement.isNull(_cursorIndexOfCode)) {
+ _tmpInfo.code = null;
+ } else {
+ _tmpInfo.code = statement.getText(_cursorIndexOfCode);
+ }
+ } else {
+ _tmpInfo = null;
+ }
+ _item = new Child1(_tmpId,_tmpName,_tmpInfo);
+ _result.add(_item);
+ }
+ return _result;
+ }
+ };
+ }
+
@Override
public User getUserViaRawQuery(final SupportSQLiteQuery rawQuery) {
__db.assertNotSuspendingTransaction();
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
index 5115d6f..0deb550 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
@@ -22,8 +22,8 @@
import androidx.sqlite.SQLiteConnection
import java.util.concurrent.Callable
import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
/**
* A LiveData implementation that closely works with [InvalidationTracker] to implement database
@@ -53,6 +53,17 @@
private val computing = AtomicBoolean(false)
private val registeredObserver = AtomicBoolean(false)
+ private val launchContext =
+ if (database.inCompatibilityMode()) {
+ if (inTransaction) {
+ database.getTransactionContext()
+ } else {
+ database.getQueryContext()
+ }
+ } else {
+ EmptyCoroutineContext
+ }
+
private suspend fun refresh() {
if (registeredObserver.compareAndSet(false, true)) {
database.invalidationTracker.subscribe(
@@ -105,7 +116,7 @@
val isActive = hasActiveObservers()
if (invalid.compareAndSet(false, true)) {
if (isActive) {
- database.getCoroutineScope().launch { refresh() }
+ database.getCoroutineScope().launch(launchContext) { refresh() }
}
}
}
@@ -115,7 +126,7 @@
override fun onActive() {
super.onActive()
container.onActive(this)
- database.getCoroutineScope().launch { refresh() }
+ database.getCoroutineScope().launch(launchContext) { refresh() }
}
override fun onInactive() {
@@ -132,13 +143,7 @@
private val callableFunction: Callable<T?>
) : RoomTrackingLiveData<T>(database, container, inTransaction, tableNames) {
override suspend fun compute(): T? {
- val queryContext =
- if (inTransaction) {
- database.getTransactionContext()
- } else {
- database.getQueryContext()
- }
- return withContext(queryContext) { callableFunction.call() }
+ return callableFunction.call()
}
}
diff --git a/settings.gradle b/settings.gradle
index 988cca5..ca5d8f6 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -29,7 +29,12 @@
classpath("com.gradle:develocity-gradle-plugin:3.18")
classpath("com.gradle:common-custom-user-data-gradle-plugin:2.0.1")
classpath("androidx.build.gradle.gcpbuildcache:gcpbuildcache:1.0.0-beta10")
- classpath("com.android.settings:com.android.settings.gradle.plugin:8.7.0-alpha02")
+ def agpOverride = System.getenv("GRADLE_PLUGIN_VERSION")
+ if (agpOverride != null) {
+ classpath("com.android.settings:com.android.settings.gradle.plugin:$agpOverride")
+ } else {
+ classpath("com.android.settings:com.android.settings.gradle.plugin:8.7.0-alpha02")
+ }
}
}
diff --git a/wear/compose/compose-foundation/api/current.txt b/wear/compose/compose-foundation/api/current.txt
index ea38e1a..596925c 100644
--- a/wear/compose/compose-foundation/api/current.txt
+++ b/wear/compose/compose-foundation/api/current.txt
@@ -388,8 +388,10 @@
public sealed interface LazyColumnLayoutInfo {
method public int getTotalItemsCount();
+ method public long getViewportSize();
method public java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> getVisibleItems();
property public abstract int totalItemsCount;
+ property public abstract long viewportSize;
property public abstract java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> visibleItems;
}
diff --git a/wear/compose/compose-foundation/api/restricted_current.txt b/wear/compose/compose-foundation/api/restricted_current.txt
index ea38e1a..596925c 100644
--- a/wear/compose/compose-foundation/api/restricted_current.txt
+++ b/wear/compose/compose-foundation/api/restricted_current.txt
@@ -388,8 +388,10 @@
public sealed interface LazyColumnLayoutInfo {
method public int getTotalItemsCount();
+ method public long getViewportSize();
method public java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> getVisibleItems();
property public abstract int totalItemsCount;
+ property public abstract long viewportSize;
property public abstract java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> visibleItems;
}
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfoTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfoTest.kt
new file mode 100644
index 0000000..f07a6b7
--- /dev/null
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfoTest.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.foundation.lazy
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+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.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+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 com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyColumnLayoutInfoTest {
+ @get:Rule val rule = createComposeRule()
+
+ private var itemSizePx: Int = 50
+ private var itemSizeDp: Dp = Dp.Infinity
+
+ @Before
+ fun before() {
+ with(rule.density) { itemSizeDp = itemSizePx.toDp() }
+ }
+
+ @Test
+ fun visibleItemsAreCorrect() {
+ lateinit var state: LazyColumnState
+
+ rule.setContent {
+ LazyColumn(
+ state = rememberLazyColumnState().also { state = it },
+ // Viewport take 4 items, item 0 is exactly above the center and there is space for
+ // two more items below the center line.
+ modifier = Modifier.requiredSize(itemSizeDp * 4f),
+ verticalArrangement = Arrangement.spacedBy(0.dp)
+ ) {
+ items((0..5).toList()) { Box(Modifier.requiredSize(itemSizeDp)) }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportSize.height).isEqualTo(itemSizePx * 4)
+ // Start offset compensates for the layout where the first item is exactly above the
+ // center line.
+ state.layoutInfo.assertVisibleItems(count = 3, startOffset = itemSizePx)
+ }
+ }
+
+ @Test
+ fun visibleItemsAreCorrectWithSpacing() {
+ lateinit var state: LazyColumnState
+
+ rule.setContent {
+ LazyColumn(
+ state = rememberLazyColumnState().also { state = it },
+ // Viewport take 4 items, item 0 is exactly above the center and there is space for
+ // two more items below the center line.
+ modifier = Modifier.requiredSize(itemSizeDp * 4f),
+ verticalArrangement = Arrangement.spacedBy(itemSizeDp),
+ ) {
+ items((0..5).toList()) { Box(Modifier.requiredSize(itemSizeDp)) }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportSize.height).isEqualTo(itemSizePx * 4)
+ // Start offset compensates for the layout where the first item is exactly above the
+ // center line.
+ state.layoutInfo.assertVisibleItems(
+ count = 2,
+ spacing = itemSizePx,
+ startOffset = itemSizePx
+ )
+ }
+ }
+
+ @Test
+ fun visibleItemsAreObservableWhenResize() {
+ lateinit var state: LazyColumnState
+ var size by mutableStateOf(itemSizeDp * 2)
+ var currentInfo: LazyColumnLayoutInfo? = null
+ @Composable
+ fun observingFun() {
+ currentInfo = state.layoutInfo
+ }
+ rule.setContent {
+ LazyColumn(
+ state = rememberLazyColumnState().also { state = it },
+ modifier = Modifier.requiredSize(itemSizeDp * 4f)
+ ) {
+ item { Box(Modifier.requiredSize(size)) }
+ }
+ observingFun()
+ }
+
+ rule.runOnIdle {
+ assertThat(currentInfo).isNotNull()
+ currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx * 2)
+ currentInfo = null
+ size = itemSizeDp
+ }
+
+ rule.runOnIdle {
+ assertThat(currentInfo).isNotNull()
+ currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx)
+ }
+ }
+
+ @Test
+ fun totalCountIsCorrect() {
+ var count by mutableStateOf(10)
+ lateinit var state: LazyColumnState
+ rule.setContent {
+ LazyColumn(state = rememberLazyColumnState().also { state = it }) {
+ items((0 until count).toList()) { Box(Modifier.requiredSize(10.dp)) }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.totalItemsCount).isEqualTo(10)
+ count = 20
+ }
+
+ rule.runOnIdle { assertThat(state.layoutInfo.totalItemsCount).isEqualTo(20) }
+ }
+
+ @Test
+ fun viewportOffsetsAndSizeAreCorrect() {
+ val sizePx = 45
+ val sizeDp = with(rule.density) { sizePx.toDp() }
+ lateinit var state: LazyColumnState
+ rule.setContent {
+ LazyColumn(
+ Modifier.height(sizeDp).width(sizeDp * 2),
+ state = rememberLazyColumnState().also { state = it }
+ ) {
+ items((0..3).toList()) { Box(Modifier.requiredSize(sizeDp)) }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportSize).isEqualTo(IntSize(sizePx * 2, sizePx))
+ }
+ }
+
+ private fun LazyColumnLayoutInfo.assertVisibleItems(
+ count: Int,
+ startIndex: Int = 0,
+ startOffset: Int = 0,
+ expectedSize: Int = itemSizePx,
+ spacing: Int = 0
+ ) {
+ assertThat(visibleItems.size).isEqualTo(count)
+ var currentIndex = startIndex
+ var currentOffset = startOffset
+ visibleItems.forEach {
+ assertThat(it.index).isEqualTo(currentIndex)
+ assertWithMessage("Offset of item $currentIndex")
+ .that(it.offset)
+ .isEqualTo(currentOffset)
+ assertThat(it.height).isEqualTo(expectedSize)
+ currentIndex++
+ currentOffset += it.height + spacing
+ }
+ }
+}
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt
index c5002a0..14fc4c4 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt
@@ -16,6 +16,8 @@
package androidx.wear.compose.foundation.lazy
+import androidx.compose.ui.unit.IntSize
+
/**
* Scroll progress of an item in a [LazyColumn] before any modifications to the item's height are
* applied (using [LazyColumnItemScope.transformedHeight] modifier).
@@ -44,21 +46,26 @@
sealed interface LazyColumnVisibleItemInfo {
/** The index of the item in the underlying data source. */
val index: Int
+
/** The offset of the item from the start of the visible area. */
val offset: Int
+
/** The height of the item after applying any height changes. */
val height: Int
+
/** The scroll progress of the item, indicating its position within the visible area. */
val scrollProgress: LazyColumnItemScrollProgress
}
/** Holds the layout information for a [LazyColumn]. */
sealed interface LazyColumnLayoutInfo {
+
/** A list of [LazyColumnVisibleItemInfo] objects representing the visible items in the list. */
val visibleItems: List<LazyColumnVisibleItemInfo>
/** The total count of items passed to [LazyColumn]. */
val totalItemsCount: Int
- // TODO: b/352686661 - Expose more properties related to layout.
+ /** The size of the viewport in pixels. */
+ val viewportSize: IntSize
}
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
index 3f316f2..1bd6f3e 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
@@ -17,6 +17,7 @@
package androidx.wear.compose.foundation.lazy
import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.unit.IntSize
/** The result of the measure pass of the [LazyColumn]. */
internal class LazyColumnMeasureResult(
@@ -32,4 +33,8 @@
override val visibleItems: List<LazyColumnVisibleItemInfo>,
/** see [LazyColumnLayoutInfo.totalItemsCount] */
override val totalItemsCount: Int,
-) : LazyColumnLayoutInfo, MeasureResult by measureResult
+) : LazyColumnLayoutInfo, MeasureResult by measureResult {
+ /** see [LazyColumnLayoutInfo.viewportSize] */
+ override val viewportSize: IntSize
+ get() = IntSize(width = width, height = height)
+}
diff --git a/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ProgressIndicatorBenchmark.kt b/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ProgressIndicatorBenchmark.kt
new file mode 100644
index 0000000..e9e624a
--- /dev/null
+++ b/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ProgressIndicatorBenchmark.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.wear.compose.material3.benchmark
+
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.wear.compose.material3.CircularProgressIndicator
+import androidx.wear.compose.material3.MaterialTheme
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class ProgressIndicatorBenchmark {
+ @get:Rule val benchmarkRule = ComposeBenchmarkRule()
+
+ private val testCaseFactory = { ProgressIndicatorTestCase() }
+
+ @Test
+ fun first_pixel() {
+ benchmarkRule.benchmarkToFirstPixel(testCaseFactory)
+ }
+}
+
+internal class ProgressIndicatorTestCase : LayeredComposeTestCase() {
+ @Composable
+ override fun MeasuredContent() {
+ CircularProgressIndicator(progress = { 0.5f })
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme { content() }
+ }
+}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt
index e049442..7074d90 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt
@@ -64,12 +64,12 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = false,
- showText = false,
showContent = false,
showTwoButtons = false,
scrollToBottom = false,
screenSize = screenSize,
- titleText = "Network error"
+ titleText = "Network error",
+ messageText = null
)
@Test
@@ -78,12 +78,12 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = false,
- showText = false,
showContent = false,
showTwoButtons = true,
scrollToBottom = false,
screenSize = screenSize,
- titleText = "Network error"
+ titleText = "Network error",
+ messageText = null
)
@Test
@@ -92,11 +92,11 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = false,
- showText = false,
showContent = false,
showTwoButtons = false,
scrollToBottom = false,
screenSize = screenSize,
+ messageText = null,
)
@Test
@@ -105,11 +105,11 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = false,
- showText = false,
showContent = false,
showTwoButtons = true,
scrollToBottom = false,
- screenSize = screenSize
+ screenSize = screenSize,
+ messageText = null
)
@Test
@@ -118,11 +118,11 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = true,
- showText = false,
showContent = false,
showTwoButtons = false,
scrollToBottom = false,
- screenSize = screenSize
+ screenSize = screenSize,
+ messageText = null
)
}
@@ -132,11 +132,11 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = true,
- showText = false,
showContent = false,
showTwoButtons = true,
scrollToBottom = false,
- screenSize = screenSize
+ screenSize = screenSize,
+ messageText = null
)
}
@@ -146,7 +146,6 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = true,
- showText = true,
showContent = false,
showTwoButtons = false,
scrollToBottom = false,
@@ -162,7 +161,6 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = true,
- showText = true,
showContent = true,
showTwoButtons = false,
scrollToBottom = false,
@@ -178,7 +176,6 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = true,
- showText = true,
showContent = true,
showTwoButtons = false,
scrollToBottom = true,
@@ -194,7 +191,6 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = true,
- showText = true,
showContent = true,
showTwoButtons = true,
scrollToBottom = true,
@@ -202,15 +198,43 @@
)
}
+ @Test
+ fun alert_title_longMessageText_bottomButton(@TestParameter screenSize: ScreenSize) {
+ rule.verifyAlertDialogScreenshot(
+ testName = testName,
+ screenshotRule = screenshotRule,
+ showIcon = false,
+ showContent = false,
+ showTwoButtons = false,
+ scrollToBottom = false,
+ screenSize = screenSize,
+ messageText = longMessageText
+ )
+ }
+
+ @Test
+ fun alert_title_longMessageText_confirmDismissButtons(@TestParameter screenSize: ScreenSize) {
+ rule.verifyAlertDialogScreenshot(
+ testName = testName,
+ screenshotRule = screenshotRule,
+ showIcon = false,
+ showContent = false,
+ showTwoButtons = true,
+ scrollToBottom = false,
+ screenSize = screenSize,
+ messageText = longMessageText
+ )
+ }
+
private fun ComposeContentTestRule.verifyAlertDialogScreenshot(
testName: TestName,
screenshotRule: AndroidXScreenshotTestRule,
showIcon: Boolean,
- showText: Boolean,
showContent: Boolean,
showTwoButtons: Boolean,
scrollToBottom: Boolean,
screenSize: ScreenSize,
+ messageText: String? = "Your battery is low. Turn on battery saver.",
titleText: String = "Mobile network is not currently available"
) {
setContentWithTheme() {
@@ -239,8 +263,8 @@
} else null,
showTwoButtons = showTwoButtons,
text =
- if (showText) {
- { Text("Your battery is low. Turn on battery saver.") }
+ if (messageText != null) {
+ { Text(messageText) }
} else null,
content =
if (showContent) {
@@ -308,3 +332,6 @@
)
}
}
+
+internal const val longMessageText =
+ "Allow Map to access your location even when you're not using the app? Your location is used to automatically map places to activities."
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
index 6e59369..f5f3893 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
@@ -259,6 +259,48 @@
}
@Test
+ fun calls_onDismissRequest_when_dialogBottomButton_becomes_hidden() {
+ val show = mutableStateOf(true)
+ var dismissed = false
+
+ rule.setContentWithTheme {
+ AlertDialog(
+ modifier = Modifier.testTag(TEST_TAG),
+ title = {},
+ bottomButton = {},
+ onDismissRequest = { dismissed = true },
+ show = show.value
+ )
+ }
+ rule.waitForIdle()
+ show.value = false
+
+ rule.waitForIdle()
+ assert(dismissed)
+ }
+
+ @Test
+ fun calls_onDismissRequest_when_dialogConfirmDismissButtons_becomes_hidden() {
+ val show = mutableStateOf(true)
+ var dismissed = false
+
+ rule.setContentWithTheme {
+ AlertDialog(
+ modifier = Modifier.testTag(TEST_TAG),
+ title = {},
+ confirmButton = {},
+ onDismissRequest = { dismissed = true },
+ show = show.value
+ )
+ }
+ rule.waitForIdle()
+ show.value = false
+
+ rule.waitForIdle()
+ assert(dismissed)
+ }
+
+ @Test
fun applies_correct_titleProperties() {
var expectedContentColor: Color = Color.Unspecified
var expectedTextStyle: TextStyle = TextStyle.Default
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ConfirmationTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ConfirmationTest.kt
index 31206a1..31069ee 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ConfirmationTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ConfirmationTest.kt
@@ -212,6 +212,86 @@
}
@Test
+ fun calls_onDismissRequest_when_confirmationLinearText_becomes_hidden() {
+ val show = mutableStateOf(true)
+ var dismissed = false
+
+ rule.setContentWithTheme {
+ Confirmation(
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = { dismissed = true },
+ text = {},
+ show = show.value
+ ) {}
+ }
+ rule.waitForIdle()
+ show.value = false
+
+ rule.waitForIdle()
+ assert(dismissed)
+ }
+
+ @Test
+ fun calls_onDismissRequest_when_confirmationCurvedText_becomes_hidden() {
+ val show = mutableStateOf(true)
+ var dismissed = false
+
+ rule.setContentWithTheme {
+ Confirmation(
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = { dismissed = true },
+ curvedText = {},
+ show = show.value
+ ) {}
+ }
+ rule.waitForIdle()
+ show.value = false
+
+ rule.waitForIdle()
+ assert(dismissed)
+ }
+
+ @Test
+ fun calls_onDismissRequest_when_successConfirmation_becomes_hidden() {
+ val show = mutableStateOf(true)
+ var dismissed = false
+
+ rule.setContentWithTheme {
+ SuccessConfirmation(
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = { dismissed = true },
+ curvedText = {},
+ show = show.value
+ )
+ }
+ rule.waitForIdle()
+ show.value = false
+
+ rule.waitForIdle()
+ assert(dismissed)
+ }
+
+ @Test
+ fun calls_onDismissRequest_when_failureConfirmation_becomes_hidden() {
+ val show = mutableStateOf(true)
+ var dismissed = false
+
+ rule.setContentWithTheme {
+ FailureConfirmation(
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = { dismissed = true },
+ curvedText = {},
+ show = show.value
+ )
+ }
+ rule.waitForIdle()
+ show.value = false
+
+ rule.waitForIdle()
+ assert(dismissed)
+ }
+
+ @Test
fun confirmation_displays_icon_with_linearText() {
rule.setContentWithTheme {
Confirmation(
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/OpenOnPhoneDialogTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/OpenOnPhoneDialogTest.kt
index c5de9c1..e8b933a 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/OpenOnPhoneDialogTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/OpenOnPhoneDialogTest.kt
@@ -68,6 +68,25 @@
}
@Test
+ fun calls_onDismissRequest_when_openOnPhone_becomes_hidden() {
+ val show = mutableStateOf(true)
+ var dismissed = false
+
+ rule.setContentWithTheme {
+ OpenOnPhoneDialog(
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = { dismissed = true },
+ show = show.value
+ )
+ }
+ rule.waitForIdle()
+ show.value = false
+
+ rule.waitForIdle()
+ assert(dismissed)
+ }
+
+ @Test
fun hides_openOnPhone_when_show_false() {
rule.setContentWithTheme {
OpenOnPhoneDialog(
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
index f0cc000..a20ffa0 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
@@ -67,9 +67,11 @@
* Example of an [AlertDialog] with an icon, title and two buttons to confirm and dismiss:
*
* @sample androidx.wear.compose.material3.samples.AlertDialogWithConfirmAndDismissSample
- * @param show A boolean indicating whether the dialog should be displayed.
+ * @param show A boolean indicating whether the dialog should be displayed. When set to true an
+ * 'intro' animation is triggered and the dialog is displayed. Subsequently, when set to false an
+ * 'outro' animation is triggered, then [onDismissRequest] is called and dialog becomes hidden.
* @param onDismissRequest A lambda function to be called when the dialog is dismissed by swiping
- * right (typically also called by the [dismissButton]).
+ * right (typically also called by the [dismissButton]) or by other dismiss action.
* @param confirmButton A slot for a [Button] indicating positive sentiment. Clicking the button
* must remove the dialog from the composition hierarchy. It's recommended to use
* [AlertDialogDefaults.ConfirmButton] in this slot with onClick callback.
@@ -138,7 +140,9 @@
* Example of an [AlertDialog] with content groups and a bottom [EdgeButton]:
*
* @sample androidx.wear.compose.material3.samples.AlertDialogWithContentGroupsSample
- * @param show A boolean indicating whether the dialog should be displayed.
+ * @param show A boolean indicating whether the dialog should be displayed. When set to true an
+ * 'intro' animation is triggered and the dialog is displayed. Subsequently, when set to false an
+ * 'outro' animation is triggered, then [onDismissRequest] is called and dialog becomes hidden.
* @param onDismissRequest A lambda function to be called when the dialog is dismissed by swiping to
* the right or by other dismiss action.
* @param bottomButton A slot for a [EdgeButton] indicating positive sentiment. Clicking the button
@@ -338,7 +342,7 @@
alertButtonsParams: AlertButtonsParams,
content: (ScalingLazyListScope.() -> Unit)?
) {
- val state = rememberScalingLazyListState()
+ val state = rememberScalingLazyListState(initialCenterItemIndex = 0)
Dialog(
showDialog = show,
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt
index 0a94d14..2328f65 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt
@@ -70,9 +70,12 @@
* Example of a [Confirmation] with an icon and a curved text content:
*
* @sample androidx.wear.compose.material3.samples.ConfirmationSample
- * @param show A boolean indicating whether the confirmation should be displayed.
+ * @param show A boolean indicating whether the confirmation should be displayed. When set to true
+ * an 'intro' animation is triggered and the confirmation is displayed. Subsequently, when set to
+ * false an 'outro' animation is triggered, then [onDismissRequest] is called and confirmation
+ * becomes hidden.
* @param onDismissRequest A lambda function to be called when the dialog is dismissed - either by
- * swiping right or when the [durationMillis] has passed.
+ * swiping right, when the [durationMillis] has passed or by other dismiss action.
* @param curvedText A slot for displaying curved text content which will be shown along the bottom
* edge of the dialog.
* @param modifier Modifier to be applied to the confirmation content.
@@ -120,9 +123,12 @@
* Example of a [Confirmation] with an icon and a text which fits into 3 lines:
*
* @sample androidx.wear.compose.material3.samples.LongTextConfirmationSample
- * @param show A boolean indicating whether the confirmation should be displayed.
+ * @param show A boolean indicating whether the confirmation should be displayed. When set to true
+ * an 'intro' animation is triggered and the confirmation is displayed. Subsequently, when set to
+ * false an 'outro' animation is triggered, then [onDismissRequest] is called and confirmation
+ * becomes hidden.
* @param onDismissRequest A lambda function to be called when the dialog is dismissed - either by
- * swiping right or when the [durationMillis] has passed.
+ * swiping right, when the [durationMillis] has passed or by other dismiss action.
* @param text A slot for displaying text below the icon. It should not exceed 3 lines.
* @param modifier Modifier to be applied to the confirmation content.
* @param colors A [ConfirmationColors] object for customizing the colors used in this
@@ -210,9 +216,12 @@
* Example of a [SuccessConfirmation] usage:
*
* @sample androidx.wear.compose.material3.samples.SuccessConfirmationSample
- * @param show A boolean indicating whether the confirmation should be displayed.
+ * @param show A boolean indicating whether the confirmation should be displayed. When set to true
+ * an 'intro' animation is triggered and the confirmation is displayed. Subsequently, when set to
+ * false an 'outro' animation is triggered, then [onDismissRequest] is called and confirmation
+ * becomes hidden.
* @param onDismissRequest A lambda function to be called when the dialog is dismissed - either by
- * swiping right or when the [durationMillis] has passed.
+ * swiping right, when the [durationMillis] has passed or by other dismiss action.
* @param modifier Modifier to be applied to the confirmation content.
* @param curvedText A slot for displaying curved text content which will be shown along the bottom
* edge of the dialog. Defaults to a localized success message.
@@ -259,9 +268,12 @@
* Example of a [FailureConfirmation] usage:
*
* @sample androidx.wear.compose.material3.samples.FailureConfirmationSample
- * @param show A boolean indicating whether the confirmation should be displayed.
+ * @param show A boolean indicating whether the confirmation should be displayed. When set to true
+ * an 'intro' animation is triggered and the confirmation is displayed. Subsequently, when set to
+ * false an 'outro' animation is triggered, then [onDismissRequest] is called and confirmation
+ * becomes hidden.
* @param onDismissRequest A lambda function to be called when the dialog is dismissed - either by
- * swiping right or when the [durationMillis] has passed.
+ * swiping right, when the [durationMillis] has passed or by other dismiss action.
* @param modifier Modifier to be applied to the confirmation content.
* @param curvedText A slot for displaying curved text content which will be shown along the bottom
* edge of the dialog. Defaults to a localized failure message.
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Dialog.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Dialog.kt
index f5c6dc6..c119f66 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Dialog.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Dialog.kt
@@ -103,18 +103,17 @@
transitionState.targetState = DialogVisibility.Hide
}
}
-
- LaunchedEffect(transitionState.currentState) {
- if (
- pendingOnDismissCall &&
- transitionState.currentState == DialogVisibility.Hide &&
- transitionState.isIdle
- ) {
- // After the outro animation, leave the dialog & reset alpha/scale transitions.
- onDismissRequest()
- pendingOnDismissCall = false
- }
- }
+ }
+ }
+ LaunchedEffect(transitionState.currentState) {
+ if (
+ pendingOnDismissCall &&
+ transitionState.currentState == DialogVisibility.Hide &&
+ transitionState.isIdle
+ ) {
+ // After the outro animation, leave the dialog & reset alpha/scale transitions.
+ onDismissRequest()
+ pendingOnDismissCall = false
}
}
}
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 fa819c7..bea3905 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
@@ -72,9 +72,11 @@
* Example of an [OpenOnPhoneDialog] usage:
*
* @sample androidx.wear.compose.material3.samples.OpenOnPhoneDialogSample
- * @param show A boolean indicating whether the dialog should be displayed.
+ * @param show A boolean indicating whether the dialog should be displayed. When set to true an
+ * 'intro' animation is triggered and the dialog is displayed. Subsequently, when set to false an
+ * 'outro' animation is triggered, then [onDismissRequest] is called and dialog becomes hidden.
* @param onDismissRequest A lambda function to be called when the dialog is dismissed - either by
- * swiping right or when the [durationMillis] has passed.
+ * swiping right, when the [durationMillis] has passed or by other dismiss action.
* @param modifier Modifier to be applied to the dialog content.
* @param curvedText A slot for displaying curved text content which will be shown along the bottom
* edge of the dialog. Defaults to a localized open on phone message.
diff --git a/window/window-core/api/current.txt b/window/window-core/api/current.txt
index 2c8a4d2..8b80f00 100644
--- a/window/window-core/api/current.txt
+++ b/window/window-core/api/current.txt
@@ -32,9 +32,9 @@
method public int getMinWidthDp();
method @Deprecated public androidx.window.core.layout.WindowHeightSizeClass getWindowHeightSizeClass();
method @Deprecated public androidx.window.core.layout.WindowWidthSizeClass getWindowWidthSizeClass();
- method public boolean isAtLeast(int widthDp, int heightDp);
- method public boolean isHeightAtLeast(int heightDp);
- method public boolean isWidthAtLeast(int widthDp);
+ method public boolean isAtLeastBreakpoint(int widthBreakpointDp, int heightBreakpointDp);
+ method public boolean isHeightAtLeastBreakpoint(int heightBreakpointDp);
+ method public boolean isWidthAtLeastBreakpoint(int widthBreakpointDp);
property public final int minHeightDp;
property public final int minWidthDp;
property @Deprecated public final androidx.window.core.layout.WindowHeightSizeClass windowHeightSizeClass;
diff --git a/window/window-core/api/restricted_current.txt b/window/window-core/api/restricted_current.txt
index 2c8a4d2..8b80f00 100644
--- a/window/window-core/api/restricted_current.txt
+++ b/window/window-core/api/restricted_current.txt
@@ -32,9 +32,9 @@
method public int getMinWidthDp();
method @Deprecated public androidx.window.core.layout.WindowHeightSizeClass getWindowHeightSizeClass();
method @Deprecated public androidx.window.core.layout.WindowWidthSizeClass getWindowWidthSizeClass();
- method public boolean isAtLeast(int widthDp, int heightDp);
- method public boolean isHeightAtLeast(int heightDp);
- method public boolean isWidthAtLeast(int widthDp);
+ method public boolean isAtLeastBreakpoint(int widthBreakpointDp, int heightBreakpointDp);
+ method public boolean isHeightAtLeastBreakpoint(int heightBreakpointDp);
+ method public boolean isWidthAtLeastBreakpoint(int widthBreakpointDp);
property public final int minHeightDp;
property public final int minWidthDp;
property @Deprecated public final androidx.window.core.layout.WindowHeightSizeClass windowHeightSizeClass;
diff --git a/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt b/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt
index 43bef20..847ea73 100644
--- a/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt
+++ b/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt
@@ -80,25 +80,28 @@
get() = WindowHeightSizeClass.compute(minHeightDp.toFloat())
/**
- * Returns `true` when [widthDp] is greater than or equal to [minWidthDp], `false` otherwise.
+ * Returns `true` when [minWidthDp] is greater than or equal to [widthBreakpointDp], `false`
+ * otherwise.
*/
- fun isWidthAtLeast(widthDp: Int): Boolean {
- return widthDp >= minWidthDp
+ fun isWidthAtLeastBreakpoint(widthBreakpointDp: Int): Boolean {
+ return minWidthDp >= widthBreakpointDp
}
/**
- * Returns `true` when [heightDp] is greater than or equal to [minHeightDp], `false` otherwise.
+ * Returns `true` when [minHeightDp] is greater than or equal to [heightBreakpointDp], `false`
+ * otherwise.
*/
- fun isHeightAtLeast(heightDp: Int): Boolean {
- return heightDp >= minHeightDp
+ fun isHeightAtLeastBreakpoint(heightBreakpointDp: Int): Boolean {
+ return minHeightDp >= heightBreakpointDp
}
/**
- * Returns `true` when [widthDp] is greater than or equal to [minWidthDp] and [heightDp] is
- * greater than or equal to [minHeightDp], `false` otherwise.
+ * Returns `true` when [widthBreakpointDp] is greater than or equal to [minWidthDp] and
+ * [heightBreakpointDp] is greater than or equal to [minHeightDp], `false` otherwise.
*/
- fun isAtLeast(widthDp: Int, heightDp: Int): Boolean {
- return isWidthAtLeast(widthDp) && isHeightAtLeast(heightDp)
+ fun isAtLeastBreakpoint(widthBreakpointDp: Int, heightBreakpointDp: Int): Boolean {
+ return isWidthAtLeastBreakpoint(widthBreakpointDp) &&
+ isHeightAtLeastBreakpoint(heightBreakpointDp)
}
override fun equals(other: Any?): Boolean {
diff --git a/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt b/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt
index 816e40c..e9a0aba 100644
--- a/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt
+++ b/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt
@@ -16,6 +16,10 @@
package androidx.window.core.layout
+import androidx.window.core.layout.WindowSizeClass.Companion.HEIGHT_DP_EXPANDED_LOWER_BOUND
+import androidx.window.core.layout.WindowSizeClass.Companion.HEIGHT_DP_MEDIUM_LOWER_BOUND
+import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_EXPANDED_LOWER_BOUND
+import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
@@ -115,85 +119,252 @@
}
@Test
- fun is_width_at_least_returns_true_when_input_is_greater() {
+ fun is_width_at_least_breakpoint_returns_false_when_breakpoint_is_greater() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertTrue(sizeClass.isWidthAtLeast(width + 1))
+ assertFalse(sizeClass.isWidthAtLeastBreakpoint(width + 1))
}
@Test
- fun is_width_at_least_returns_true_when_input_is_equal() {
+ fun is_width_at_least_breakpoint_returns_true_when_breakpoint_is_equal() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertTrue(sizeClass.isWidthAtLeast(width))
+ assertTrue(sizeClass.isWidthAtLeastBreakpoint(width))
}
@Test
- fun is_width_at_least_returns_false_when_input_is_smaller() {
+ fun is_width_at_least_breakpoint_returns_true_when_breakpoint_is_smaller() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertFalse(sizeClass.isWidthAtLeast(width - 1))
+ assertTrue(sizeClass.isWidthAtLeastBreakpoint(width - 1))
+ }
+
+ /**
+ * Tests that the width breakpoint logic works as expected. The following sample shows what the
+ * dev use site should be
+ *
+ * WIDTH_DP_MEDIUM_LOWER_BOUND = 600 WIDTH_DP_EXPANDED_LOWER_BOUND = 840
+ *
+ * fun process(sizeClass: WindowSizeClass) { when {
+ * sizeClass.isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) -> doExpanded()
+ * sizeClass.isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) -> doMedium() else -> doCompact() } }
+ *
+ * val belowMediumBreakpoint = WindowSizeClass(minWidthDp = 300, minHeightDp = 0) val
+ * equalMediumBreakpoint = WindowSizeClass(minWidthDp = 600, minHeightDp = 0) val
+ * expandedBreakpoint = WindowSizeClass(minWidthDp = 840, minHeightDp = 0)
+ *
+ * process(belowBreakpoint) -> doSomethingCompact() process(equalMediumBreakpoint) ->
+ * doSomethingMedium() process(expandedBreakpoint) -> doSomethingExpanded()
+ *
+ * So the following must be true
+ *
+ * expandedBreakpoint WindowSizeClass(840, 0).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) ==
+ * true WindowSizeClass(840, 0).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == true
+ *
+ * equalMediumBreakpoint WindowSizeClass(600, 0).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND)
+ * == false WindowSizeClass(600, 0).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == true
+ *
+ * belowBreakpoint WindowSizeClass(0, 0).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) == false
+ * WindowSizeClass(0, 0).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == false
+ */
+ @Test
+ fun is_width_at_least_bounds_checks() {
+ // expandedBreakpoint
+ assertTrue(
+ WindowSizeClass(WIDTH_DP_EXPANDED_LOWER_BOUND, 0)
+ .isWidthAtLeastBreakpoint(WIDTH_DP_EXPANDED_LOWER_BOUND)
+ )
+ assertTrue(
+ WindowSizeClass(WIDTH_DP_EXPANDED_LOWER_BOUND, 0)
+ .isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)
+ )
+
+ // equalMediumBreakpoint
+ assertFalse(
+ WindowSizeClass(WIDTH_DP_MEDIUM_LOWER_BOUND, 0)
+ .isWidthAtLeastBreakpoint(WIDTH_DP_EXPANDED_LOWER_BOUND)
+ )
+ assertTrue(
+ WindowSizeClass(WIDTH_DP_MEDIUM_LOWER_BOUND, 0)
+ .isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)
+ )
+
+ // belowBreakpoint
+ assertFalse(WindowSizeClass(0, 0).isWidthAtLeastBreakpoint(WIDTH_DP_EXPANDED_LOWER_BOUND))
+ assertFalse(WindowSizeClass(0, 0).isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND))
+ }
+
+ /**
+ * Tests that the width breakpoint logic works as expected. The following sample shows what the
+ * dev use site should be
+ *
+ * HEIGHT_DP_MEDIUM_LOWER_BOUND = 480 HEIGHT_DP_EXPANDED_LOWER_BOUND = 900
+ *
+ * fun process(sizeClass: WindowSizeClass) { when {
+ * sizeClass.isHeightAtLeast(HEIGHT_DP_EXPANDED_LOWER_BOUND) -> doExpanded()
+ * sizeClass.isHeightAtLeast(HEIGHT_DP_MEDIUM_LOWER_BOUND) -> doMedium() else -> doCompact() } }
+ *
+ * val belowMediumBreakpoint = WindowSizeClass(minWidthDp = 0, minHeightDp = 0) val
+ * equalMediumBreakpoint = WindowSizeClass(minWidthDp = 0, minHeightDp = 480) val
+ * expandedBreakpoint = WindowSizeClass(minWidthDp = 0, minHeightDp = 900)
+ *
+ * process(belowBreakpoint) -> doSomethingCompact() process(equalMediumBreakpoint) ->
+ * doSomethingMedium() process(expandedBreakpoint) -> doSomethingExpanded()
+ *
+ * So the following must be true
+ *
+ * expandedBreakpoint WindowSizeClass(0, 900).isWidthAtLeast(HEIGHT_DP_EXPANDED_LOWER_BOUND) ==
+ * true WindowSizeClass(0, 900).isWidthAtLeast(HEIGHT_DP_MEDIUM_LOWER_BOUND) == true
+ *
+ * equalMediumBreakpoint WindowSizeClass(0, 480).isWidthAtLeast(HEIGHT_DP_EXPANDED_LOWER_BOUND)
+ * == false WindowSizeClass(0, 480).isWidthAtLeast(HEIGHT_DP_MEDIUM_LOWER_BOUND) == true
+ *
+ * belowBreakpoint WindowSizeClass(0, 0).isWidthAtLeast(HEIGHT_DP_EXPANDED_LOWER_BOUND) == false
+ * WindowSizeClass(0, 0).isWidthAtLeast(HEIGHT_DP_MEDIUM_LOWER_BOUND) == false
+ */
+ @Test
+ fun is_height_at_least_bounds_checks() {
+ // expandedBreakpoint
+ assertTrue(
+ WindowSizeClass(0, HEIGHT_DP_EXPANDED_LOWER_BOUND)
+ .isHeightAtLeastBreakpoint(HEIGHT_DP_EXPANDED_LOWER_BOUND)
+ )
+ assertTrue(
+ WindowSizeClass(0, HEIGHT_DP_EXPANDED_LOWER_BOUND)
+ .isHeightAtLeastBreakpoint(HEIGHT_DP_MEDIUM_LOWER_BOUND)
+ )
+
+ // equalMediumBreakpoint
+ assertFalse(
+ WindowSizeClass(0, HEIGHT_DP_MEDIUM_LOWER_BOUND)
+ .isHeightAtLeastBreakpoint(HEIGHT_DP_EXPANDED_LOWER_BOUND)
+ )
+ assertTrue(
+ WindowSizeClass(0, HEIGHT_DP_MEDIUM_LOWER_BOUND)
+ .isHeightAtLeastBreakpoint(HEIGHT_DP_MEDIUM_LOWER_BOUND)
+ )
+
+ // belowBreakpoint
+ assertFalse(WindowSizeClass(0, 0).isHeightAtLeastBreakpoint(HEIGHT_DP_EXPANDED_LOWER_BOUND))
+ assertFalse(WindowSizeClass(0, 0).isHeightAtLeastBreakpoint(HEIGHT_DP_MEDIUM_LOWER_BOUND))
+ }
+
+ /**
+ * Tests that the width breakpoint logic works as expected. The following sample shows what the
+ * dev use site should be
+ *
+ * DIAGONAL_BOUND_MEDIUM = 600, 600 DIAGONAL_BOUND_EXPANDED = 900, 900
+ *
+ * fun process(sizeClass: WindowSizeClass) { when { sizeClass.isAtLeast(DIAGONAL_BOUND_EXPANDED,
+ * DIAGONAL_BOUND_EXPANDED) -> doExpanded() sizeClass.isAtLeast(DIAGONAL_BOUND_MEDIUM,
+ * DIAGONAL_BOUND_MEDIUM) -> doMedium() else -> doCompact() } }
+ *
+ * val belowMediumBreakpoint = WindowSizeClass(minWidthDp = 0, minHeightDp = 0) val
+ * equalMediumBreakpoint = WindowSizeClass(minWidthDp = 600, minHeightDp = 600) val
+ * expandedBreakpoint = WindowSizeClass(minWidthDp = 900, minHeightDp = 900)
+ *
+ * process(belowBreakpoint) -> doSomethingCompact() process(equalMediumBreakpoint) ->
+ * doSomethingMedium() process(expandedBreakpoint) -> doSomethingExpanded()
+ *
+ * So the following must be true
+ *
+ * expandedBreakpoint WindowSizeClass(900, 900).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) ==
+ * true WindowSizeClass(900, 900).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == true
+ *
+ * equalMediumBreakpoint WindowSizeClass(600, 600).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND)
+ * == false WindowSizeClass(600, 600).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == true
+ *
+ * belowBreakpoint WindowSizeClass(0, 0).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) == false
+ * WindowSizeClass(0, 0).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == false
+ */
+ @Test
+ fun is_area_at_least_bounds_checks() {
+ val diagonalMedium = 600
+ val diagonalExpanded = 900
+ // expandedBreakpoint
+ assertTrue(
+ WindowSizeClass(diagonalExpanded, diagonalExpanded)
+ .isAtLeastBreakpoint(diagonalExpanded, diagonalExpanded)
+ )
+ assertTrue(
+ WindowSizeClass(diagonalExpanded, diagonalExpanded)
+ .isAtLeastBreakpoint(diagonalMedium, diagonalMedium)
+ )
+
+ // equalMediumBreakpoint
+ assertFalse(
+ WindowSizeClass(diagonalMedium, diagonalMedium)
+ .isAtLeastBreakpoint(diagonalExpanded, diagonalExpanded)
+ )
+ assertTrue(
+ WindowSizeClass(diagonalMedium, diagonalMedium)
+ .isAtLeastBreakpoint(diagonalMedium, diagonalMedium)
+ )
+
+ // belowBreakpoint
+ assertFalse(WindowSizeClass(0, 0).isAtLeastBreakpoint(diagonalExpanded, diagonalExpanded))
+ assertFalse(WindowSizeClass(0, 0).isAtLeastBreakpoint(diagonalMedium, diagonalMedium))
}
@Test
- fun is_height_at_least_returns_true_when_input_is_greater() {
+ fun is_height_at_least_breakpoint_returns_false_when_breakpoint_is_greater() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertTrue(sizeClass.isHeightAtLeast(height + 1))
+ assertFalse(sizeClass.isHeightAtLeastBreakpoint(height + 1))
}
@Test
- fun is_height_at_least_returns_true_when_input_is_equal() {
+ fun is_height_at_least_breakpoint_returns_true_when_breakpoint_is_equal() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertTrue(sizeClass.isHeightAtLeast(height))
+ assertTrue(sizeClass.isHeightAtLeastBreakpoint(height))
}
@Test
- fun is_height_at_least_returns_false_when_input_is_smaller() {
+ fun is_height_at_least_breakpoint_returns_true_when_breakpoint_is_smaller() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertFalse(sizeClass.isHeightAtLeast(height - 1))
+ assertTrue(sizeClass.isHeightAtLeastBreakpoint(height - 1))
}
@Test
- fun is_at_least_returns_true_when_input_is_greater() {
+ fun is_at_least_breakpoint_returns_false_when_breakpoint_is_greater() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertTrue(sizeClass.isAtLeast(width, height + 1))
- assertTrue(sizeClass.isAtLeast(width + 1, height))
+ assertFalse(sizeClass.isAtLeastBreakpoint(width, height + 1))
+ assertFalse(sizeClass.isAtLeastBreakpoint(width + 1, height))
}
@Test
- fun is_at_least_returns_true_when_input_is_equal() {
+ fun is_at_least_breakpoint_returns_true_when_breakpoint_is_equal() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertTrue(sizeClass.isAtLeast(width, height))
+ assertTrue(sizeClass.isAtLeastBreakpoint(width, height))
}
@Test
- fun is_at_least_returns_false_when_input_is_smaller() {
+ fun is_at_least_breakpoint_returns_true_when_breakpoint_is_smaller() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertFalse(sizeClass.isAtLeast(width, height - 1))
- assertFalse(sizeClass.isAtLeast(width - 1, height))
+ assertTrue(sizeClass.isAtLeastBreakpoint(width, height - 1))
+ assertTrue(sizeClass.isAtLeastBreakpoint(width - 1, height))
}
}