Merge "Migrate PreparedStatementWriter and PreparedQueryAdapter to XPoet" into androidx-main
diff --git a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt
index 65be75b..ef20bed 100644
--- a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt
+++ b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt
@@ -49,6 +49,7 @@
import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly
import org.jetbrains.uast.UAnnotated
import org.jetbrains.uast.UAnnotation
+import org.jetbrains.uast.UAnonymousClass
import org.jetbrains.uast.UArrayAccessExpression
import org.jetbrains.uast.UBinaryExpression
import org.jetbrains.uast.UCallExpression
@@ -638,16 +639,24 @@
val elementLabel = when (element) {
is UMethod -> "'${element.name}'"
is UClass -> "containing class '${element.name}'"
+ is UAnonymousClass -> "containing anonymous class"
else -> throw IllegalArgumentException("Unsupported element type")
}
+ // If the element has a modifier list, e.g. not an anonymous class or lambda, then place the
+ // insertion point at the beginning of the modifiers list. This ensures that we don't insert
+ // the annotation in an invalid position, such as after the "public" or "fun" keywords. We
+ // also don't want to place it on the element range itself, since that would place it before
+ // the comments.
+ val elementLocation = context.getLocation(element.modifierList ?: element as UElement)
+
return fix()
- .name("Add '$annotation' annotation to $elementLabel")
.replace()
- .range(context.getLocation(element as UElement))
+ .name("Add '$annotation' annotation to $elementLabel")
+ .range(elementLocation)
.beginning()
- .with("$annotation ")
.shortenNames()
+ .with("$annotation ")
.build()
}
diff --git a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/ExperimentalDetectorTest.kt b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/ExperimentalDetectorTest.kt
index 2344e4d..167577e 100644
--- a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/ExperimentalDetectorTest.kt
+++ b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/ExperimentalDetectorTest.kt
@@ -24,7 +24,6 @@
import com.android.tools.lint.checks.infrastructure.TestFiles.xml
import com.android.tools.lint.checks.infrastructure.TestLintResult
import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -74,11 +73,11 @@
Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 25: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalDateTime.class)' annotation to 'getDateUnsafe':
@@ -24 +24
- int getDateUnsafe() {
-+ int @androidx.annotation.OptIn(markerClass = ExperimentalDateTime.class) getDateUnsafe() {
++ @androidx.annotation.OptIn(markerClass = ExperimentalDateTime.class) int getDateUnsafe() {
Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 25: Add '@sample.experimental.ExperimentalDateTime' annotation to 'getDateUnsafe':
@@ -24 +24
- int getDateUnsafe() {
-+ int @ExperimentalDateTime getDateUnsafe() {
++ @ExperimentalDateTime int getDateUnsafe() {
Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 25: Add '@sample.experimental.ExperimentalDateTime' annotation to containing class 'UseJavaExperimentalFromJava':
@@ -19 +19
- @SuppressWarnings({"unused", "WeakerAccess", "deprecation"})
@@ -86,35 +85,35 @@
Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 26: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalDateTime.class)' annotation to 'getDateUnsafe':
@@ -24 +24
- int getDateUnsafe() {
-+ int @androidx.annotation.OptIn(markerClass = ExperimentalDateTime.class) getDateUnsafe() {
++ @androidx.annotation.OptIn(markerClass = ExperimentalDateTime.class) int getDateUnsafe() {
Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 26: Add '@sample.experimental.ExperimentalDateTime' annotation to 'getDateUnsafe':
@@ -24 +24
- int getDateUnsafe() {
-+ int @ExperimentalDateTime getDateUnsafe() {
++ @ExperimentalDateTime int getDateUnsafe() {
Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 26: Add '@sample.experimental.ExperimentalDateTime' annotation to containing class 'UseJavaExperimentalFromJava':
@@ -19 +19
- @SuppressWarnings({"unused", "WeakerAccess", "deprecation"})
+ @ExperimentalDateTime @SuppressWarnings({"unused", "WeakerAccess", "deprecation"})
Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 53: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalLocation.class)' annotation to 'getDateExperimentalLocationUnsafe':
-@@ -51 +51
-- int getDateExperimentalLocationUnsafe() {
-+ int @androidx.annotation.OptIn(markerClass = ExperimentalLocation.class) getDateExperimentalLocationUnsafe() {
+@@ -50 +50
+- @ExperimentalDateTime
++ @androidx.annotation.OptIn(markerClass = ExperimentalLocation.class) @ExperimentalDateTime
Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 53: Add '@sample.experimental.ExperimentalLocation' annotation to 'getDateExperimentalLocationUnsafe':
-@@ -51 +51
-- int getDateExperimentalLocationUnsafe() {
-+ int @ExperimentalLocation getDateExperimentalLocationUnsafe() {
+@@ -50 +50
+- @ExperimentalDateTime
++ @ExperimentalLocation @ExperimentalDateTime
Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 53: Add '@sample.experimental.ExperimentalLocation' annotation to containing class 'UseJavaExperimentalFromJava':
@@ -19 +19
- @SuppressWarnings({"unused", "WeakerAccess", "deprecation"})
+ @ExperimentalLocation @SuppressWarnings({"unused", "WeakerAccess", "deprecation"})
Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 54: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalLocation.class)' annotation to 'getDateExperimentalLocationUnsafe':
-@@ -51 +51
-- int getDateExperimentalLocationUnsafe() {
-+ int @androidx.annotation.OptIn(markerClass = ExperimentalLocation.class) getDateExperimentalLocationUnsafe() {
+@@ -50 +50
+- @ExperimentalDateTime
++ @androidx.annotation.OptIn(markerClass = ExperimentalLocation.class) @ExperimentalDateTime
Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 54: Add '@sample.experimental.ExperimentalLocation' annotation to 'getDateExperimentalLocationUnsafe':
-@@ -51 +51
-- int getDateExperimentalLocationUnsafe() {
-+ int @ExperimentalLocation getDateExperimentalLocationUnsafe() {
+@@ -50 +50
+- @ExperimentalDateTime
++ @ExperimentalLocation @ExperimentalDateTime
Fix for src/sample/experimental/UseJavaExperimentalFromJava.java line 54: Add '@sample.experimental.ExperimentalLocation' annotation to containing class 'UseJavaExperimentalFromJava':
@@ -19 +19
- @SuppressWarnings({"unused", "WeakerAccess", "deprecation"})
@@ -190,49 +189,49 @@
val expectedFix = """
Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 29: Add '@androidx.annotation.OptIn(sample.experimental.ExperimentalDateTime::class)' annotation to 'getDateUnsafe':
-@@ -28 +28
-- fun getDateUnsafe(): Int {
-+ fun @androidx.annotation.OptIn(ExperimentalDateTime::class) getDateUnsafe(): Int {
+@@ -1 +1
+- /*
++ @androidx.annotation.OptIn(ExperimentalDateTime::class) /*
Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 29: Add '@sample.experimental.ExperimentalDateTime' annotation to 'getDateUnsafe':
-@@ -28 +28
-- fun getDateUnsafe(): Int {
-+ fun @ExperimentalDateTime getDateUnsafe(): Int {
+@@ -1 +1
+- /*
++ @ExperimentalDateTime /*
Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 29: Add '@sample.experimental.ExperimentalDateTime' annotation to containing class 'UseJavaExperimentalFromKt':
@@ -23 +23
- @Suppress("unused", "MemberVisibilityCanBePrivate")
+ @ExperimentalDateTime @Suppress("unused", "MemberVisibilityCanBePrivate")
Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 30: Add '@androidx.annotation.OptIn(sample.experimental.ExperimentalDateTime::class)' annotation to 'getDateUnsafe':
-@@ -28 +28
-- fun getDateUnsafe(): Int {
-+ fun @androidx.annotation.OptIn(ExperimentalDateTime::class) getDateUnsafe(): Int {
+@@ -1 +1
+- /*
++ @androidx.annotation.OptIn(ExperimentalDateTime::class) /*
Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 30: Add '@sample.experimental.ExperimentalDateTime' annotation to 'getDateUnsafe':
-@@ -28 +28
-- fun getDateUnsafe(): Int {
-+ fun @ExperimentalDateTime getDateUnsafe(): Int {
+@@ -1 +1
+- /*
++ @ExperimentalDateTime /*
Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 30: Add '@sample.experimental.ExperimentalDateTime' annotation to containing class 'UseJavaExperimentalFromKt':
@@ -23 +23
- @Suppress("unused", "MemberVisibilityCanBePrivate")
+ @ExperimentalDateTime @Suppress("unused", "MemberVisibilityCanBePrivate")
Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 57: Add '@androidx.annotation.OptIn(sample.experimental.ExperimentalLocation::class)' annotation to 'getDateExperimentalLocationUnsafe':
-@@ -55 +55
-- fun getDateExperimentalLocationUnsafe(): Int {
-+ fun @androidx.annotation.OptIn(ExperimentalLocation::class) getDateExperimentalLocationUnsafe(): Int {
+@@ -54 +54
+- @ExperimentalDateTime
++ @androidx.annotation.OptIn(ExperimentalLocation::class) @ExperimentalDateTime
Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 57: Add '@sample.experimental.ExperimentalLocation' annotation to 'getDateExperimentalLocationUnsafe':
-@@ -55 +55
-- fun getDateExperimentalLocationUnsafe(): Int {
-+ fun @ExperimentalLocation getDateExperimentalLocationUnsafe(): Int {
+@@ -54 +54
+- @ExperimentalDateTime
++ @ExperimentalLocation @ExperimentalDateTime
Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 57: Add '@sample.experimental.ExperimentalLocation' annotation to containing class 'UseJavaExperimentalFromKt':
@@ -23 +23
- @Suppress("unused", "MemberVisibilityCanBePrivate")
+ @ExperimentalLocation @Suppress("unused", "MemberVisibilityCanBePrivate")
Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 58: Add '@androidx.annotation.OptIn(sample.experimental.ExperimentalLocation::class)' annotation to 'getDateExperimentalLocationUnsafe':
-@@ -55 +55
-- fun getDateExperimentalLocationUnsafe(): Int {
-+ fun @androidx.annotation.OptIn(ExperimentalLocation::class) getDateExperimentalLocationUnsafe(): Int {
+@@ -54 +54
+- @ExperimentalDateTime
++ @androidx.annotation.OptIn(ExperimentalLocation::class) @ExperimentalDateTime
Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 58: Add '@sample.experimental.ExperimentalLocation' annotation to 'getDateExperimentalLocationUnsafe':
-@@ -55 +55
-- fun getDateExperimentalLocationUnsafe(): Int {
-+ fun @ExperimentalLocation getDateExperimentalLocationUnsafe(): Int {
+@@ -54 +54
+- @ExperimentalDateTime
++ @ExperimentalLocation @ExperimentalDateTime
Fix for src/sample/experimental/UseJavaExperimentalFromKt.kt line 58: Add '@sample.experimental.ExperimentalLocation' annotation to containing class 'UseJavaExperimentalFromKt':
@@ -23 +23
- @Suppress("unused", "MemberVisibilityCanBePrivate")
@@ -243,7 +242,6 @@
check(*input).expect(expected).expectFixDiffs(expectedFix)
}
- @Ignore("b/196881523")
@Test
fun useKtExperimentalFromJava() {
val input = arrayOf(
@@ -289,11 +287,11 @@
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 25: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalDateTimeKt.class)' annotation to 'getDateUnsafe':
@@ -24 +24
- int getDateUnsafe() {
-+ int @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class) getDateUnsafe() {
++ @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class) int getDateUnsafe() {
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 25: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to 'getDateUnsafe':
@@ -24 +24
- int getDateUnsafe() {
-+ int @ExperimentalDateTimeKt getDateUnsafe() {
++ @ExperimentalDateTimeKt int getDateUnsafe() {
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 25: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to containing class 'UseKtExperimentalFromJava':
@@ -19 +19
- @SuppressWarnings({"unused", "WeakerAccess"})
@@ -301,35 +299,35 @@
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 26: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalDateTimeKt.class)' annotation to 'getDateUnsafe':
@@ -24 +24
- int getDateUnsafe() {
-+ int @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class) getDateUnsafe() {
++ @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class) int getDateUnsafe() {
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 26: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to 'getDateUnsafe':
@@ -24 +24
- int getDateUnsafe() {
-+ int @ExperimentalDateTimeKt getDateUnsafe() {
++ @ExperimentalDateTimeKt int getDateUnsafe() {
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 26: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to containing class 'UseKtExperimentalFromJava':
@@ -19 +19
- @SuppressWarnings({"unused", "WeakerAccess"})
+ @ExperimentalDateTimeKt @SuppressWarnings({"unused", "WeakerAccess"})
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 54: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalLocationKt.class)' annotation to 'getDateExperimentalLocationUnsafe':
-@@ -52 +52
-- int getDateExperimentalLocationUnsafe() {
-+ int @androidx.annotation.OptIn(markerClass = ExperimentalLocationKt.class) getDateExperimentalLocationUnsafe() {
+@@ -51 +51
+- @ExperimentalDateTimeKt
++ @androidx.annotation.OptIn(markerClass = ExperimentalLocationKt.class) @ExperimentalDateTimeKt
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 54: Add '@sample.experimental.ExperimentalLocationKt' annotation to 'getDateExperimentalLocationUnsafe':
-@@ -52 +52
-- int getDateExperimentalLocationUnsafe() {
-+ int @ExperimentalLocationKt getDateExperimentalLocationUnsafe() {
+@@ -51 +51
+- @ExperimentalDateTimeKt
++ @ExperimentalLocationKt @ExperimentalDateTimeKt
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 54: Add '@sample.experimental.ExperimentalLocationKt' annotation to containing class 'UseKtExperimentalFromJava':
@@ -19 +19
- @SuppressWarnings({"unused", "WeakerAccess"})
+ @ExperimentalLocationKt @SuppressWarnings({"unused", "WeakerAccess"})
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 55: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalLocationKt.class)' annotation to 'getDateExperimentalLocationUnsafe':
-@@ -52 +52
-- int getDateExperimentalLocationUnsafe() {
-+ int @androidx.annotation.OptIn(markerClass = ExperimentalLocationKt.class) getDateExperimentalLocationUnsafe() {
+@@ -51 +51
+- @ExperimentalDateTimeKt
++ @androidx.annotation.OptIn(markerClass = ExperimentalLocationKt.class) @ExperimentalDateTimeKt
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 55: Add '@sample.experimental.ExperimentalLocationKt' annotation to 'getDateExperimentalLocationUnsafe':
-@@ -52 +52
-- int getDateExperimentalLocationUnsafe() {
-+ int @ExperimentalLocationKt getDateExperimentalLocationUnsafe() {
+@@ -51 +51
+- @ExperimentalDateTimeKt
++ @ExperimentalLocationKt @ExperimentalDateTimeKt
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 55: Add '@sample.experimental.ExperimentalLocationKt' annotation to containing class 'UseKtExperimentalFromJava':
@@ -19 +19
- @SuppressWarnings({"unused", "WeakerAccess"})
@@ -337,11 +335,11 @@
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 88: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalDateTimeKt.class)' annotation to 'regressionTestStaticUsage':
@@ -87 +87
- void regressionTestStaticUsage() {
-+ void @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class) regressionTestStaticUsage() {
++ @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class) void regressionTestStaticUsage() {
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 88: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to 'regressionTestStaticUsage':
@@ -87 +87
- void regressionTestStaticUsage() {
-+ void @ExperimentalDateTimeKt regressionTestStaticUsage() {
++ @ExperimentalDateTimeKt void regressionTestStaticUsage() {
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 88: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to containing class 'UseKtExperimentalFromJava':
@@ -19 +19
- @SuppressWarnings({"unused", "WeakerAccess"})
@@ -349,11 +347,11 @@
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 89: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalDateTimeKt.class)' annotation to 'regressionTestStaticUsage':
@@ -87 +87
- void regressionTestStaticUsage() {
-+ void @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class) regressionTestStaticUsage() {
++ @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class) void regressionTestStaticUsage() {
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 89: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to 'regressionTestStaticUsage':
@@ -87 +87
- void regressionTestStaticUsage() {
-+ void @ExperimentalDateTimeKt regressionTestStaticUsage() {
++ @ExperimentalDateTimeKt void regressionTestStaticUsage() {
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 89: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to containing class 'UseKtExperimentalFromJava':
@@ -19 +19
- @SuppressWarnings({"unused", "WeakerAccess"})
@@ -361,11 +359,11 @@
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 96: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalDateTimeKt.class)' annotation to 'regressionTestInlineUsage':
@@ -95 +95
- void regressionTestInlineUsage() {
-+ void @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class) regressionTestInlineUsage() {
++ @androidx.annotation.OptIn(markerClass = ExperimentalDateTimeKt.class) void regressionTestInlineUsage() {
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 96: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to 'regressionTestInlineUsage':
@@ -95 +95
- void regressionTestInlineUsage() {
-+ void @ExperimentalDateTimeKt regressionTestInlineUsage() {
++ @ExperimentalDateTimeKt void regressionTestInlineUsage() {
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 96: Add '@sample.experimental.ExperimentalDateTimeKt' annotation to containing class 'UseKtExperimentalFromJava':
@@ -19 +19
- @SuppressWarnings({"unused", "WeakerAccess"})
@@ -373,11 +371,11 @@
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 97: Add '@androidx.annotation.OptIn(markerClass = sample.experimental.ExperimentalDateTime.class)' annotation to 'regressionTestInlineUsage':
@@ -95 +95
- void regressionTestInlineUsage() {
-+ void @androidx.annotation.OptIn(markerClass = ExperimentalDateTime.class) regressionTestInlineUsage() {
++ @androidx.annotation.OptIn(markerClass = ExperimentalDateTime.class) void regressionTestInlineUsage() {
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 97: Add '@sample.experimental.ExperimentalDateTime' annotation to 'regressionTestInlineUsage':
@@ -95 +95
- void regressionTestInlineUsage() {
-+ void @ExperimentalDateTime regressionTestInlineUsage() {
++ @ExperimentalDateTime void regressionTestInlineUsage() {
Fix for src/sample/experimental/UseKtExperimentalFromJava.java line 97: Add '@sample.experimental.ExperimentalDateTime' annotation to containing class 'UseKtExperimentalFromJava':
@@ -19 +19
- @SuppressWarnings({"unused", "WeakerAccess"})
@@ -452,7 +450,7 @@
*/
val ANDROIDX_EXPERIMENTAL_KT: TestFile = kotlin(
"""
- package androidx.annotation.experimental;
+ package androidx.annotation.experimental
import kotlin.annotation.Retention
import kotlin.annotation.Target
@@ -478,7 +476,7 @@
*/
val ANDROIDX_USE_EXPERIMENTAL_KT: TestFile = kotlin(
"""
- package androidx.annotation.experimental;
+ package androidx.annotation.experimental
import kotlin.annotation.Retention
import kotlin.annotation.Target
diff --git a/benchmark/benchmark-darwin-gradle-plugin/README.md b/benchmark/benchmark-darwin-gradle-plugin/README.md
new file mode 100644
index 0000000..3fce72d
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/README.md
@@ -0,0 +1,70 @@
+# Introduction
+
+A Gradle Plugin to build and generate benchmarking results for KMP iOS benchmarks.
+
+* Generates Skia Dashboard compatible results.
+* Automatically generates the XCode project, and runs benchmarks on a target device running iOS
+ or macOS (simulator or physical devices).
+
+# Usage
+
+A KMP project needs to do something like:
+
+```groovy
+plugins {
+ id("androidx.benchmark.darwin")
+}
+```
+
+and then it can use the `darwinBenchmark` block like so:
+
+```groovy
+darwinBenchmark {
+ // XCodegen Schema YAML
+ xcodeGenConfigFile = project.rootProject.file(
+ "benchmark/benchmark-darwin-samples-xcode/xcodegen-project.yml"
+ )
+ // XCode project name
+ xcodeProjectName = "benchmark-darwin-samples-xcode"
+ // iOS app scheme
+ scheme = "testapp-ios"
+ // ios 13, 15.2
+ destination = "id=7F61C467-4E4A-437C-B6EF-026FEEF3904C"
+ // The XCFrameworkConfig name
+ xcFrameworkConfig = "AndroidXDarwinSampleBenchmarks"
+}
+```
+
+Example metrics look like:
+
+```json
+{
+ "key": {
+ "testDescription": "Allocate an ArrayList of size 1000",
+ "metricName": "Memory Peak Physical",
+ "metricIdentifier": "com.apple.dt.XCTMetric_Memory.physical_peak",
+ "polarity": "prefers smaller",
+ "units": "kB"
+ },
+ "measurements": {
+ "stat": [
+ {
+ "value": "min",
+ "measurement": 0.0
+ },
+ {
+ "value": "median",
+ "measurement": 0.0
+ },
+ {
+ "value": "max",
+ "measurement": 0.0
+ },
+ {
+ "value": "stddev",
+ "measurement": 0.0
+ }
+ ]
+ }
+}
+```
diff --git a/benchmark/benchmark-darwin-gradle-plugin/build.gradle b/benchmark/benchmark-darwin-gradle-plugin/build.gradle
new file mode 100644
index 0000000..42825133
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/build.gradle
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+import androidx.build.SdkResourceGenerator
+
+plugins {
+ id("AndroidXPlugin")
+ id("kotlin")
+ id("java-gradle-plugin")
+}
+
+dependencies {
+ api(gradleApi())
+ implementation(libs.gson)
+ implementation(libs.apacheCommonsMath)
+ testImplementation(gradleTestKit())
+ testImplementation(libs.junit)
+ testImplementation(libs.truth)
+}
+
+SdkResourceGenerator.generateForHostTest(project)
+
+gradlePlugin {
+ plugins {
+ darwinBenchmark {
+ id = "androidx.benchmark.darwin"
+ implementationClass = "androidx.benchmark.darwin.gradle.DarwinBenchmarkPlugin"
+ }
+ }
+}
+
+androidx {
+ name = "AndroidX Benchmarks - Darwin Gradle Plugin"
+ type = LibraryType.GRADLE_PLUGIN
+ mavenGroup = LibraryGroups.BENCHMARK
+ inceptionYear = "2022"
+ description = "AndroidX Benchmarks - Darwin Gradle Plugin"
+}
+
+validatePlugins {
+ enableStricterValidation = true
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
new file mode 100644
index 0000000..5974f14
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.darwin.gradle
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+
+/**
+ * The Darwin benchmark plugin that helps run KMP benchmarks on iOS devices, and extracts benchmark
+ * results in `json`.
+ */
+class DarwinBenchmarkPlugin : Plugin<Project> {
+ override fun apply(project: Project) {
+ val extension =
+ project.extensions.create("darwinBenchmark", DarwinBenchmarkPluginExtension::class.java)
+
+ val xcodeProjectPath = extension.xcodeProjectName.flatMap { name ->
+ project.layout.buildDirectory.dir("$name.xcodeproj")
+ }
+
+ val xcResultPath = extension.xcodeProjectName.flatMap { name ->
+ project.layout.buildDirectory.dir("$name.xcresult")
+ }
+
+ val generateXCodeProjectTask = project.tasks.register(
+ GENERATE_XCODE_PROJECT_TASK, GenerateXCodeProjectTask::class.java
+ ) {
+ it.yamlFile.set(extension.xcodeGenConfigFile)
+ it.projectName.set(extension.xcodeProjectName)
+ it.xcProjectPath.set(xcodeProjectPath)
+ it.infoPlistPath.set(project.layout.buildDirectory.file("Info.plist"))
+ }
+
+ val runDarwinBenchmarks = project.tasks.register(
+ RUN_DARWIN_BENCHMARKS_TASK, RunDarwinBenchmarksTask::class.java
+ ) {
+ it.xcodeProjectPath.set(generateXCodeProjectTask.flatMap { task ->
+ task.xcProjectPath
+ })
+ it.destination.set(extension.destination)
+ it.scheme.set(extension.scheme)
+ it.xcResultPath.set(xcResultPath)
+ it.dependsOn("assemble${extension.xcFrameworkConfig.get()}ReleaseXCFramework")
+ }
+
+ project.tasks.register(
+ DARWIN_BENCHMARK_RESULTS_TASK, DarwinBenchmarkResultsTask::class.java
+ ) {
+ it.xcResultPath.set(runDarwinBenchmarks.flatMap { task ->
+ task.xcResultPath
+ })
+ it.outputFile.set(
+ project.layout.buildDirectory.file(
+ "${extension.xcodeProjectName.get()}-benchmark-result.json"
+ )
+ )
+ }
+ }
+
+ private companion object {
+ const val GENERATE_XCODE_PROJECT_TASK = "generateXCodeProject"
+ const val RUN_DARWIN_BENCHMARKS_TASK = "runDarwinBenchmarks"
+ const val DARWIN_BENCHMARK_RESULTS_TASK = "darwinBenchmarkResults"
+ }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt
new file mode 100644
index 0000000..83781e8
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.darwin.gradle
+
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
+
+/**
+ * The [DarwinBenchmarkPlugin] extension.
+ */
+abstract class DarwinBenchmarkPluginExtension {
+ /**
+ * The path to the YAML file that can be used to generate the XCode project.
+ */
+ abstract val xcodeGenConfigFile: RegularFileProperty
+
+ /**
+ * The project name as defined in the YAML file.
+ */
+ abstract val xcodeProjectName: Property<String>
+
+ /**
+ * The project scheme as defined in the YAML file that is the unit test target.
+ */
+ abstract val scheme: Property<String>
+
+ /**
+ * The benchmark device `id`.
+ * This is typically discovered by using `xcrun xctrace list devices`.
+ */
+ abstract val destination: Property<String>
+
+ /**
+ * The name of the `XCFrameworkConfig` defined in the benchmark's module build.gradle.
+ * This is used to derive the name of the `release` task to generate the `XCFramework`.
+ */
+ abstract val xcFrameworkConfig: Property<String>
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt
new file mode 100644
index 0000000..0ecdb27
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.darwin.gradle
+
+import androidx.benchmark.darwin.gradle.skia.Metrics
+import androidx.benchmark.darwin.gradle.xcode.Models
+import androidx.benchmark.darwin.gradle.xcode.XcResultParser
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import javax.inject.Inject
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.process.ExecOperations
+
+@CacheableTask
+abstract class DarwinBenchmarkResultsTask @Inject constructor(
+ private val execOperations: ExecOperations
+) : DefaultTask() {
+ @get:InputDirectory
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val xcResultPath: DirectoryProperty
+
+ @get:OutputFile
+ abstract val outputFile: RegularFileProperty
+
+ @TaskAction
+ fun benchmarkResults() {
+ val xcResultFile = xcResultPath.get().asFile
+ val parser = XcResultParser(xcResultFile) { args ->
+ val output = ByteArrayOutputStream()
+ val result = execOperations.exec { spec ->
+ spec.commandLine = args
+ spec.standardOutput = output
+ }
+ result.assertNormalExitValue()
+ output.use {
+ val input = ByteArrayInputStream(output.toByteArray())
+ input.use {
+ input.reader().readText()
+ }
+ }
+ }
+ val (record, summaries) = parser.parseResults()
+ val metrics = Metrics.buildMetrics(record, summaries)
+ val output = Models.gsonBuilder()
+ .setPrettyPrinting()
+ .create()
+ .toJson(metrics)
+
+ outputFile.get().asFile.writeText(output)
+ }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/GenerateXCodeProjectTask.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/GenerateXCodeProjectTask.kt
new file mode 100644
index 0000000..3dec4f1
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/GenerateXCodeProjectTask.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.darwin.gradle
+
+import java.io.File
+import javax.inject.Inject
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.process.ExecOperations
+
+@CacheableTask
+abstract class GenerateXCodeProjectTask @Inject constructor(
+ private val execOperations: ExecOperations
+) : DefaultTask() {
+
+ @get:InputFile
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val yamlFile: RegularFileProperty
+
+ @get:Input
+ abstract val projectName: Property<String>
+
+ @get:OutputFile
+ abstract val infoPlistPath: RegularFileProperty
+
+ @get:OutputDirectory
+ abstract val xcProjectPath: DirectoryProperty
+
+ @TaskAction
+ fun buildXCodeProject() {
+ val outputFile = xcProjectPath.get().asFile
+ if (outputFile.exists()) {
+ require(outputFile.deleteRecursively()) {
+ "Unable to delete xcode project $outputFile"
+ }
+ }
+ val args = listOf(
+ "xcodegen",
+ "--spec",
+ yamlFile.get().asFile.absolutePath,
+ "--project",
+ outputFile.parent
+ )
+ val result = execOperations.exec { spec ->
+ spec.commandLine = args
+ }
+ result.assertNormalExitValue()
+ require(outputFile.exists()) {
+ "Project $projectName must match the `name` declaration in $yamlFile"
+ }
+ copyProjectMetadata()
+ }
+
+ private fun copyProjectMetadata() {
+ val sourceFile = File(yamlFile.get().asFile.parent, "Info.plist")
+ require(sourceFile.exists())
+ val targetFile = infoPlistPath.get().asFile
+ val copied = sourceFile.copyRecursively(targetFile, overwrite = true)
+ require(copied) {
+ "Unable to copy $sourceFile to $targetFile"
+ }
+ }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/RunDarwinBenchmarksTask.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/RunDarwinBenchmarksTask.kt
new file mode 100644
index 0000000..55aec880
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/RunDarwinBenchmarksTask.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.darwin.gradle
+
+import javax.inject.Inject
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.process.ExecOperations
+
+@CacheableTask
+abstract class RunDarwinBenchmarksTask @Inject constructor(
+ private val execOperations: ExecOperations
+) : DefaultTask() {
+ @get:InputDirectory
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val xcodeProjectPath: DirectoryProperty
+
+ @get:Input
+ abstract val destination: Property<String>
+
+ @get:Input
+ abstract val scheme: Property<String>
+
+ @get:OutputDirectory
+ abstract val xcResultPath: DirectoryProperty
+
+ @TaskAction
+ fun runBenchmarks() {
+ requireXcodeBuild()
+ val xcodeProject = xcodeProjectPath.get().asFile
+ val xcResultFile = xcResultPath.get().asFile
+ if (xcResultFile.exists()) {
+ xcResultFile.deleteRecursively()
+ }
+ val args = listOf(
+ "xcodebuild",
+ "test",
+ "-project", xcodeProject.absolutePath.toString(),
+ "-scheme", scheme.get(),
+ "-destination", destination.get(),
+ "-resultBundlePath", xcResultFile.absolutePath,
+ )
+ logger.info("Command : ${args.joinToString(" ")}")
+ val result = execOperations.exec { spec ->
+ spec.commandLine = args
+ }
+ result.assertNormalExitValue()
+ }
+
+ private fun requireXcodeBuild() {
+ val result = execOperations.exec { spec ->
+ spec.commandLine = listOf("which", "xcodebuild")
+ // Ignore exit value here to return a better exception message
+ spec.isIgnoreExitValue = true
+ }
+ require(result.exitValue == 0) {
+ "xcodebuild is missing on this machine."
+ }
+ }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/skia/Metric.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/skia/Metric.kt
new file mode 100644
index 0000000..6bed2de
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/skia/Metric.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.darwin.gradle.skia
+
+import androidx.benchmark.darwin.gradle.xcode.ActionTestSummary
+import androidx.benchmark.darwin.gradle.xcode.ActionsInvocationRecord
+import com.google.gson.annotations.SerializedName
+import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics
+
+/**
+ * https://skia.googlesource.com/buildbot/+/refs/heads/main/perf/FORMAT.md
+ */
+
+data class Stat(val value: String, val measurement: Double)
+data class Measurements(
+ @SerializedName("stat")
+ val stats: List<Stat>
+)
+
+data class Metric(val key: Map<String, String>, val measurements: Measurements)
+data class Metrics(val key: Map<String, String>, val results: List<Metric>) {
+ companion object {
+ fun buildMetrics(
+ record: ActionsInvocationRecord,
+ summaries: List<ActionTestSummary>
+ ): Metrics {
+ require(record.actions.actionRecords.isNotEmpty())
+ val runDestination = record.actions.actionRecords.first().runDestination
+ val metricsKeys = mapOf(
+ "destination" to runDestination.displayName.value,
+ "arch" to runDestination.targetArchitecture.value,
+ "targetSdk" to runDestination.targetSDKRecord.identifier.value,
+ "identifier" to runDestination.localComputerRecord.identifier.value,
+ "modelName" to runDestination.localComputerRecord.modelName.value,
+ "modelCode" to runDestination.localComputerRecord.modelCode.value
+ )
+ val results = summaries.flatMap { it.toMetrics() }
+ return Metrics(metricsKeys, results)
+ }
+
+ private fun ActionTestSummary.toMetrics(): List<Metric> {
+ return performanceMetrics.values.map { metricSummary ->
+ val key = mutableMapOf(
+ "testDescription" to (title() ?: "No description"),
+ "metricName" to metricSummary.displayName.value,
+ "metricIdentifier" to metricSummary.identifier.value,
+ "polarity" to metricSummary.polarity.value,
+ "units" to metricSummary.unitOfMeasurement.value,
+ )
+ val statistics = DescriptiveStatistics()
+ metricSummary.measurements.values.forEach {
+ statistics.addValue(it.value)
+ }
+ val min = Stat("min", statistics.min)
+ // The 50th percentile is the median
+ val median = Stat("median", statistics.getPercentile(50.0))
+ val max = Stat("max", statistics.max)
+ val deviation = Stat("stddev", statistics.standardDeviation)
+ Metric(key, Measurements(listOf(min, median, max, deviation)))
+ }
+ }
+ }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/Models.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/Models.kt
new file mode 100644
index 0000000..863da27
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/Models.kt
@@ -0,0 +1,341 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.darwin.gradle.xcode
+
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import com.google.gson.annotations.SerializedName
+import java.lang.reflect.Type
+
+// Rather unfortunate that all values types are wrapped in a property bag containing a single
+// key called "_value". This is as per the JSON schema used by `xcresulttool`.
+
+data class StringTypedValue(
+ @SerializedName("_value") val value: String
+)
+
+data class IntTypedValue(
+ @SerializedName("_value") val value: Int
+)
+
+data class DoubleTypedValue(
+ @SerializedName("_value")
+ val value: Double
+)
+
+data class BooleanTypedValue(
+ @SerializedName("_value")
+ val value: Boolean
+)
+
+data class Metrics(
+ private val testsCount: IntTypedValue
+) {
+ fun size(): Int {
+ return testsCount.value
+ }
+}
+
+data class TestReference(
+ val id: StringTypedValue,
+)
+
+data class ActionResult(
+ private val status: StringTypedValue,
+ @SerializedName("testsRef") private val testsReference: TestReference
+) {
+ fun isSuccessful(): Boolean {
+ return status.value == "succeeded"
+ }
+
+ fun testsReferenceId(): String {
+ return testsReference.id.value
+ }
+}
+
+data class ActionPlatformRecord(
+ val identifier: StringTypedValue,
+ val userDescription: StringTypedValue,
+)
+
+data class ActionSDKRecord(
+ val name: StringTypedValue,
+ val identifier: StringTypedValue,
+ val operatingSystemVersion: StringTypedValue,
+ val isInternal: BooleanTypedValue,
+)
+
+data class ActionDeviceRecord(
+ val name: StringTypedValue,
+ val isConcreteDevice: BooleanTypedValue,
+ val operatingSystemVersion: StringTypedValue,
+ val operatingSystemVersionWithBuildNumber: StringTypedValue,
+ val nativeArchitecture: StringTypedValue,
+ val modelName: StringTypedValue,
+ val modelCode: StringTypedValue,
+ val identifier: StringTypedValue,
+ val isWireless: BooleanTypedValue,
+ val cpuKind: StringTypedValue,
+ val cpuCount: IntTypedValue?,
+ val cpuSpeedInMHz: IntTypedValue?,
+ val busSpeedInMHz: IntTypedValue?,
+ val ramSizeInMegabytes: IntTypedValue?,
+ val physicalCPUCoresPerPackage: IntTypedValue?,
+ val logicalCPUCoresPerPackage: IntTypedValue?,
+ val platformRecord: ActionPlatformRecord,
+)
+
+data class ActionRunDestinationRecord(
+ val displayName: StringTypedValue,
+ val targetArchitecture: StringTypedValue,
+ val targetDeviceRecord: ActionDeviceRecord,
+ val localComputerRecord: ActionDeviceRecord,
+ val targetSDKRecord: ActionSDKRecord,
+)
+
+data class ActionRecord(
+ val actionResult: ActionResult,
+ val runDestination: ActionRunDestinationRecord
+)
+
+data class Actions(
+ @SerializedName("_values") val actionRecords: List<ActionRecord>
+) {
+ fun testReferences(): List<String> {
+ return actionRecords.asSequence()
+ .map { it.actionResult.testsReferenceId() }
+ .toList()
+ }
+
+ fun isSuccessful(): Boolean {
+ return actionRecords.all { it.actionResult.isSuccessful() }
+ }
+}
+
+data class ActionsInvocationRecord(
+ val metrics: Metrics,
+ val actions: Actions
+)
+
+// Test Plan Summaries
+
+data class ActionTestMetadataSummary(
+ val id: StringTypedValue
+)
+
+data class TypeDefinition(
+ @SerializedName("_name")
+ val name: String
+)
+
+// Marker interface
+sealed interface ActionsTestSummaryGroupOrMeta
+
+data class ActionsTestSummaryGroupOrMetaArray(
+ @SerializedName("_values")
+ val values: List<ActionsTestSummaryGroupOrMeta>
+)
+
+data class ActionTestSummaryGroup(
+ val duration: DoubleTypedValue,
+ val identifier: StringTypedValue,
+ val name: StringTypedValue,
+ @SerializedName("subtests")
+ val subTests: ActionsTestSummaryGroupOrMetaArray
+) : ActionsTestSummaryGroupOrMeta {
+ fun summaries(): List<ActionTestSummaryMeta> {
+ return buildSummaries(mutableListOf(), this)
+ }
+
+ companion object {
+ internal fun buildSummaries(
+ summaries: MutableList<ActionTestSummaryMeta>,
+ group: ActionTestSummaryGroup
+ ): MutableList<ActionTestSummaryMeta> {
+ for (subTest in group.subTests.values) {
+ when (subTest) {
+ is ActionTestSummaryGroup -> buildSummaries(summaries, subTest)
+ is ActionTestSummaryMeta -> summaries += subTest
+ }
+ }
+ return summaries
+ }
+ }
+}
+
+data class ActionTestSummaryMeta(
+ val duration: DoubleTypedValue,
+ val identifier: StringTypedValue,
+ val name: StringTypedValue,
+ val summaryRef: ActionTestMetadataSummary,
+ val testStatus: StringTypedValue
+) : ActionsTestSummaryGroupOrMeta {
+ fun isSuccessful(): Boolean {
+ return testStatus.value == "Success"
+ }
+
+ fun summaryRefId(): String {
+ return summaryRef.id.value
+ }
+}
+
+class ActionTestSummaryDeserializer : JsonDeserializer<ActionsTestSummaryGroupOrMeta> {
+ override fun deserialize(
+ jsonElement: JsonElement,
+ typeOfT: Type,
+ context: JsonDeserializationContext
+ ): ActionsTestSummaryGroupOrMeta {
+ return if (checkType(jsonElement, ACTION_TEST_SUMMARY_GROUP)) {
+ val adapter = Models.gson().getAdapter(ActionTestSummaryGroup::class.java)
+ adapter.fromJson(jsonElement.toString())
+ } else if (checkType(jsonElement, ACTION_TEST_SUMMARY_META)) {
+ val adapter = Models.gson().getAdapter(ActionTestSummaryMeta::class.java)
+ adapter.fromJson(jsonElement.toString())
+ } else {
+ reportException(jsonElement)
+ }
+ }
+
+ private fun reportException(jsonElement: JsonElement): Nothing {
+ throw IllegalStateException("Unable to deserialize to ActionTestSummary ($jsonElement)")
+ }
+
+ companion object {
+ private const val TYPE = "_type"
+ private const val ACTION_TEST_SUMMARY_GROUP = "ActionTestSummaryGroup"
+ private const val ACTION_TEST_SUMMARY_META = "ActionTestMetadata"
+
+ internal fun checkType(jsonElement: JsonElement, name: String): Boolean {
+ if (!jsonElement.isJsonObject) return false
+ val json = jsonElement.asJsonObject
+ val jsonType: JsonElement? = json.get(TYPE)
+ if (jsonType != null && jsonType.isJsonObject) {
+ val adapter = Models.gson().getAdapter(TypeDefinition::class.java)
+ val type = adapter.fromJson(jsonType.toString())
+ return type.name == name
+ }
+ return false
+ }
+ }
+}
+
+data class ActionTestSummaryGroupArray(
+ @SerializedName("_values")
+ val values: List<ActionTestSummaryGroup>
+)
+
+data class ActionTestableSummary(
+ val diagnosticsDirectoryName: StringTypedValue,
+ val name: StringTypedValue,
+ val projectRelativePath: StringTypedValue,
+ val targetName: StringTypedValue,
+ val tests: ActionTestSummaryGroupArray
+)
+
+data class ActionTestableSummaryArray(
+ @SerializedName("_values")
+ val values: List<ActionTestableSummary>
+)
+
+data class ActionTestPlanRunSummary(
+ val testableSummaries: ActionTestableSummaryArray
+)
+
+data class ActionTestPlanSummaryArray(
+ @SerializedName("_values")
+ val values: List<ActionTestPlanRunSummary>
+)
+
+data class ActionTestPlanRunSummaries(
+ val summaries: ActionTestPlanSummaryArray
+) {
+ fun testSummaries(): List<ActionTestSummaryMeta> {
+ return summaries.values.flatMap { testPlanSummary ->
+ testPlanSummary.testableSummaries.values.flatMap { testableSummary ->
+ testableSummary.tests.values.flatMap { summaryGroup ->
+ summaryGroup.summaries()
+ }
+ }
+ }
+ }
+}
+
+// Test Metrics
+
+data class ActionTestActivitySummary(
+ val title: StringTypedValue
+)
+
+data class ActionTestActivitySummaryArray(
+ @SerializedName("_values")
+ val values: List<ActionTestActivitySummary>
+) {
+ fun title(): String? {
+ return values.firstOrNull()?.title?.value
+ }
+}
+
+data class MeasurementArray(
+ @SerializedName("_values")
+ val values: List<DoubleTypedValue>
+)
+
+data class ActionTestPerformanceMetricSummary(
+ val displayName: StringTypedValue,
+ val identifier: StringTypedValue,
+ val measurements: MeasurementArray,
+ val polarity: StringTypedValue,
+ val unitOfMeasurement: StringTypedValue,
+)
+
+data class ActionTestPerformanceMetricSummaryArray(
+ @SerializedName("_values")
+ val values: List<ActionTestPerformanceMetricSummary>
+)
+
+data class ActionTestSummary(
+ val activitySummaries: ActionTestActivitySummaryArray,
+ val name: StringTypedValue,
+ val testStatus: StringTypedValue,
+ val performanceMetrics: ActionTestPerformanceMetricSummaryArray
+) {
+ fun isSuccessful(): Boolean {
+ return testStatus.value == "Success"
+ }
+
+ fun title(): String? {
+ return activitySummaries.title()
+ }
+}
+
+object Models {
+ internal fun gsonBuilder(): GsonBuilder {
+ val builder = GsonBuilder()
+ builder.registerTypeAdapter(
+ ActionsTestSummaryGroupOrMeta::class.java,
+ ActionTestSummaryDeserializer()
+ )
+ return builder
+ }
+
+ fun gson(): Gson {
+ return gsonBuilder().create()
+ }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XcResultParser.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XcResultParser.kt
new file mode 100644
index 0000000..c0c3c15
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XcResultParser.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.darwin.gradle.xcode
+
+import java.io.File
+
+/**
+ * Parses benchmark results from the xcresult file.
+ * @param xcResultFile The XCResult output file to parse.
+ * @param commandExecutor An executor that can invoke the `xcrun` and get the results from `stdout`.
+ */
+class XcResultParser(
+ private val xcResultFile: File,
+ private val commandExecutor: (args: List<String>) -> String
+) {
+ fun parseResults(): Pair<ActionsInvocationRecord, List<ActionTestSummary>> {
+ val json = commandExecutor(xcRunCommand())
+ val gson = Models.gson()
+ val record = gson.fromJson(json, ActionsInvocationRecord::class.java)
+ val summaries = record.actions.testReferences().flatMap { testRef ->
+ val summary = commandExecutor(xcRunCommand(testRef))
+ val testPlanSummaries = gson.fromJson(summary, ActionTestPlanRunSummaries::class.java)
+ testPlanSummaries.testSummaries().map { summaryMeta ->
+ val output = commandExecutor(xcRunCommand(summaryMeta.summaryRefId()))
+ val testSummary = gson.fromJson(output, ActionTestSummary::class.java)
+ testSummary
+ }
+ }
+ return record to summaries
+ }
+
+ /**
+ * Builds an `xcrun` command that can parse both the xcresult scaffold, and additionally
+ * traverse nested `plist`s to pull additional benchmark result metadata.
+ */
+ private fun xcRunCommand(id: String? = null): List<String> {
+ val args = mutableListOf(
+ "xcrun",
+ "xcresulttool",
+ "get",
+ "--path",
+ xcResultFile.absolutePath,
+ "--format",
+ "json"
+ )
+
+ if (!id.isNullOrEmpty()) {
+ args += listOf("--id", id)
+ }
+
+ return args
+ }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/ModelsTest.kt b/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/ModelsTest.kt
new file mode 100644
index 0000000..663b7ea
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/ModelsTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.benchmark.darwin.gradle.xcode.ActionTestPlanRunSummaries
+import androidx.benchmark.darwin.gradle.xcode.ActionTestSummary
+import androidx.benchmark.darwin.gradle.xcode.ActionsInvocationRecord
+import androidx.benchmark.darwin.gradle.xcode.Models
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class ModelsTest {
+ @Test
+ fun parseXcResultOutputs() {
+ val json = testData(XCRESULT_OUTPUT_JSON).readText()
+ val gson = Models.gson()
+ val record = gson.fromJson(json, ActionsInvocationRecord::class.java)
+ assertThat(record.actions.testReferences().size).isEqualTo(1)
+ assertThat(record.metrics.size()).isEqualTo(1)
+ assertThat(record.actions.isSuccessful()).isTrue()
+ }
+
+ @Test
+ fun parseTestsReferenceOutput() {
+ val json = testData(XC_TESTS_REFERENCE_OUTPUT_JSON).readText()
+ val gson = Models.gson()
+ val testPlanSummaries = gson.fromJson(json, ActionTestPlanRunSummaries::class.java)
+ val testSummaryMetas = testPlanSummaries.testSummaries()
+ assertThat(testSummaryMetas.size).isEqualTo(1)
+ assertThat(testSummaryMetas[0].summaryRefId()).isNotEmpty()
+ assertThat(testSummaryMetas[0].isSuccessful()).isTrue()
+ }
+
+ @Test
+ fun parseTestOutput() {
+ val json = testData(XC_TEST_OUTPUT_JSON).readText()
+ val gson = Models.gson()
+ val testSummary = gson.fromJson(json, ActionTestSummary::class.java)
+ assertThat(testSummary.title()).isNotEmpty()
+ assertThat(testSummary.isSuccessful()).isTrue()
+ }
+
+ companion object {
+ private const val XCRESULT_OUTPUT_JSON = "xcresult_output.json"
+ private const val XC_TESTS_REFERENCE_OUTPUT_JSON = "tests_reference_output.json"
+ private const val XC_TEST_OUTPUT_JSON = "test_output.json"
+ }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/TestExtensions.kt b/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/TestExtensions.kt
new file mode 100644
index 0000000..28003fa
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/TestExtensions.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.File
+
+internal fun testData(name: String): File {
+ return File("src/test/test-data", name)
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt b/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt
new file mode 100644
index 0000000..00468a9
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.benchmark.darwin.gradle.skia.Metrics
+import androidx.benchmark.darwin.gradle.xcode.Models
+import androidx.benchmark.darwin.gradle.xcode.XcResultParser
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assume.assumeTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class XcResultParserTest {
+ @Test
+ fun parseXcResultFileTest() {
+ val operatingSystem = System.getProperty("os.name")
+ // Only run this test on an `Mac OS X` machine.
+ assumeTrue(operatingSystem.contains("Mac", ignoreCase = true))
+ val xcResultFile = testData("sample-xcode.xcresult")
+ val parser = XcResultParser(xcResultFile) { args ->
+ val builder = ProcessBuilder(*args.toTypedArray())
+ val process = builder.start()
+ val resultCode = process.waitFor()
+ require(resultCode == 0) {
+ "Process terminated unexpectedly (${args.joinToString(separator = " ")})"
+ }
+ process.inputStream.use {
+ it.reader().readText()
+ }
+ }
+ val (record, summaries) = parser.parseResults()
+ // Usually corresponds to the size of the test suite
+ // In the case of KMP benchmarks, this is always 1 per module.
+ assertThat(record.actions.testReferences().size).isEqualTo(1)
+ assertThat(record.actions.isSuccessful()).isTrue()
+ // Metrics typically correspond to the number of tests
+ assertThat(record.metrics.size()).isEqualTo(2)
+ assertThat(summaries.isNotEmpty()).isTrue()
+ val metrics = Metrics.buildMetrics(record, summaries)
+ val json = Models.gsonBuilder()
+ .setPrettyPrinting()
+ .create()
+ .toJson(metrics)
+ println()
+ println(json)
+ println()
+ assertThat(json).isNotEmpty()
+ }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/README.md b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/README.md
new file mode 100644
index 0000000..6610e6b
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/README.md
@@ -0,0 +1,18 @@
+# Test Data
+
+## [sample-xcode.xcresult](./sample-xcode.xcresult)
+
+This is an example benchmark `xcresult` directory that is the result of running an `xcodebuild`.
+
+An example invocation might look something like:
+
+```bash
+xcodebuild test -project $SRCROOT/benchmark-darwin-samples-xcode.xcodeproj \
+ -scheme testapp-ios \
+ -destination id=7F61C467-4E4A-437C-B6EF-026FEEF3904C \
+ -resultBundlePath $SRCROOT/benchmark-darwin-samples-xcode.xcresult
+```
+
+The `xcresult` output directory stores results in a nested `plist` format. Entities in the top-level
+`plist` file point to other entities stored in other`plist` files (inside the `Data` directory),
+using a unique filename identifier.
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~4VqMqsI5lOfxRppnud6-VDWcNsU8J7VgFCJfW2dXPwOcAkvU-I8Um5yp9n0Zv6nr3VmcxYggaVMDFfR0U_vjKw== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~4VqMqsI5lOfxRppnud6-VDWcNsU8J7VgFCJfW2dXPwOcAkvU-I8Um5yp9n0Zv6nr3VmcxYggaVMDFfR0U_vjKw==
new file mode 100644
index 0000000..0637a08
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~4VqMqsI5lOfxRppnud6-VDWcNsU8J7VgFCJfW2dXPwOcAkvU-I8Um5yp9n0Zv6nr3VmcxYggaVMDFfR0U_vjKw==
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~809cv4Ku3dnBcPFN0EL1A1xx_h4MH0THT41Rlz6CEankhzddTnjMa6CWhjt9F69jFsquD4x8Offmms1sFw6EKA== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~809cv4Ku3dnBcPFN0EL1A1xx_h4MH0THT41Rlz6CEankhzddTnjMa6CWhjt9F69jFsquD4x8Offmms1sFw6EKA==
new file mode 100644
index 0000000..bf96d3c
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~809cv4Ku3dnBcPFN0EL1A1xx_h4MH0THT41Rlz6CEankhzddTnjMa6CWhjt9F69jFsquD4x8Offmms1sFw6EKA==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~BVkwcGZLlAQkY6Iyxs4jrfCad9IriiNRwFgkj-q5CYa3nlIwjU66k7hfhLcnmUHGVk4q8lK8eljDDjEI2_UwrA== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~BVkwcGZLlAQkY6Iyxs4jrfCad9IriiNRwFgkj-q5CYa3nlIwjU66k7hfhLcnmUHGVk4q8lK8eljDDjEI2_UwrA==
new file mode 100644
index 0000000..07b9130
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~BVkwcGZLlAQkY6Iyxs4jrfCad9IriiNRwFgkj-q5CYa3nlIwjU66k7hfhLcnmUHGVk4q8lK8eljDDjEI2_UwrA==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~NcxF_MrjcLDoh1KWbeGq453YSibqfUcZRlcfxDlh1HVpo6_BqONP2ZB1AiJYY9lYfIGL1g5h3VsbOKnwGLjywQ== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~NcxF_MrjcLDoh1KWbeGq453YSibqfUcZRlcfxDlh1HVpo6_BqONP2ZB1AiJYY9lYfIGL1g5h3VsbOKnwGLjywQ==
new file mode 100644
index 0000000..13d30be
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~NcxF_MrjcLDoh1KWbeGq453YSibqfUcZRlcfxDlh1HVpo6_BqONP2ZB1AiJYY9lYfIGL1g5h3VsbOKnwGLjywQ==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~PBlxYYh3AAyWwRJ85oIfJdGUcOuzcjExLhwrde-sNFno45FanCv9KkZmQ_eAJL9BVGKBh78r4OF_Fw2MeiGPYg== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~PBlxYYh3AAyWwRJ85oIfJdGUcOuzcjExLhwrde-sNFno45FanCv9KkZmQ_eAJL9BVGKBh78r4OF_Fw2MeiGPYg==
new file mode 100644
index 0000000..3032bd0
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~PBlxYYh3AAyWwRJ85oIfJdGUcOuzcjExLhwrde-sNFno45FanCv9KkZmQ_eAJL9BVGKBh78r4OF_Fw2MeiGPYg==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~TV3vc3QcCkBeyTI4OZibWTUhnLOa2pj_khWYMDIsFQfzU1CC6gAO1HxXclYQhodcuQjZSw7GGBlpuUT8X0uctA== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~TV3vc3QcCkBeyTI4OZibWTUhnLOa2pj_khWYMDIsFQfzU1CC6gAO1HxXclYQhodcuQjZSw7GGBlpuUT8X0uctA==
new file mode 100644
index 0000000..305a73b
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~TV3vc3QcCkBeyTI4OZibWTUhnLOa2pj_khWYMDIsFQfzU1CC6gAO1HxXclYQhodcuQjZSw7GGBlpuUT8X0uctA==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~YK5eNAHGX7bQW65jgs2rLhWNoTj7vgF1CBLZ2k12n3svTuVa_whSNkDWJY9V9-QVC_GCh2-DzDDLoIpXvYOToA== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~YK5eNAHGX7bQW65jgs2rLhWNoTj7vgF1CBLZ2k12n3svTuVa_whSNkDWJY9V9-QVC_GCh2-DzDDLoIpXvYOToA==
new file mode 100644
index 0000000..9a0adba
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~YK5eNAHGX7bQW65jgs2rLhWNoTj7vgF1CBLZ2k12n3svTuVa_whSNkDWJY9V9-QVC_GCh2-DzDDLoIpXvYOToA==
@@ -0,0 +1 @@
+[{"name":"testmanagerd.log","type":1}]
\ No newline at end of file
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~bO5MkLRUHsU78utGaMM3HVQkRM7RbYps9mAfFre5AyZnGEAQmWmCQMb2RChRtxj7aiaDPHDpQxhAswnGBW3rAA== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~bO5MkLRUHsU78utGaMM3HVQkRM7RbYps9mAfFre5AyZnGEAQmWmCQMb2RChRtxj7aiaDPHDpQxhAswnGBW3rAA==
new file mode 100644
index 0000000..52a8bc8
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~bO5MkLRUHsU78utGaMM3HVQkRM7RbYps9mAfFre5AyZnGEAQmWmCQMb2RChRtxj7aiaDPHDpQxhAswnGBW3rAA==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~eMJEUpMqij97v52rtaj8rSl-UZJhPmN04NCDD0JZ0cWyFbAHRCbjw5PJqWyqalQuFswnMkB6LvsT3uFLJVGVqw== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~eMJEUpMqij97v52rtaj8rSl-UZJhPmN04NCDD0JZ0cWyFbAHRCbjw5PJqWyqalQuFswnMkB6LvsT3uFLJVGVqw==
new file mode 100644
index 0000000..b385fa3
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~eMJEUpMqij97v52rtaj8rSl-UZJhPmN04NCDD0JZ0cWyFbAHRCbjw5PJqWyqalQuFswnMkB6LvsT3uFLJVGVqw==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~hncU2qbL3W7Jwz-E6zuM7ThWI10rl9EEkPM5cV70-odmasyId-QIR9ZRmIMwYF8DCechev-bgJgPAiYsyImmnQ== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~hncU2qbL3W7Jwz-E6zuM7ThWI10rl9EEkPM5cV70-odmasyId-QIR9ZRmIMwYF8DCechev-bgJgPAiYsyImmnQ==
new file mode 100644
index 0000000..195b954
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~hncU2qbL3W7Jwz-E6zuM7ThWI10rl9EEkPM5cV70-odmasyId-QIR9ZRmIMwYF8DCechev-bgJgPAiYsyImmnQ==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~l90yex7j_mtyE5LhZFKn-M7ovFj_tArzqr984SmaEo2RQ3JhValxuXW7UXxzNRGaNamX9MMhtWVxU-wT9DA6Zg== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~l90yex7j_mtyE5LhZFKn-M7ovFj_tArzqr984SmaEo2RQ3JhValxuXW7UXxzNRGaNamX9MMhtWVxU-wT9DA6Zg==
new file mode 100644
index 0000000..88dc22a
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~l90yex7j_mtyE5LhZFKn-M7ovFj_tArzqr984SmaEo2RQ3JhValxuXW7UXxzNRGaNamX9MMhtWVxU-wT9DA6Zg==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~no8uvpmY2pkdGvoMP79k6Q4ikuedatoSuF6vBG0HR8h0GsBvPND_wHS-M8mBpxtX6NzpHV4bKIhwFkxUsd7RXA== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~no8uvpmY2pkdGvoMP79k6Q4ikuedatoSuF6vBG0HR8h0GsBvPND_wHS-M8mBpxtX6NzpHV4bKIhwFkxUsd7RXA==
new file mode 100644
index 0000000..c4b8635
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~no8uvpmY2pkdGvoMP79k6Q4ikuedatoSuF6vBG0HR8h0GsBvPND_wHS-M8mBpxtX6NzpHV4bKIhwFkxUsd7RXA==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~vcNmCXushFQo2Asf-Ddow5-J6PKttgTE0SMgzoRxYZv26906WiKBSKSrE4LJnPfLcKZXgqb8b9B9dS0xNC6phw== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~vcNmCXushFQo2Asf-Ddow5-J6PKttgTE0SMgzoRxYZv26906WiKBSKSrE4LJnPfLcKZXgqb8b9B9dS0xNC6phw==
new file mode 100644
index 0000000..47daeba
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~vcNmCXushFQo2Asf-Ddow5-J6PKttgTE0SMgzoRxYZv26906WiKBSKSrE4LJnPfLcKZXgqb8b9B9dS0xNC6phw==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~wV6va8rQlQLAcZ8rb5UAf1mKxaJdF9ZDlsizSwQ7FqQynxq_02ZsiipQ63-2YJ3jEODImbyk3FvKgJ2KaOPOPQ== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~wV6va8rQlQLAcZ8rb5UAf1mKxaJdF9ZDlsizSwQ7FqQynxq_02ZsiipQ63-2YJ3jEODImbyk3FvKgJ2KaOPOPQ==
new file mode 100644
index 0000000..ff4a430
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~wV6va8rQlQLAcZ8rb5UAf1mKxaJdF9ZDlsizSwQ7FqQynxq_02ZsiipQ63-2YJ3jEODImbyk3FvKgJ2KaOPOPQ==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~wpEwfbWjFv9AR7VY2bJ5g_ndbKMYwUEFGZ1w3MdarhPZE605CCX5XHRoL5cjlzYTdIpHF8aXtcLt6kz2ELrAsQ== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~wpEwfbWjFv9AR7VY2bJ5g_ndbKMYwUEFGZ1w3MdarhPZE605CCX5XHRoL5cjlzYTdIpHF8aXtcLt6kz2ELrAsQ==
new file mode 100644
index 0000000..70db723
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~wpEwfbWjFv9AR7VY2bJ5g_ndbKMYwUEFGZ1w3MdarhPZE605CCX5XHRoL5cjlzYTdIpHF8aXtcLt6kz2ELrAsQ==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~z_lmQvK-mq_MbksvJ2oEALgVyhIwk-VirNem09JWgC47IaBCD7JaSPfQeouKa8iSKUchkiKh993EavN5hgc0Mg== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~z_lmQvK-mq_MbksvJ2oEALgVyhIwk-VirNem09JWgC47IaBCD7JaSPfQeouKa8iSKUchkiKh993EavN5hgc0Mg==
new file mode 100644
index 0000000..0d78c58
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/data.0~z_lmQvK-mq_MbksvJ2oEALgVyhIwk-VirNem09JWgC47IaBCD7JaSPfQeouKa8iSKUchkiKh993EavN5hgc0Mg==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~4VqMqsI5lOfxRppnud6-VDWcNsU8J7VgFCJfW2dXPwOcAkvU-I8Um5yp9n0Zv6nr3VmcxYggaVMDFfR0U_vjKw== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~4VqMqsI5lOfxRppnud6-VDWcNsU8J7VgFCJfW2dXPwOcAkvU-I8Um5yp9n0Zv6nr3VmcxYggaVMDFfR0U_vjKw==
new file mode 100644
index 0000000..f76dd23
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~4VqMqsI5lOfxRppnud6-VDWcNsU8J7VgFCJfW2dXPwOcAkvU-I8Um5yp9n0Zv6nr3VmcxYggaVMDFfR0U_vjKw==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~809cv4Ku3dnBcPFN0EL1A1xx_h4MH0THT41Rlz6CEankhzddTnjMa6CWhjt9F69jFsquD4x8Offmms1sFw6EKA== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~809cv4Ku3dnBcPFN0EL1A1xx_h4MH0THT41Rlz6CEankhzddTnjMa6CWhjt9F69jFsquD4x8Offmms1sFw6EKA==
new file mode 100644
index 0000000..0ec3518
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~809cv4Ku3dnBcPFN0EL1A1xx_h4MH0THT41Rlz6CEankhzddTnjMa6CWhjt9F69jFsquD4x8Offmms1sFw6EKA==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~BVkwcGZLlAQkY6Iyxs4jrfCad9IriiNRwFgkj-q5CYa3nlIwjU66k7hfhLcnmUHGVk4q8lK8eljDDjEI2_UwrA== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~BVkwcGZLlAQkY6Iyxs4jrfCad9IriiNRwFgkj-q5CYa3nlIwjU66k7hfhLcnmUHGVk4q8lK8eljDDjEI2_UwrA==
new file mode 100644
index 0000000..f76dd23
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~BVkwcGZLlAQkY6Iyxs4jrfCad9IriiNRwFgkj-q5CYa3nlIwjU66k7hfhLcnmUHGVk4q8lK8eljDDjEI2_UwrA==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~NcxF_MrjcLDoh1KWbeGq453YSibqfUcZRlcfxDlh1HVpo6_BqONP2ZB1AiJYY9lYfIGL1g5h3VsbOKnwGLjywQ== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~NcxF_MrjcLDoh1KWbeGq453YSibqfUcZRlcfxDlh1HVpo6_BqONP2ZB1AiJYY9lYfIGL1g5h3VsbOKnwGLjywQ==
new file mode 100644
index 0000000..f76dd23
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~NcxF_MrjcLDoh1KWbeGq453YSibqfUcZRlcfxDlh1HVpo6_BqONP2ZB1AiJYY9lYfIGL1g5h3VsbOKnwGLjywQ==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~PBlxYYh3AAyWwRJ85oIfJdGUcOuzcjExLhwrde-sNFno45FanCv9KkZmQ_eAJL9BVGKBh78r4OF_Fw2MeiGPYg== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~PBlxYYh3AAyWwRJ85oIfJdGUcOuzcjExLhwrde-sNFno45FanCv9KkZmQ_eAJL9BVGKBh78r4OF_Fw2MeiGPYg==
new file mode 100644
index 0000000..6fde618
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~PBlxYYh3AAyWwRJ85oIfJdGUcOuzcjExLhwrde-sNFno45FanCv9KkZmQ_eAJL9BVGKBh78r4OF_Fw2MeiGPYg==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~TV3vc3QcCkBeyTI4OZibWTUhnLOa2pj_khWYMDIsFQfzU1CC6gAO1HxXclYQhodcuQjZSw7GGBlpuUT8X0uctA== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~TV3vc3QcCkBeyTI4OZibWTUhnLOa2pj_khWYMDIsFQfzU1CC6gAO1HxXclYQhodcuQjZSw7GGBlpuUT8X0uctA==
new file mode 100644
index 0000000..f76dd23
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~TV3vc3QcCkBeyTI4OZibWTUhnLOa2pj_khWYMDIsFQfzU1CC6gAO1HxXclYQhodcuQjZSw7GGBlpuUT8X0uctA==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~YK5eNAHGX7bQW65jgs2rLhWNoTj7vgF1CBLZ2k12n3svTuVa_whSNkDWJY9V9-QVC_GCh2-DzDDLoIpXvYOToA== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~YK5eNAHGX7bQW65jgs2rLhWNoTj7vgF1CBLZ2k12n3svTuVa_whSNkDWJY9V9-QVC_GCh2-DzDDLoIpXvYOToA==
new file mode 100644
index 0000000..7bc9ef9c
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~YK5eNAHGX7bQW65jgs2rLhWNoTj7vgF1CBLZ2k12n3svTuVa_whSNkDWJY9V9-QVC_GCh2-DzDDLoIpXvYOToA==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~bO5MkLRUHsU78utGaMM3HVQkRM7RbYps9mAfFre5AyZnGEAQmWmCQMb2RChRtxj7aiaDPHDpQxhAswnGBW3rAA== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~bO5MkLRUHsU78utGaMM3HVQkRM7RbYps9mAfFre5AyZnGEAQmWmCQMb2RChRtxj7aiaDPHDpQxhAswnGBW3rAA==
new file mode 100644
index 0000000..f76dd23
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~bO5MkLRUHsU78utGaMM3HVQkRM7RbYps9mAfFre5AyZnGEAQmWmCQMb2RChRtxj7aiaDPHDpQxhAswnGBW3rAA==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~eMJEUpMqij97v52rtaj8rSl-UZJhPmN04NCDD0JZ0cWyFbAHRCbjw5PJqWyqalQuFswnMkB6LvsT3uFLJVGVqw== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~eMJEUpMqij97v52rtaj8rSl-UZJhPmN04NCDD0JZ0cWyFbAHRCbjw5PJqWyqalQuFswnMkB6LvsT3uFLJVGVqw==
new file mode 100644
index 0000000..f76dd23
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~eMJEUpMqij97v52rtaj8rSl-UZJhPmN04NCDD0JZ0cWyFbAHRCbjw5PJqWyqalQuFswnMkB6LvsT3uFLJVGVqw==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~hncU2qbL3W7Jwz-E6zuM7ThWI10rl9EEkPM5cV70-odmasyId-QIR9ZRmIMwYF8DCechev-bgJgPAiYsyImmnQ== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~hncU2qbL3W7Jwz-E6zuM7ThWI10rl9EEkPM5cV70-odmasyId-QIR9ZRmIMwYF8DCechev-bgJgPAiYsyImmnQ==
new file mode 100644
index 0000000..f76dd23
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~hncU2qbL3W7Jwz-E6zuM7ThWI10rl9EEkPM5cV70-odmasyId-QIR9ZRmIMwYF8DCechev-bgJgPAiYsyImmnQ==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~l90yex7j_mtyE5LhZFKn-M7ovFj_tArzqr984SmaEo2RQ3JhValxuXW7UXxzNRGaNamX9MMhtWVxU-wT9DA6Zg== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~l90yex7j_mtyE5LhZFKn-M7ovFj_tArzqr984SmaEo2RQ3JhValxuXW7UXxzNRGaNamX9MMhtWVxU-wT9DA6Zg==
new file mode 100644
index 0000000..f76dd23
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~l90yex7j_mtyE5LhZFKn-M7ovFj_tArzqr984SmaEo2RQ3JhValxuXW7UXxzNRGaNamX9MMhtWVxU-wT9DA6Zg==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~no8uvpmY2pkdGvoMP79k6Q4ikuedatoSuF6vBG0HR8h0GsBvPND_wHS-M8mBpxtX6NzpHV4bKIhwFkxUsd7RXA== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~no8uvpmY2pkdGvoMP79k6Q4ikuedatoSuF6vBG0HR8h0GsBvPND_wHS-M8mBpxtX6NzpHV4bKIhwFkxUsd7RXA==
new file mode 100644
index 0000000..e12afe0
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~no8uvpmY2pkdGvoMP79k6Q4ikuedatoSuF6vBG0HR8h0GsBvPND_wHS-M8mBpxtX6NzpHV4bKIhwFkxUsd7RXA==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~vcNmCXushFQo2Asf-Ddow5-J6PKttgTE0SMgzoRxYZv26906WiKBSKSrE4LJnPfLcKZXgqb8b9B9dS0xNC6phw== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~vcNmCXushFQo2Asf-Ddow5-J6PKttgTE0SMgzoRxYZv26906WiKBSKSrE4LJnPfLcKZXgqb8b9B9dS0xNC6phw==
new file mode 100644
index 0000000..bd95f97
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~vcNmCXushFQo2Asf-Ddow5-J6PKttgTE0SMgzoRxYZv26906WiKBSKSrE4LJnPfLcKZXgqb8b9B9dS0xNC6phw==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~wV6va8rQlQLAcZ8rb5UAf1mKxaJdF9ZDlsizSwQ7FqQynxq_02ZsiipQ63-2YJ3jEODImbyk3FvKgJ2KaOPOPQ== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~wV6va8rQlQLAcZ8rb5UAf1mKxaJdF9ZDlsizSwQ7FqQynxq_02ZsiipQ63-2YJ3jEODImbyk3FvKgJ2KaOPOPQ==
new file mode 100644
index 0000000..f76dd23
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~wV6va8rQlQLAcZ8rb5UAf1mKxaJdF9ZDlsizSwQ7FqQynxq_02ZsiipQ63-2YJ3jEODImbyk3FvKgJ2KaOPOPQ==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~wpEwfbWjFv9AR7VY2bJ5g_ndbKMYwUEFGZ1w3MdarhPZE605CCX5XHRoL5cjlzYTdIpHF8aXtcLt6kz2ELrAsQ== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~wpEwfbWjFv9AR7VY2bJ5g_ndbKMYwUEFGZ1w3MdarhPZE605CCX5XHRoL5cjlzYTdIpHF8aXtcLt6kz2ELrAsQ==
new file mode 100644
index 0000000..a5fa9ca
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~wpEwfbWjFv9AR7VY2bJ5g_ndbKMYwUEFGZ1w3MdarhPZE605CCX5XHRoL5cjlzYTdIpHF8aXtcLt6kz2ELrAsQ==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~z_lmQvK-mq_MbksvJ2oEALgVyhIwk-VirNem09JWgC47IaBCD7JaSPfQeouKa8iSKUchkiKh993EavN5hgc0Mg== b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~z_lmQvK-mq_MbksvJ2oEALgVyhIwk-VirNem09JWgC47IaBCD7JaSPfQeouKa8iSKUchkiKh993EavN5hgc0Mg==
new file mode 100644
index 0000000..f76dd23
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Data/refs.0~z_lmQvK-mq_MbksvJ2oEALgVyhIwk-VirNem09JWgC47IaBCD7JaSPfQeouKa8iSKUchkiKh993EavN5hgc0Mg==
Binary files differ
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Info.plist b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Info.plist
new file mode 100644
index 0000000..8e63641
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/sample-xcode.xcresult/Info.plist
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>dateCreated</key>
+ <date>2022-09-23T17:38:01Z</date>
+ <key>externalLocations</key>
+ <array/>
+ <key>rootId</key>
+ <dict>
+ <key>hash</key>
+ <string>0~vcNmCXushFQo2Asf-Ddow5-J6PKttgTE0SMgzoRxYZv26906WiKBSKSrE4LJnPfLcKZXgqb8b9B9dS0xNC6phw==</string>
+ </dict>
+ <key>storage</key>
+ <dict>
+ <key>backend</key>
+ <string>fileBacked2</string>
+ <key>compression</key>
+ <string>standard</string>
+ </dict>
+ <key>version</key>
+ <dict>
+ <key>major</key>
+ <integer>3</integer>
+ <key>minor</key>
+ <integer>34</integer>
+ </dict>
+</dict>
+</plist>
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/test_output.json b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/test_output.json
new file mode 100644
index 0000000..a8287ad
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/test_output.json
@@ -0,0 +1,624 @@
+{
+ "_type" : {
+ "_name" : "ActionTestSummary",
+ "_supertype" : {
+ "_name" : "ActionTestSummaryIdentifiableObject",
+ "_supertype" : {
+ "_name" : "ActionAbstractTestSummary"
+ }
+ }
+ },
+ "activitySummaries" : {
+ "_type" : {
+ "_name" : "Array"
+ },
+ "_values" : [
+ {
+ "_type" : {
+ "_name" : "ActionTestActivitySummary"
+ },
+ "activityType" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "com.apple.dt.xctest.activity-type.userCreated"
+ },
+ "finish" : {
+ "_type" : {
+ "_name" : "Date"
+ },
+ "_value" : "2022-09-20T15:39:30.433-0700"
+ },
+ "start" : {
+ "_type" : {
+ "_name" : "Date"
+ },
+ "_value" : "2022-09-20T15:39:30.366-0700"
+ },
+ "title" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "Allocate an ArrayList of size 1000"
+ },
+ "uuid" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "660362D3-AA8A-4630-B098-780ED90922E2"
+ }
+ }
+ ]
+ },
+ "duration" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0741419792175293"
+ },
+ "identifier" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "BenchmarkTest\/runBenchmark()"
+ },
+ "name" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "runBenchmark()"
+ },
+ "performanceMetrics" : {
+ "_type" : {
+ "_name" : "Array"
+ },
+ "_values" : [
+ {
+ "_type" : {
+ "_name" : "ActionTestPerformanceMetricSummary"
+ },
+ "displayName" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "Memory Physical"
+ },
+ "identifier" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "com.apple.dt.XCTMetric_Memory.physical"
+ },
+ "maxPercentRegression" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "10.0"
+ },
+ "maxPercentRelativeStandardDeviation" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "10.0"
+ },
+ "maxRegression" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0"
+ },
+ "maxStandardDeviation" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0"
+ },
+ "measurements" : {
+ "_type" : {
+ "_name" : "Array"
+ },
+ "_values" : [
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "8208.448"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "8192.0"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "8192.0"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "8208.384"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "8257.536"
+ }
+ ]
+ },
+ "polarity" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "prefers smaller"
+ },
+ "unitOfMeasurement" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "kB"
+ }
+ },
+ {
+ "_type" : {
+ "_name" : "ActionTestPerformanceMetricSummary"
+ },
+ "displayName" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "Clock Monotonic Time"
+ },
+ "identifier" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "com.apple.dt.XCTMetric_Clock.time.monotonic"
+ },
+ "maxPercentRegression" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "10.0"
+ },
+ "maxPercentRelativeStandardDeviation" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "10.0"
+ },
+ "maxRegression" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0"
+ },
+ "maxStandardDeviation" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0"
+ },
+ "measurements" : {
+ "_type" : {
+ "_name" : "Array"
+ },
+ "_values" : [
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.003374259"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0011105260000000001"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0009685430000000001"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.001031847"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.006419944"
+ }
+ ]
+ },
+ "polarity" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "prefers smaller"
+ },
+ "unitOfMeasurement" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "s"
+ }
+ },
+ {
+ "_type" : {
+ "_name" : "ActionTestPerformanceMetricSummary"
+ },
+ "displayName" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "CPU Cycles"
+ },
+ "identifier" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "com.apple.dt.XCTMetric_CPU.cycles"
+ },
+ "maxPercentRegression" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "10.0"
+ },
+ "maxPercentRelativeStandardDeviation" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "10.0"
+ },
+ "maxRegression" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0"
+ },
+ "maxStandardDeviation" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0"
+ },
+ "measurements" : {
+ "_type" : {
+ "_name" : "Array"
+ },
+ "_values" : [
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "8568.973"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "5173.538"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "4663.702"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "4684.408"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "21681.682"
+ }
+ ]
+ },
+ "polarity" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "prefers smaller"
+ },
+ "unitOfMeasurement" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "kC"
+ }
+ },
+ {
+ "_type" : {
+ "_name" : "ActionTestPerformanceMetricSummary"
+ },
+ "displayName" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "CPU Time"
+ },
+ "identifier" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "com.apple.dt.XCTMetric_CPU.time"
+ },
+ "maxPercentRegression" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "10.0"
+ },
+ "maxPercentRelativeStandardDeviation" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "10.0"
+ },
+ "maxRegression" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0"
+ },
+ "maxStandardDeviation" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0"
+ },
+ "measurements" : {
+ "_type" : {
+ "_name" : "Array"
+ },
+ "_values" : [
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0027404810000000003"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0016844030000000001"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0014999850000000001"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0015120390000000002"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.007035559"
+ }
+ ]
+ },
+ "polarity" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "prefers smaller"
+ },
+ "unitOfMeasurement" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "s"
+ }
+ },
+ {
+ "_type" : {
+ "_name" : "ActionTestPerformanceMetricSummary"
+ },
+ "displayName" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "CPU Instructions Retired"
+ },
+ "identifier" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "com.apple.dt.XCTMetric_CPU.instructions_retired"
+ },
+ "maxPercentRegression" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "10.0"
+ },
+ "maxPercentRelativeStandardDeviation" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "10.0"
+ },
+ "maxRegression" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0"
+ },
+ "maxStandardDeviation" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0"
+ },
+ "measurements" : {
+ "_type" : {
+ "_name" : "Array"
+ },
+ "_values" : [
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "16386.777"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "8917.514"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "8777.292"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "8398.467"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "40257.553"
+ }
+ ]
+ },
+ "polarity" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "prefers smaller"
+ },
+ "unitOfMeasurement" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "kI"
+ }
+ },
+ {
+ "_type" : {
+ "_name" : "ActionTestPerformanceMetricSummary"
+ },
+ "displayName" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "Memory Peak Physical"
+ },
+ "identifier" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "com.apple.dt.XCTMetric_Memory.physical_peak"
+ },
+ "maxPercentRegression" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "10.0"
+ },
+ "maxPercentRelativeStandardDeviation" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "10.0"
+ },
+ "maxRegression" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0"
+ },
+ "maxStandardDeviation" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0"
+ },
+ "measurements" : {
+ "_type" : {
+ "_name" : "Array"
+ },
+ "_values" : [
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0"
+ },
+ {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0"
+ }
+ ]
+ },
+ "polarity" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "prefers smaller"
+ },
+ "unitOfMeasurement" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "kB"
+ }
+ }
+ ]
+ },
+ "testStatus" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "Success"
+ }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/tests_reference_output.json b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/tests_reference_output.json
new file mode 100644
index 0000000..9b3286a
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/tests_reference_output.json
@@ -0,0 +1,277 @@
+{
+ "_type" : {
+ "_name" : "ActionTestPlanRunSummaries"
+ },
+ "summaries" : {
+ "_type" : {
+ "_name" : "Array"
+ },
+ "_values" : [
+ {
+ "_type" : {
+ "_name" : "ActionTestPlanRunSummary",
+ "_supertype" : {
+ "_name" : "ActionAbstractTestSummary"
+ }
+ },
+ "name" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "Test Scheme Action"
+ },
+ "testableSummaries" : {
+ "_type" : {
+ "_name" : "Array"
+ },
+ "_values" : [
+ {
+ "_type" : {
+ "_name" : "ActionTestableSummary",
+ "_supertype" : {
+ "_name" : "ActionAbstractTestSummary"
+ }
+ },
+ "diagnosticsDirectoryName" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "testapp-ios-benchmarks-D5CA694B-66BF-479C-ABE4-0D3BDF591768-Configuration-Test Scheme Action-Iteration-1"
+ },
+ "name" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "testapp-ios-benchmarks"
+ },
+ "projectRelativePath" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "benchmark-darwin-sample-xcode.xcodeproj"
+ },
+ "targetName" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "testapp-ios-benchmarks"
+ },
+ "testKind" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "xctest-tool hosted"
+ },
+ "testLanguage" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : ""
+ },
+ "testRegion" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : ""
+ },
+ "tests" : {
+ "_type" : {
+ "_name" : "Array"
+ },
+ "_values" : [
+ {
+ "_type" : {
+ "_name" : "ActionTestSummaryGroup",
+ "_supertype" : {
+ "_name" : "ActionTestSummaryIdentifiableObject",
+ "_supertype" : {
+ "_name" : "ActionAbstractTestSummary"
+ }
+ }
+ },
+ "duration" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.07568299770355225"
+ },
+ "identifier" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "All tests"
+ },
+ "name" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "All tests"
+ },
+ "subtests" : {
+ "_type" : {
+ "_name" : "Array"
+ },
+ "_values" : [
+ {
+ "_type" : {
+ "_name" : "ActionTestSummaryGroup",
+ "_supertype" : {
+ "_name" : "ActionTestSummaryIdentifiableObject",
+ "_supertype" : {
+ "_name" : "ActionAbstractTestSummary"
+ }
+ }
+ },
+ "duration" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.07503998279571533"
+ },
+ "identifier" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "testapp-ios-benchmarks.xctest"
+ },
+ "name" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "testapp-ios-benchmarks.xctest"
+ },
+ "subtests" : {
+ "_type" : {
+ "_name" : "Array"
+ },
+ "_values" : [
+ {
+ "_type" : {
+ "_name" : "ActionTestSummaryGroup",
+ "_supertype" : {
+ "_name" : "ActionTestSummaryIdentifiableObject",
+ "_supertype" : {
+ "_name" : "ActionAbstractTestSummary"
+ }
+ }
+ },
+ "duration" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.07448196411132812"
+ },
+ "identifier" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "BenchmarkTest"
+ },
+ "name" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "BenchmarkTest"
+ },
+ "subtests" : {
+ "_type" : {
+ "_name" : "Array"
+ },
+ "_values" : [
+ {
+ "_type" : {
+ "_name" : "ActionTestMetadata",
+ "_supertype" : {
+ "_name" : "ActionTestSummaryIdentifiableObject",
+ "_supertype" : {
+ "_name" : "ActionAbstractTestSummary"
+ }
+ }
+ },
+ "duration" : {
+ "_type" : {
+ "_name" : "Double"
+ },
+ "_value" : "0.0741419792175293"
+ },
+ "identifier" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "BenchmarkTest\/runBenchmark()"
+ },
+ "name" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "runBenchmark()"
+ },
+ "summaryRef" : {
+ "_type" : {
+ "_name" : "Reference"
+ },
+ "id" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "0~9QZnTT_BgRyY356cOax6Xlj_lclcNKu46aQPHsHicDo5aDaBNGg0f1y5hrlGl30pfE8RtG0-7IY1sZEQSgv-yA=="
+ },
+ "targetType" : {
+ "_type" : {
+ "_name" : "TypeDefinition"
+ },
+ "name" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "ActionTestSummary"
+ },
+ "supertype" : {
+ "_type" : {
+ "_name" : "TypeDefinition"
+ },
+ "name" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "ActionTestSummaryIdentifiableObject"
+ },
+ "supertype" : {
+ "_type" : {
+ "_name" : "TypeDefinition"
+ },
+ "name" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "ActionAbstractTestSummary"
+ }
+ }
+ }
+ }
+ },
+ "testStatus" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "Success"
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/xcresult_output.json b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/xcresult_output.json
new file mode 100644
index 0000000..bedddde
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/test-data/xcresult_output.json
@@ -0,0 +1,502 @@
+{
+ "_type" : {
+ "_name" : "ActionsInvocationRecord"
+ },
+ "actions" : {
+ "_type" : {
+ "_name" : "Array"
+ },
+ "_values" : [
+ {
+ "_type" : {
+ "_name" : "ActionRecord"
+ },
+ "actionResult" : {
+ "_type" : {
+ "_name" : "ActionResult"
+ },
+ "coverage" : {
+ "_type" : {
+ "_name" : "CodeCoverageInfo"
+ }
+ },
+ "diagnosticsRef" : {
+ "_type" : {
+ "_name" : "Reference"
+ },
+ "id" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "0~jAQXUmgyNkmcnoWDboB4OeaM0-whlsynWt0yZ3wo4KvsnpcYa713pAK1NtXSCTE8Uc0lFtZHoOCqewug4plRDQ=="
+ }
+ },
+ "issues" : {
+ "_type" : {
+ "_name" : "ResultIssueSummaries"
+ }
+ },
+ "logRef" : {
+ "_type" : {
+ "_name" : "Reference"
+ },
+ "id" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "0~Qjtn8Ykt-p4Y2zYr6DbsVuygoZGAblK7Eph8ZFqa-w0iUTl09NihLHxd7DNfeeR1B0dwrjrN5Fvx_yZPpHvUjQ=="
+ },
+ "targetType" : {
+ "_type" : {
+ "_name" : "TypeDefinition"
+ },
+ "name" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "ActivityLogSection"
+ }
+ }
+ },
+ "metrics" : {
+ "_type" : {
+ "_name" : "ResultMetrics"
+ },
+ "testsCount" : {
+ "_type" : {
+ "_name" : "Int"
+ },
+ "_value" : "1"
+ }
+ },
+ "resultName" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "action"
+ },
+ "status" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "succeeded"
+ },
+ "testsRef" : {
+ "_type" : {
+ "_name" : "Reference"
+ },
+ "id" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "0~CRExaYFNITauPqprkYOVFcX44BiMR7Y5SK7vYQTvwKNnaF1--St6QQlAOz693pVVLLQkXitHdwytuCOA_J3AmA=="
+ },
+ "targetType" : {
+ "_type" : {
+ "_name" : "TypeDefinition"
+ },
+ "name" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "ActionTestPlanRunSummaries"
+ }
+ }
+ }
+ },
+ "buildResult" : {
+ "_type" : {
+ "_name" : "ActionResult"
+ },
+ "coverage" : {
+ "_type" : {
+ "_name" : "CodeCoverageInfo"
+ }
+ },
+ "issues" : {
+ "_type" : {
+ "_name" : "ResultIssueSummaries"
+ }
+ },
+ "logRef" : {
+ "_type" : {
+ "_name" : "Reference"
+ },
+ "id" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "0~o__Z27AtErlXE6_lRzQF5cFdk50BUQF_aPv-O8Qsdvi5HMz3kXluFX3_xOAAfqzm0-YWnbEyrw1wXekD7fMHEw=="
+ },
+ "targetType" : {
+ "_type" : {
+ "_name" : "TypeDefinition"
+ },
+ "name" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "ActivityLogSection"
+ }
+ }
+ },
+ "metrics" : {
+ "_type" : {
+ "_name" : "ResultMetrics"
+ }
+ },
+ "resultName" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "build"
+ },
+ "status" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "succeeded"
+ }
+ },
+ "endedTime" : {
+ "_type" : {
+ "_name" : "Date"
+ },
+ "_value" : "2022-09-20T15:39:30.709-0700"
+ },
+ "runDestination" : {
+ "_type" : {
+ "_name" : "ActionRunDestinationRecord"
+ },
+ "displayName" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "iPhone 13"
+ },
+ "localComputerRecord" : {
+ "_type" : {
+ "_name" : "ActionDeviceRecord"
+ },
+ "busSpeedInMHz" : {
+ "_type" : {
+ "_name" : "Int"
+ },
+ "_value" : "0"
+ },
+ "cpuCount" : {
+ "_type" : {
+ "_name" : "Int"
+ },
+ "_value" : "1"
+ },
+ "cpuKind" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "Apple M1 Pro"
+ },
+ "cpuSpeedInMHz" : {
+ "_type" : {
+ "_name" : "Int"
+ },
+ "_value" : "0"
+ },
+ "identifier" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "00006000-000239D03CE2801E"
+ },
+ "isConcreteDevice" : {
+ "_type" : {
+ "_name" : "Bool"
+ },
+ "_value" : "true"
+ },
+ "logicalCPUCoresPerPackage" : {
+ "_type" : {
+ "_name" : "Int"
+ },
+ "_value" : "10"
+ },
+ "modelCode" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "MacBookPro18,1"
+ },
+ "modelName" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "MacBook Pro"
+ },
+ "modelUTI" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "com.apple.macbookpro-16-2021"
+ },
+ "name" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "My Mac"
+ },
+ "nativeArchitecture" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "arm64e"
+ },
+ "operatingSystemVersion" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "12.6"
+ },
+ "operatingSystemVersionWithBuildNumber" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "12.6 (21G115)"
+ },
+ "physicalCPUCoresPerPackage" : {
+ "_type" : {
+ "_name" : "Int"
+ },
+ "_value" : "10"
+ },
+ "platformRecord" : {
+ "_type" : {
+ "_name" : "ActionPlatformRecord"
+ },
+ "identifier" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "com.apple.platform.macosx"
+ },
+ "userDescription" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "macOS"
+ }
+ },
+ "ramSizeInMegabytes" : {
+ "_type" : {
+ "_name" : "Int"
+ },
+ "_value" : "16384"
+ }
+ },
+ "targetArchitecture" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "arm64"
+ },
+ "targetDeviceRecord" : {
+ "_type" : {
+ "_name" : "ActionDeviceRecord"
+ },
+ "busSpeedInMHz" : {
+ "_type" : {
+ "_name" : "Int"
+ },
+ "_value" : "0"
+ },
+ "cpuCount" : {
+ "_type" : {
+ "_name" : "Int"
+ },
+ "_value" : "0"
+ },
+ "cpuSpeedInMHz" : {
+ "_type" : {
+ "_name" : "Int"
+ },
+ "_value" : "0"
+ },
+ "identifier" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "7F61C467-4E4A-437C-B6EF-026FEEF3904C"
+ },
+ "isConcreteDevice" : {
+ "_type" : {
+ "_name" : "Bool"
+ },
+ "_value" : "true"
+ },
+ "logicalCPUCoresPerPackage" : {
+ "_type" : {
+ "_name" : "Int"
+ },
+ "_value" : "0"
+ },
+ "modelCode" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "iPhone14,5"
+ },
+ "modelName" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "iPhone 13"
+ },
+ "modelUTI" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "com.apple.iphone-13-1"
+ },
+ "name" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "iPhone 13"
+ },
+ "nativeArchitecture" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "arm64"
+ },
+ "operatingSystemVersion" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "15.2"
+ },
+ "operatingSystemVersionWithBuildNumber" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "15.2 (19C51)"
+ },
+ "physicalCPUCoresPerPackage" : {
+ "_type" : {
+ "_name" : "Int"
+ },
+ "_value" : "0"
+ },
+ "platformRecord" : {
+ "_type" : {
+ "_name" : "ActionPlatformRecord"
+ },
+ "identifier" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "com.apple.platform.iphonesimulator"
+ },
+ "userDescription" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "iOS Simulator"
+ }
+ },
+ "ramSizeInMegabytes" : {
+ "_type" : {
+ "_name" : "Int"
+ },
+ "_value" : "0"
+ }
+ },
+ "targetSDKRecord" : {
+ "_type" : {
+ "_name" : "ActionSDKRecord"
+ },
+ "identifier" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "iphonesimulator15.2"
+ },
+ "name" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "Simulator - iOS 15.2"
+ },
+ "operatingSystemVersion" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "15.2"
+ }
+ }
+ },
+ "schemeCommandName" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "Test"
+ },
+ "schemeTaskName" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "BuildAndAction"
+ },
+ "startedTime" : {
+ "_type" : {
+ "_name" : "Date"
+ },
+ "_value" : "2022-09-20T15:38:15.930-0700"
+ },
+ "title" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "Testing project benchmark-darwin-sample-xcode with scheme testapp-ios"
+ }
+ }
+ ]
+ },
+ "issues" : {
+ "_type" : {
+ "_name" : "ResultIssueSummaries"
+ }
+ },
+ "metadataRef" : {
+ "_type" : {
+ "_name" : "Reference"
+ },
+ "id" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "0~9QqgSKuPd_WJ00r0MtQ53EH7hHP4lpwwOlDne6b7j-OjJ6jPH5Vm31modXkV3kE_g1-SD2HbuvM_J1hZSGH-mw=="
+ },
+ "targetType" : {
+ "_type" : {
+ "_name" : "TypeDefinition"
+ },
+ "name" : {
+ "_type" : {
+ "_name" : "String"
+ },
+ "_value" : "ActionsInvocationMetadata"
+ }
+ }
+ },
+ "metrics" : {
+ "_type" : {
+ "_name" : "ResultMetrics"
+ },
+ "testsCount" : {
+ "_type" : {
+ "_name" : "Int"
+ },
+ "_value" : "1"
+ }
+ }
+}
diff --git a/benchmark/benchmark-darwin-samples-xcode/xcodegen-project.yml b/benchmark/benchmark-darwin-samples-xcode/xcodegen-project.yml
index bece70f..b0e054a 100644
--- a/benchmark/benchmark-darwin-samples-xcode/xcodegen-project.yml
+++ b/benchmark/benchmark-darwin-samples-xcode/xcodegen-project.yml
@@ -1,6 +1,6 @@
# XCodeGen for the :benchmark:benchmark-darwin-samples module..
-name: benchmark-darwin-sample-xcode
+name: benchmark-darwin-samples-xcode
targets:
testapp-ios:
@@ -13,19 +13,9 @@
scheme:
testTargets:
- testapp-ios-benchmarks
- preActions:
- - name: build AndroidXDarwinSampleBenchmarks.xcframework
- basedOnDependencyAnalysis: false
- settingsTarget: testapp-ios
- script: |
- cd ${PROJECT_DIR}/../..
- ANDROIDX_PROJECTS=KMP ./gradlew :benchmark:benchmark-darwin-samples:assembleAndroidXDarwinSampleBenchmarksReleaseXCFramework \
- --no-configuration-cache < /dev/null
- outputFiles:
- - "${PROJECT_DIR}/../../../../out/androidx/benchmark/benchmark-darwin-samples/build/XCFrameworks/release/AndroidXDarwinSampleBenchmarks.xcframework"
gatherCoverageData: false
dependencies:
- - framework: "${PROJECT_DIR}/../../../../out/androidx/benchmark/benchmark-darwin-samples/build/XCFrameworks/release/AndroidXDarwinSampleBenchmarks.xcframework"
+ - framework: "${PROJECT_DIR}/../../../../androidx/benchmark/benchmark-darwin-samples/build/XCFrameworks/release/AndroidXDarwinSampleBenchmarks.xcframework"
settings:
PRODUCT_NAME: testapp-ios
diff --git a/benchmark/benchmark-darwin-samples/build.gradle b/benchmark/benchmark-darwin-samples/build.gradle
index f31dd41..80282e3 100644
--- a/benchmark/benchmark-darwin-samples/build.gradle
+++ b/benchmark/benchmark-darwin-samples/build.gradle
@@ -3,6 +3,7 @@
plugins {
id("AndroidXPlugin")
+ id("androidx.benchmark.darwin")
}
androidXMultiplatform {
@@ -46,6 +47,17 @@
}
}
+darwinBenchmark {
+ xcodeGenConfigFile = project.rootProject.file(
+ "benchmark/benchmark-darwin-samples-xcode/xcodegen-project.yml"
+ )
+ xcodeProjectName = "benchmark-darwin-samples-xcode"
+ scheme = "testapp-ios"
+ // ios 13, 15.2
+ destination = "id=7F61C467-4E4A-437C-B6EF-026FEEF3904C"
+ xcFrameworkConfig = "AndroidXDarwinSampleBenchmarks"
+}
+
androidx {
name = "AndroidX Benchmarks - Darwin Samples"
mavenGroup = LibraryGroups.BENCHMARK
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index fe156f1..5db44a4 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -28,6 +28,7 @@
import androidx.benchmark.DeviceInfo
import androidx.benchmark.InstrumentationResults
import androidx.benchmark.ResultWriter
+import androidx.benchmark.Shell
import androidx.benchmark.UserspaceTracing
import androidx.benchmark.checkAndGetSuppressionState
import androidx.benchmark.conditionalError
@@ -65,6 +66,7 @@
false
}
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
val errors = DeviceInfo.errors +
// TODO: Merge this debuggable check / definition with Errors.kt in benchmark-common
listOfNotNull(
@@ -97,6 +99,38 @@
<!--suppress AndroidElementNotAllowed -->
<profileable android:shell="true"/>
""".trimIndent()
+ ),
+ conditionalError(
+ hasError = instrumentation.targetContext.packageName !=
+ instrumentation.context.packageName,
+ id = "NOT-SELF-INSTRUMENTING",
+ summary = "Benchmark manifest is instrumenting separate process",
+ message = """
+ Macrobenchmark instrumentation target in manifest
+ ${instrumentation.targetContext.packageName} does not match macrobenchmark
+ package ${instrumentation.context.packageName}. While macrobenchmarks 'target' a
+ separate app they measure, they can not declare it as their instrumentation
+ targetPackage in their manifest. Doing so would cause the macrobenchmark test
+ app to be loaded into the target application process, which would prevent
+ macrobenchmark from killing, compiling, or launching the target process.
+
+ Ensure your macrobenchmark test apk's manifest matches the manifest package, and
+ instrumentation target package, also called 'self-instrumenting':
+
+ <manifest
+ package="com.mymacrobenchpackage" ...>
+ <instrumentation
+ android:name="androidx.benchmark.junit4.AndroidBenchmarkRunner"
+ android:targetPackage="mymacrobenchpackage"/>
+
+ In gradle library modules, this is the default behavior. In gradle test modules,
+ specify the experimental self-instrumenting property:
+ android {
+ targetProjectPath = ":app"
+ // Enable the benchmark to run separately from the app process
+ experimentalProperties["android.experimental.self-instrumenting"] = true
+ }
+ """.trimIndent()
)
).sortedBy { it.id }
@@ -129,12 +163,12 @@
"Empty list of metrics passed to metrics param, must pass at least one Metric"
}
- // skip benchmark if not supported by vm settings
- compilationMode.assumeSupportedWithVmSettings()
-
val suppressionState = checkErrors(packageName)
var warningMessage = suppressionState?.warningMessage ?: ""
+ // skip benchmark if not supported by vm settings
+ compilationMode.assumeSupportedWithVmSettings()
+
val startTime = System.nanoTime()
val scope = MacrobenchmarkScope(packageName, launchWithClearTask)
@@ -340,31 +374,37 @@
userspaceTracingPackage = userspaceTracingPackage,
setupBlock = {
if (startupMode == StartupMode.COLD) {
+ // Run setup before killing process
+ setupBlock(this)
+
+ // Kill - code below must not wake process!
killProcess()
+
// Shader caches are stored in the code cache directory. Make sure that
// they are cleared every iteration.
dropShaderCache()
- // drop app pages from page cache to ensure it is loaded from disk, from scratch
- // resetAndCompile uses ProfileInstallReceiver to write a skip file.
- // This is done to reduce the interference from ProfileInstaller,
- // so long-running benchmarks don't get optimized due to a background dexopt.
-
- // To restore the state of the process we need to drop app pages so its
- // loaded from disk, from scratch.
+ // Ensure app's pages are not cached in memory for a true _cold_ start.
dropKernelPageCache()
- } else if (iteration == 0 && startupMode != null) {
- try {
- iteration = null // override to null for warmup, before starting measurements
- // warmup process by running the measure block once unmeasured
- setupBlock(this)
- measureBlock()
- } finally {
- iteration = 0
+ // validate process is not running just before returning
+ check(!Shell.isPackageAlive(packageName)) {
+ "Package $packageName must not be running prior to cold start!"
}
+ } else {
+ if (iteration == 0 && startupMode != null) {
+ try {
+ iteration = null // override to null for warmup
+
+ // warmup process by running the measure block once unmeasured
+ setupBlock(this)
+ measureBlock()
+ } finally {
+ iteration = 0 // resume counting
+ }
+ }
+ setupBlock(this)
}
- setupBlock(this)
},
// Don't reuse activities by default in COLD / WARM
launchWithClearTask = startupMode == StartupMode.COLD || startupMode == StartupMode.WARM,
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
index 9524df3..1accf72 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
@@ -36,7 +36,6 @@
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.plugins.ExtraPropertiesExtension
import org.gradle.api.tasks.ClasspathNormalizer
-import org.gradle.api.tasks.bundling.Zip
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.findByType
@@ -54,8 +53,8 @@
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination"
const val composeReportsOption =
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination"
-const val zipComposeReportsTaskName = "zipComposeCompilerReports"
-const val zipComposeMetricsTaskName = "zipComposeCompilerMetrics"
+const val enableMetricsArg = "androidx.enableComposeCompilerMetrics"
+const val enableReportsArg = "androidx.enableComposeCompilerReports"
/**
* Plugin to apply common configuration for Compose projects.
@@ -359,63 +358,46 @@
}
}.files
- val enableMetrics = project.enableComposeCompilerMetrics()
- val enableReports = project.enableComposeCompilerReports()
+ val enableMetricsProvider = project.providers.gradleProperty(enableMetricsArg)
+ val enableReportsProvider = project.providers.gradleProperty(enableReportsArg)
- val compileTasks = project.tasks.withType(KotlinCompile::class.java)
-
- compileTasks.configureEach { compile ->
+ val libraryMetricsDirectory = project.rootProject.getLibraryMetricsDirectory()
+ val libraryReportsDirectory = project.rootProject.getLibraryReportsDirectory()
+ project.tasks.withType(KotlinCompile::class.java).configureEach { compile ->
// Append inputs to KotlinCompile so tasks get invalidated if any of these values change
compile.inputs.files({ kotlinPlugin })
.withPropertyName("composeCompilerExtension")
.withNormalizer(ClasspathNormalizer::class.java)
- compile.inputs.property("composeMetricsEnabled", enableMetrics)
- compile.inputs.property("composeReportsEnabled", enableReports)
+ compile.inputs.property("composeMetricsEnabled", enableMetricsProvider).optional(true)
+ compile.inputs.property("composeReportsEnabled", enableReportsProvider).optional(true)
// Gradle hack ahead, we use of absolute paths, but is OK here because we do it in
// doFirst which happens after Gradle task input snapshotting. AGP does the same.
compile.doFirst {
compile.kotlinOptions.freeCompilerArgs += "-Xplugin=${kotlinPlugin.first()}"
+ if (enableMetricsProvider.orNull == "true") {
+ val metricsDest = File(libraryMetricsDirectory, "compose")
+ compile.kotlinOptions.freeCompilerArgs +=
+ listOf(
+ "-P",
+ "$composeMetricsOption=${metricsDest.absolutePath}"
+ )
+ }
+ if ((enableReportsProvider.orNull == "true")) {
+ val reportsDest = File(libraryReportsDirectory, "compose")
+ compile.kotlinOptions.freeCompilerArgs +=
+ listOf(
+ "-P",
+ "$composeReportsOption=${reportsDest.absolutePath}"
+ )
+ }
if (shouldPublish) {
compile.kotlinOptions.freeCompilerArgs +=
listOf("-P", composeSourceOption)
}
}
}
-
- if (enableMetrics) {
- project.rootProject.tasks.named(zipComposeMetricsTaskName).configure({ zipTask ->
- zipTask.dependsOn(compileTasks)
- })
-
- val metricsIntermediateDir = project.compilerMetricsIntermediatesDir()
- compileTasks.configureEach { compile ->
- compile.doFirst {
- compile.kotlinOptions.freeCompilerArgs +=
- listOf(
- "-P",
- "$composeMetricsOption=$metricsIntermediateDir"
- )
- }
- }
- }
- if (enableReports) {
- project.rootProject.tasks.named(zipComposeReportsTaskName).configure({ zipTask ->
- zipTask.dependsOn(compileTasks)
- })
-
- val reportsIntermediateDir = project.compilerReportsIntermediatesDir()
- compileTasks.configureEach { compile ->
- compile.doFirst {
- compile.kotlinOptions.freeCompilerArgs +=
- listOf(
- "-P",
- "$composeReportsOption=$reportsIntermediateDir"
- )
- }
- }
- }
}
}
@@ -524,39 +506,3 @@
return if (KotlinVersion.CURRENT.isAtLeast(1, 4)) ArrayDeque(initialSize)
else ArrayList(initialSize)
}
-
-public fun Project.zipComposeCompilerMetrics() {
- if (project.enableComposeCompilerMetrics()) {
- val reportsIntermediateDir = project.compilerMetricsIntermediatesDir()
- val libraryMetricsDirectory = project.rootProject.getLibraryMetricsDirectory()
- val zipComposeMetrics = project.tasks.register(zipComposeMetricsTaskName, Zip::class.java) {
- zipTask ->
- zipTask.from(reportsIntermediateDir)
- zipTask.destinationDirectory.set(File(libraryMetricsDirectory, "compose"))
- zipTask.archiveBaseName.set("composemetrics")
- }
- project.addToBuildOnServer(zipComposeMetrics)
- }
-}
-
-public fun Project.zipComposeCompilerReports() {
- if (project.enableComposeCompilerReports()) {
- val metricsIntermediateDir = project.compilerReportsIntermediatesDir()
- val libraryReportsDirectory = project.rootProject.getLibraryReportsDirectory()
- val zipComposeReports = project.tasks.register(zipComposeReportsTaskName, Zip::class.java) {
- zipTask ->
- zipTask.from(metricsIntermediateDir)
- zipTask.destinationDirectory.set(File(libraryReportsDirectory, "compose"))
- zipTask.archiveBaseName.set("composereports")
- }
- project.addToBuildOnServer(zipComposeReports)
- }
-}
-
-fun Project.compilerMetricsIntermediatesDir(): File {
- return project.rootProject.file("${project.rootProject.buildDir}/libraryreports/composemetrics")
-}
-
-fun Project.compilerReportsIntermediatesDir(): File {
- return project.rootProject.file("${project.rootProject.buildDir}/libraryreports/composereports")
-}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
index 457e325..4e1d108 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
@@ -243,16 +243,4 @@
fun Project.allowMissingLintProject() =
findBooleanProperty(ALLOW_MISSING_LINT_CHECKS_PROJECT) ?: false
-/**
- * Returns whether we export compose compiler metrics
- */
-fun Project.enableComposeCompilerMetrics() =
- findBooleanProperty(ENABLE_COMPOSE_COMPILER_METRICS) ?: false
-
-/**
- * Returns whether we export compose compiler reports
- */
-fun Project.enableComposeCompilerReports() =
- findBooleanProperty(ENABLE_COMPOSE_COMPILER_REPORTS) ?: false
-
-fun Project.findBooleanProperty(propName: String) = (findProperty(propName) as? String)?.toBoolean()
+fun Project.findBooleanProperty(propName: String) = (findProperty(propName) as? String)?.toBoolean()
\ No newline at end of file
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
index 6d02d10..672f99b 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -230,9 +230,6 @@
task.setOutput(File(project.getDistributionDirectory(), "task_outputs.txt"))
task.removePrefix(project.getCheckoutRoot().path)
}
-
- project.zipComposeCompilerMetrics()
- project.zipComposeCompilerReports()
}
// If our output message validation configuration changes, invalidate all tasks to make sure
diff --git a/buildSrc/public/build.gradle b/buildSrc/public/build.gradle
index f98bc4c..e37e156 100644
--- a/buildSrc/public/build.gradle
+++ b/buildSrc/public/build.gradle
@@ -4,6 +4,9 @@
main.java.srcDirs += "${supportRootFolder}/benchmark/gradle-plugin/src/main/kotlin"
main.resources.srcDirs += "${supportRootFolder}/benchmark/gradle-plugin/src/main/resources"
+ main.java.srcDirs += "${supportRootFolder}/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin"
+ main.resources.srcDirs += "${supportRootFolder}/benchmark/benchmark-darwin-gradle-plugin/src/main/resources"
+
main.java.srcDirs += "${supportRootFolder}/inspection/inspection-gradle-plugin/src/main/kotlin"
main.resources.srcDirs += "${supportRootFolder}/inspection/inspection-gradle-plugin/src/main" +
"/resources"
@@ -15,6 +18,11 @@
"src/main/kotlin"
}
+dependencies {
+ // This is for androidx.benchmark.darwin
+ implementation(libs.apacheCommonsMath)
+}
+
gradlePlugin {
plugins {
benchmark {
@@ -25,5 +33,9 @@
id = "androidx.inspection"
implementationClass = "androidx.inspection.gradle.InspectionPlugin"
}
+ darwinBenchmark {
+ id = "androidx.benchmark.darwin"
+ implementationClass = "androidx.benchmark.darwin.gradle.DarwinBenchmarkPlugin"
+ }
}
}
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 0d4049b..fd71cc8 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
@@ -365,6 +365,9 @@
@SuppressWarnings("WeakerAccess")
final Executor mSequentialIoExecutor;
+ @Nullable
+ private CameraEffect mCameraEffect;
+
/**
* Creates a new image capture use case from the given configuration.
*
@@ -1937,8 +1940,7 @@
Log.d(TAG, String.format("createPipelineWithNode(cameraId: %s, resolution: %s)",
cameraId, resolution));
checkState(mImagePipeline == null);
- // TODO: set CameraEffect
- mImagePipeline = new ImagePipeline(config, resolution, null);
+ mImagePipeline = new ImagePipeline(config, resolution, mCameraEffect);
checkState(mTakePictureManager == null);
mTakePictureManager = new TakePictureManager(mImageCaptureControl, mImagePipeline);
@@ -2059,6 +2061,27 @@
return mImagePipeline != null && mTakePictureManager != null;
}
+ /**
+ * @hide
+ */
+ @MainThread
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public void setEffect(@Nullable CameraEffect cameraEffect) {
+ checkMainThread();
+ mCameraEffect = cameraEffect;
+ }
+
+ /**
+ * @hide
+ */
+ @MainThread
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public CameraEffect getEffect() {
+ checkMainThread();
+ return mCameraEffect;
+ }
+
// ===== New architecture end =====
/**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
index c0f7ec02..fc8f5d3 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
@@ -459,6 +459,9 @@
preview.setProcessor(new SurfaceProcessorWithExecutor(
requireNonNull(effect.getSurfaceProcessor()),
effect.getExecutor()));
+ } else if (useCase instanceof ImageCapture) {
+ ImageCapture imageCapture = ((ImageCapture) useCase);
+ imageCapture.setEffect(effectsByTargets.get(CameraEffect.IMAGE_CAPTURE));
}
}
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
index 48dab10..82353c9 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
@@ -43,6 +43,7 @@
import androidx.camera.testing.fakes.FakeUseCase
import androidx.camera.testing.fakes.FakeUseCaseConfig
import androidx.camera.testing.fakes.FakeUseCaseConfigFactory
+import androidx.camera.testing.fakes.GrayscaleImageEffect
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -77,6 +78,7 @@
private lateinit var fakeCamera: FakeCamera
private lateinit var useCaseConfigFactory: UseCaseConfigFactory
private val fakeCameraSet = LinkedHashSet<CameraInternal>()
+ private val imageEffect = GrayscaleImageEffect()
@Before
fun setUp() {
@@ -86,7 +88,7 @@
fakeCameraSet.add(fakeCamera)
surfaceProcessor = FakeSurfaceProcessor(mainThreadExecutor())
executor = Executors.newSingleThreadExecutor()
- effects = listOf(FakePreviewEffect(executor, surfaceProcessor))
+ effects = listOf(FakePreviewEffect(executor, surfaceProcessor), imageEffect)
}
@After
@@ -619,17 +621,24 @@
@Test
fun updateEffects_effectsAddedAndRemoved() {
// Arrange.
- val preview = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val preview = Preview.Builder().build()
+ val imageCapture = ImageCapture.Builder().build()
+ val useCases = listOf(preview, imageCapture)
+
// Act: update use cases with effects.
- CameraUseCaseAdapter.updateEffects(effects, listOf(preview))
+ CameraUseCaseAdapter.updateEffects(effects, useCases)
// Assert: preview has processor wrapped with the right executor.
val previewProcessor = preview.processor as SurfaceProcessorWithExecutor
assertThat(previewProcessor.processor).isEqualTo(surfaceProcessor)
assertThat(previewProcessor.executor).isEqualTo(executor)
+ // Assert: imageCapture has the effect set.
+ assertThat(imageCapture.effect).isEqualTo(imageEffect)
+
// Act: update again with no effects.
- CameraUseCaseAdapter.updateEffects(listOf(), listOf(preview))
- // Assert: preview no longer has processors.
+ CameraUseCaseAdapter.updateEffects(listOf(), useCases)
+ // Assert: use cases no longer has effects.
assertThat(preview.processor).isNull()
+ assertThat(imageCapture.effect).isNull()
}
private fun createCoexistingRequiredRuleCameraConfig(): CameraConfig {
diff --git a/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml b/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
index 1c8e0e8..09b1c7e 100644
--- a/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2019 The Android Open Source Project
+<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 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.
@@ -14,35 +13,52 @@
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
- <uses-permission android:name="android.permission.CAMERA" />
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
- <uses-permission android:name="android.permission.RECORD_AUDIO" />
-
- <uses-feature android:name="android.hardware.camera" />
- <uses-feature android:glEsVersion="0x00020000" />
<application
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".CameraXActivity"
+ android:exported="true"
android:label="Camera Core Test App"
- android:exported="true">
+ android:taskAffinity="androidx.camera.integration.core.CameraXActivity" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ <activity
+ android:name=".CameraPipeActivity"
+ android:exported="true"
+ android:label="CameraPipe Core Test App"
+ android:taskAffinity="androidx.camera.integration.core.CameraPipeActivity"
+ android:theme="@android:style/Theme.Dialog">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
- <category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:name=".OpenGLActivity"
+ android:exported="true"
android:label="Camera Core Test App (OpenGL Activity)"
- android:theme="@style/NoTitleTheme"
- android:exported="true">
+ android:taskAffinity="androidx.camera.integration.core.OpenGLActivity"
+ android:theme="@style/NoTitleTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
+ <uses-feature android:glEsVersion="0x00020000" />
+ <uses-feature android:name="android.hardware.camera" />
+
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission
+ android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+ android:maxSdkVersion="32" />
+
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
</manifest>
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraPipeActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraPipeActivity.java
new file mode 100644
index 0000000..36307f0
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraPipeActivity.java
@@ -0,0 +1,44 @@
+/*
+ * 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.integration.core;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * An activity to launch "Camera Core Test App" with Camera Pipe configuration
+ *
+ * <p>Launches CameraXActivity with Camera Pipe configuration.
+ */
+public class CameraPipeActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent intent = new Intent(CameraPipeActivity.this, CameraXActivity.class);
+ Bundle b = new Bundle();
+ b.putString(CameraXActivity.INTENT_EXTRA_CAMERA_IMPLEMENTATION,
+ CameraXViewModel.CAMERA_PIPE_IMPLEMENTATION_OPTION);
+ intent.putExtras(b);
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index b7d26e4..24257c5 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -157,6 +157,7 @@
public class CameraXActivity extends AppCompatActivity {
private static final String TAG = "CameraXActivity";
private static final String[] REQUIRED_PERMISSIONS;
+
static {
// From Android T, skips the permission check of WRITE_EXTERNAL_STORAGE since it won't be
// granted any more.
@@ -174,6 +175,9 @@
}
}
+ //Use this activity title when Camera Pipe configuration is used by core test app
+ private static final String APP_TITLE_FOR_CAMERA_PIPE = "CameraPipe Core Test App";
+
// Possible values for this intent key: "backward" or "forward".
private static final String INTENT_EXTRA_CAMERA_DIRECTION = "camera_direction";
// Possible values for this intent key: "switch_test_case", "preview_test_case" or
@@ -647,7 +651,7 @@
} else if (outputOptions instanceof FileOutputOptions) {
videoFilePath = ((FileOutputOptions) outputOptions).getFile().getPath();
MediaScannerConnection.scanFile(this,
- new String[] { videoFilePath }, null,
+ new String[]{videoFilePath}, null,
(path, uri1) -> {
Log.i(TAG, "Scanned " + path + " -> uri= " + uri1);
updateVideoSavedSessionData(uri1);
@@ -1072,6 +1076,11 @@
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
+ //if different Camera Provider (CameraPipe vs Camera2 was initialized in previous session,
+ //then close this application.
+ closeAppIfCameraProviderMismatch(this.getIntent());
+
setContentView(R.layout.activity_camera_xmain);
mImageCaptureExecutorService = Executors.newSingleThreadExecutor();
OpenGLRenderer previewRenderer = mPreviewRenderer = new OpenGLRenderer();
@@ -1183,7 +1192,12 @@
newIntent.removeExtra(INTENT_EXTRA_CAMERA_IMPLEMENTATION_NO_HISTORY);
setIntent(newIntent);
}
+
if (cameraImplementation != null) {
+ if (cameraImplementation.equalsIgnoreCase(
+ CameraXViewModel.CAMERA_PIPE_IMPLEMENTATION_OPTION)) {
+ setTitle(APP_TITLE_FOR_CAMERA_PIPE);
+ }
CameraXViewModel.configureCameraProvider(
cameraImplementation, cameraImplementationNoHistory);
}
@@ -1215,6 +1229,34 @@
setupPermissions();
}
+ /**
+ * Close current app if CameraProvider from intent of current activity doesn't match with
+ * CameraProvider stored in the CameraXViewModel, because CameraProvider can't be changed
+ * between Camera2 and Camera Pipe while app is running.
+ */
+ private void closeAppIfCameraProviderMismatch(Intent mIntent) {
+ String cameraImplementation = null;
+ Boolean cameraImplementationNoHistory = false;
+ Bundle bundle = mIntent.getExtras();
+ if (bundle != null) {
+ cameraImplementation = bundle.getString(INTENT_EXTRA_CAMERA_IMPLEMENTATION);
+ cameraImplementationNoHistory =
+ bundle.getBoolean(INTENT_EXTRA_CAMERA_IMPLEMENTATION_NO_HISTORY, false);
+ }
+
+ if (!cameraImplementationNoHistory) {
+ if (!CameraXViewModel.isCameraProviderUnInitializedOrSameAsParameter(
+ cameraImplementation)) {
+ Toast.makeText(CameraXActivity.this, "Please relaunch "
+ + "the app to apply new CameraX configuration.",
+ Toast.LENGTH_LONG).show();
+ finish();
+ System.exit(0);
+ }
+ }
+ }
+
+
@Override
public void onDestroy() {
super.onDestroy();
@@ -1410,8 +1452,8 @@
for (String permission : REQUIRED_PERMISSIONS) {
if (!Objects.requireNonNull(result.get(permission))) {
Toast.makeText(getApplicationContext(),
- "Camera permission denied.",
- Toast.LENGTH_SHORT)
+ "Camera permission denied.",
+ Toast.LENGTH_SHORT)
.show();
finish();
return;
@@ -1596,10 +1638,10 @@
cameraInfo.getZoomState().removeObservers(this);
cameraInfo.getZoomState().observe(this,
state -> {
- String str = String.format("%.2fx", state.getZoomRatio());
- mZoomRatioLabel.setText(str);
- mZoomSeekBar.setProgress((int) (MAX_SEEKBAR_VALUE * state.getLinearZoom()));
- });
+ String str = String.format("%.2fx", state.getZoomRatio());
+ mZoomRatioLabel.setText(str);
+ mZoomSeekBar.setProgress((int) (MAX_SEEKBAR_VALUE * state.getLinearZoom()));
+ });
}
private boolean is2XZoomSupported() {
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java
index 60d7ed6..61a1f09 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java
@@ -106,6 +106,41 @@
@OptIn(markerClass = ExperimentalCameraProviderConfiguration.class)
@MainThread
+ public static boolean isCameraProviderUnInitializedOrSameAsParameter(
+ @NonNull String cameraImplementation) {
+
+ if (sConfiguredCameraXCameraImplementation == null) {
+ return true;
+ }
+ String currentCameraProvider = getCameraProviderName(
+ sConfiguredCameraXCameraImplementation);
+ cameraImplementation = getCameraProviderName(cameraImplementation);
+
+ if (currentCameraProvider.equals(cameraImplementation)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * convert null and IMPLICIT_IMPLEMENTATION_OPTION Camera Provider name to
+ * CAMERA2_IMPLEMENTATION_OPTION
+ */
+ @OptIn(markerClass = ExperimentalCameraProviderConfiguration.class)
+ @MainThread
+ private static String getCameraProviderName(String mCameraProvider) {
+ if (mCameraProvider == null) {
+ mCameraProvider = CAMERA2_IMPLEMENTATION_OPTION;
+ }
+ if (mCameraProvider.equals(IMPLICIT_IMPLEMENTATION_OPTION)) {
+ mCameraProvider = CAMERA2_IMPLEMENTATION_OPTION;
+ }
+ return mCameraProvider;
+ }
+
+ @OptIn(markerClass = ExperimentalCameraProviderConfiguration.class)
+ @MainThread
static void configureCameraProvider(@NonNull String cameraImplementation) {
configureCameraProvider(cameraImplementation, false);
}
diff --git a/collection/settings.gradle b/collection/settings.gradle
index f2f97ca..e70b8e0 100644
--- a/collection/settings.gradle
+++ b/collection/settings.gradle
@@ -28,6 +28,8 @@
setupPlayground("..")
selectProjectsFromAndroidX({ name ->
if (name.startsWith(":collection")) return true
+ if (name == ":benchmark:benchmark-darwin") return true
+ if (name == ":benchmark:benchmark-darwin-core") return true
if (name == ":internal-testutils-truth") return true
return false
})
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt
index afa7499..b763795 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt
@@ -246,7 +246,7 @@
instanceClass ?: error("Could not find class $className in loaded classes")
}
- val instanceOfClass = instanceClass.newInstance()
+ val instanceOfClass = instanceClass.getDeclaredConstructor().newInstance()
val testMethod = instanceClass.getMethod("test", Context::class.java)
val controller = Robolectric.buildActivity(TestActivity::class.java)
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenTest.kt
index 1ca1cb8..9fc894a 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenTest.kt
@@ -143,7 +143,7 @@
val loader = createClassLoader()
if (dumpClasses) dumpClasses(loader)
val loadedClass = loader.loadClass("Test")
- val instance = loadedClass.newInstance()
+ val instance = loadedClass.getDeclaredConstructor().newInstance()
val instanceClass = instance::class.java
val testMethod = instanceClass.getMethod("test")
testMethod.invoke(instance)
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLoweringTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLoweringTests.kt
index 3120221..11a509a 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLoweringTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLoweringTests.kt
@@ -120,7 +120,7 @@
instanceClass ?: error("Could not find class $className in loaded classes")
}
- val instanceOfClass = instanceClass.newInstance()
+ val instanceOfClass = instanceClass.getDeclaredConstructor().newInstance()
val testMethod = instanceClass.getMethod(
"test",
*parameterTypes,
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
index ac4674a..fc62647 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
@@ -16,6 +16,7 @@
package androidx.compose.compiler.plugins.kotlin
+import org.intellij.lang.annotations.Language
import org.junit.Test
class ControlFlowTransformTests : AbstractControlFlowTransformTests() {
@@ -219,6 +220,727 @@
"""
)
+ private fun verifyInlineReturn(
+ @Language("kotlin")
+ source: String,
+ expectedTransformed: String,
+ ) = verifyComposeIrTransform(
+ """
+ import androidx.compose.runtime.Composable
+ $source
+ """,
+ expectedTransformed,
+ """
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun A() { }
+
+ @Composable
+ fun Text(text: String) { }
+
+ @Composable
+ inline fun Wrapper(content: @Composable () -> Unit) = content()
+
+ @Composable
+ inline fun M1(content: @Composable () -> Unit) = Wrapper {
+ content()
+ }
+
+ @Composable
+ inline fun M2(content: @Composable () -> Unit) = Wrapper {
+ Wrapper {
+ content()
+ }
+ }
+
+ @Composable
+ inline fun M3(content: @Composable () -> Unit) = Wrapper {
+ Wrapper {
+ Wrapper {
+ content()
+ }
+ }
+ }
+ """.trimIndent()
+ )
+
+ @Test
+ fun testInline_CM3_RFun() = verifyInlineReturn(
+ """
+ @Composable
+ fun Test(condition: Boolean) {
+ A()
+ M3 {
+ A()
+ if (condition) {
+ return
+ }
+ A()
+ }
+ A()
+ }
+
+ """,
+ """
+ @Composable
+ fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Test)<A()>,<M3>,<A()>:Test.kt")
+ val tmp1_marker = %composer.currentMarker
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ A(%composer, 0)
+ M3({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ A(%composer, 0)
+ if (condition) {
+ %composer.endToMarker(tmp1_marker)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ return
+ }
+ A(%composer, 0)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ A(%composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ """
+ )
+
+ @Test
+ fun testInline_CM3_RFun_CM3_RFun() = verifyInlineReturn(
+ """
+ @Composable
+ fun Test(a: Boolean, b: Boolean) {
+ A()
+ M3 {
+ A()
+ if (a) {
+ return
+ }
+ A()
+ }
+ M3 {
+ A()
+ if (b) {
+ return
+ }
+ A()
+ }
+ A()
+ }
+
+ """,
+ """
+ @Composable
+ fun Test(a: Boolean, b: Boolean, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Test)<A()>,<M3>,<M3>,<A()>:Test.kt")
+ val tmp1_marker = %composer.currentMarker
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(a)) 0b0100 else 0b0010
+ }
+ if (%changed and 0b01110000 === 0) {
+ %dirty = %dirty or if (%composer.changed(b)) 0b00100000 else 0b00010000
+ }
+ if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ A(%composer, 0)
+ M3({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ A(%composer, 0)
+ if (a) {
+ %composer.endToMarker(tmp1_marker)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(a, b, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ return
+ }
+ A(%composer, 0)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ M3({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ A(%composer, 0)
+ if (b) {
+ %composer.endToMarker(tmp1_marker)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(a, b, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ return
+ }
+ A(%composer, 0)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ A(%composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(a, b, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ """
+ )
+
+ @Test
+ fun testInline_CM3_RM3() = verifyInlineReturn(
+ """
+ @Composable
+ fun Test(condition: Boolean) {
+ A()
+ M3 {
+ A()
+ if (condition) {
+ return@M3
+ }
+ A()
+ }
+ A()
+ }
+
+ """,
+ """
+ @Composable
+ fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Test)<A()>,<M3>,<A()>:Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ A(%composer, 0)
+ val tmp0_marker = %composer.currentMarker
+ M3({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ A(%composer, 0)
+ if (condition) {
+ %composer.endToMarker(tmp0_marker)
+ }
+ A(%composer, 0)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ A(%composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ """
+ )
+
+ @Test
+ fun testInline_Lambda() = verifyComposeIrTransform(
+ """
+ fun Test(condition: Boolean) {
+ T {
+ compose {
+ M1 {
+ if (condition) return@compose
+ }
+ }
+ }
+ }
+ """,
+ """
+ fun Test(condition: Boolean) {
+ T {
+ %this%T.compose(composableLambdaInstance(<>, true) { %composer: Composer?, %changed: Int ->
+ sourceInformation(%composer, "C<M1>:Test.kt")
+ val tmp0_marker = %composer.currentMarker
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ M1({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (condition) {
+ %composer.endToMarker(tmp0_marker)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ }
+ )
+ }
+ }
+ """,
+ """
+ import androidx.compose.runtime.*
+
+ class Scope {
+ fun compose(block: @Composable () -> Unit) { }
+ }
+
+ fun T(block: suspend Scope.() -> Unit) { }
+
+ @Composable
+ inline fun M1(block: @Composable () -> Unit) { block() }
+
+ @Composable
+ fun Text(text: String) { }
+ """
+ )
+
+ @Test
+ fun testInline_M3_M1_Return_M1() = verifyInlineReturn(
+ """
+ @Composable
+ fun Test_M3_M1_Return_M1(condition: Boolean) {
+ A()
+ M3 {
+ A()
+ M1 {
+ if (condition) {
+ return@M1
+ }
+ }
+ A()
+ }
+ A()
+ }
+ """,
+ """
+ @Composable
+ fun Test_M3_M1_Return_M1(condition: Boolean, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Test_M3_M1_Return_M1)<A()>,<M3>,<A()>:Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ A(%composer, 0)
+ M3({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C<A()>,<M1>,<A()>:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ A(%composer, 0)
+ val tmp0_marker = %composer.currentMarker
+ M1({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (condition) {
+ %composer.endToMarker(tmp0_marker)
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ A(%composer, 0)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ A(%composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test_M3_M1_Return_M1(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ """
+ )
+
+ @Test
+ fun testInline_M3_M1_Return_M3() = verifyInlineReturn(
+ """
+ @Composable
+ fun Test_M3_M1_Return_M3(condition: Boolean) {
+ A()
+ M3 {
+ A()
+ M1 {
+ if (condition) {
+ return@M3
+ }
+ }
+ A()
+ }
+ A()
+ }
+ """,
+ """
+ @Composable
+ fun Test_M3_M1_Return_M3(condition: Boolean, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Test_M3_M1_Return_M3)<A()>,<M3>,<A()>:Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ A(%composer, 0)
+ val tmp0_marker = %composer.currentMarker
+ M3({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C<A()>,<M1>,<A()>:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ A(%composer, 0)
+ M1({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (condition) {
+ %composer.endToMarker(tmp0_marker)
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ A(%composer, 0)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ A(%composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test_M3_M1_Return_M3(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ """
+ )
+
+ @Test
+ fun testInline_M1_W_Return_Func() = verifyInlineReturn(
+ """
+ @Composable
+ fun testInline_M1_W_Return_Func(condition: Boolean) {
+ A()
+ M1 {
+ A()
+ while(true) {
+ A()
+ if (condition) {
+ return
+ }
+ A()
+ }
+ A()
+ }
+ A()
+ }
+
+ """,
+ """
+ @Composable
+ fun testInline_M1_W_Return_Func(condition: Boolean, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(testInline_M1_W_Return_Func)<A()>,<M1>,<A()>:Test.kt")
+ val tmp1_marker = %composer.currentMarker
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ A(%composer, 0)
+ M1({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ A(%composer, 0)
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "*<A()>,<A()>")
+ while (true) {
+ A(%composer, 0)
+ if (condition) {
+ %composer.endToMarker(tmp1_marker)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ testInline_M1_W_Return_Func(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ return
+ }
+ A(%composer, 0)
+ }
+ %composer.endReplaceableGroup()
+ A(%composer, 0)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ A(%composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ testInline_M1_W_Return_Func(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ """
+ )
+
+ @Test
+ fun testInline_CM3_Return_M3_CM3_Return_M3() = verifyInlineReturn(
+ """
+ @Composable
+ fun testInline_M1_W_Return_Func(condition: Boolean) {
+ A()
+ M3 {
+ A()
+ if (condition) {
+ return@M3
+ }
+ A()
+ }
+ M3 {
+ A()
+ if (condition) {
+ return@M3
+ }
+ A()
+ }
+ A()
+ }
+
+ """,
+ """
+ @Composable
+ fun testInline_M1_W_Return_Func(condition: Boolean, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(testInline_M1_W_Return_Func)<A()>,<M3>,<M3>,<A()>:Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ A(%composer, 0)
+ val tmp0_marker = %composer.currentMarker
+ M3({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ A(%composer, 0)
+ if (condition) {
+ %composer.endToMarker(tmp0_marker)
+ }
+ A(%composer, 0)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ val tmp1_marker = %composer.currentMarker
+ M3({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ A(%composer, 0)
+ if (condition) {
+ %composer.endToMarker(tmp1_marker)
+ }
+ A(%composer, 0)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ A(%composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ testInline_M1_W_Return_Func(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ """
+ )
+
+ @Test
+ fun test_CM1_CCM1_RetFun(): Unit = verifyInlineReturn(
+ """
+ @Composable
+ fun test_CM1_CCM1_RetFun(condition: Boolean) {
+ Text("Root - before")
+ M1 {
+ Text("M1 - begin")
+ if (condition) {
+ Text("if - begin")
+ M1 {
+ Text("In CCM1")
+ return@test_CM1_CCM1_RetFun
+ }
+ }
+ Text("M1 - end")
+ }
+ Text("Root - end")
+ }
+
+ """,
+ """
+ @Composable
+ fun test_CM1_CCM1_RetFun(condition: Boolean, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(test_CM1_CCM1_RetFun)<Text("...>,<M1>,<Text("...>:Test.kt")
+ val tmp1_marker = %composer.currentMarker
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ Text("Root - before", %composer, 0b0110)
+ M1({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C<Text("...>,<Text("...>:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ Text("M1 - begin", %composer, 0b0110)
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "<Text("...>,<M1>")
+ if (condition) {
+ Text("if - begin", %composer, 0b0110)
+ M1({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C<Text("...>:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ Text("In CCM1", %composer, 0b0110)
+ %composer.endToMarker(tmp1_marker)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ test_CM1_CCM1_RetFun(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ return
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ }
+ %composer.endReplaceableGroup()
+ Text("M1 - end", %composer, 0b0110)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ Text("Root - end", %composer, 0b0110)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ test_CM1_CCM1_RetFun(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ """
+ )
+
@Test
fun testInlineReturnLabel(): Unit = controlFlow(
"""
@@ -246,11 +968,13 @@
if (isTraceInProgress()) {
traceEventStart(<>, %changed, -1, <>)
}
+ val tmp0_marker = %composer.currentMarker
FakeBox({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C<A()>:Test.kt")
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
if (condition) {
+ %composer.endToMarker(tmp0_marker)
}
A(%composer, 0)
} else {
@@ -2660,6 +3384,7 @@
fun WithReturn(%composer: Composer?, %changed: Int) {
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C(WithReturn)<A()>:Test.kt")
+ val tmp0_marker = %composer.currentMarker
if (isTraceInProgress()) {
traceEventStart(<>, %changed, -1, <>)
}
@@ -2667,7 +3392,7 @@
sourceInformation(%composer, "*<A()>")
run {
A(%composer, 0)
- %composer.endReplaceableGroup()
+ %composer.endToMarker(tmp0_marker)
if (isTraceInProgress()) {
traceEventEnd()
}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt
index aaa16df..07f487d 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt
@@ -1080,7 +1080,7 @@
instanceClass ?: error("Could not find class $mainClassName in loaded classes")
}
- val instanceOfClass = instanceClass.newInstance()
+ val instanceOfClass = instanceClass.getDeclaredConstructor().newInstance()
val advanceMethod = instanceClass.getMethod("advance")
val composeMethod = instanceClass.getMethod(
"compose",
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RunComposableTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RunComposableTests.kt
index 37041d7..5390e8c 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RunComposableTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RunComposableTests.kt
@@ -142,7 +142,7 @@
instanceClass ?: error("Could not find class $className in loaded classes")
}
- val instanceOfClass = instanceClass.newInstance()
+ val instanceOfClass = instanceClass.getDeclaredConstructor().newInstance()
val testMethod = instanceClass.getMethod(
"test",
*emptyArray(),
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt
index 5eded68..4159214 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt
@@ -118,6 +118,7 @@
fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(Test)<A()>,<Wrappe...>,<A()>:Test.kt")
+ val tmp1_marker = %composer.currentMarker
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
@@ -133,6 +134,7 @@
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
if (!condition) {
+ %composer.endToMarker(tmp1_marker)
if (isTraceInProgress()) {
traceEventEnd()
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/KtxNameConventions.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/KtxNameConventions.kt
index 35cbb05..bd3e9b5 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/KtxNameConventions.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/KtxNameConventions.kt
@@ -20,4 +20,6 @@
val TRACE_EVENT_END = "traceEventEnd"
val SOURCEINFORMATIONMARKEREND = "sourceInformationMarkerEnd"
val UPDATE_CHANGED_FLAGS = "updateChangedFlags"
+ val CURRENTMARKER = Name.identifier("currentMarker")
+ val ENDTOMARKER = Name.identifier("endToMarker")
}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index 4067649..96d4611 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -601,6 +601,24 @@
}
}
+ private val currentMarkerProperty: IrProperty? by guardedLazy {
+ composerIrClass.properties
+ .firstOrNull {
+ it.name == KtxNameConventions.CURRENTMARKER
+ }
+ }
+
+ private val endToMarkerFunction: IrSimpleFunction? by guardedLazy {
+ composerIrClass
+ .functions
+ .firstOrNull {
+ it.name == KtxNameConventions.ENDTOMARKER && it.valueParameters.size == 1
+ }
+ }
+
+ private val rollbackGroupMarkerEnabled get() =
+ currentMarkerProperty != null && endToMarkerFunction != null
+
private val endRestartGroupFunction by guardedLazy {
composerIrClass
.functions
@@ -926,6 +944,7 @@
)
else -> null
},
+ *scope.markerPreamble.statements.toTypedArray(),
*bodyPreamble.statements.toTypedArray(),
*transformed.statements.toTypedArray(),
when {
@@ -1008,7 +1027,7 @@
scope.dirty = dirty
- val (nonReturningBody, returnVar) = body.asBodyAndResultVar()
+ val (nonReturningBody, returnVar) = body.asBodyAndResultVar(declaration)
val emitTraceMarkers = traceEventMarkersEnabled && !scope.isInlinedLambda
@@ -1076,6 +1095,7 @@
irStartReplaceableGroup(body, scope, irFunctionSourceKey())
else null,
*sourceInformationPreamble.statements.toTypedArray(),
+ *scope.markerPreamble.statements.toTypedArray(),
*skipPreamble.statements.toTypedArray(),
*bodyPreamble.statements.toTypedArray(),
transformedBody,
@@ -1091,6 +1111,7 @@
body.endOffset,
listOfNotNull(
*sourceInformationPreamble.statements.toTypedArray(),
+ *scope.markerPreamble.statements.toTypedArray(),
*skipPreamble.statements.toTypedArray(),
*bodyPreamble.statements.toTypedArray(),
transformed,
@@ -1265,6 +1286,7 @@
scope,
irFunctionSourceKey()
),
+ *scope.markerPreamble.statements.toTypedArray(),
*skipPreamble.statements.toTypedArray(),
transformedBody,
if (returnVar == null) end() else null,
@@ -1827,6 +1849,9 @@
)
}
+ fun irCurrentMarker() =
+ irMethodCall(irCurrentComposer(), currentMarkerProperty!!.getter!!)
+
private fun irIsSkipping() =
irMethodCall(irCurrentComposer(), isSkippingFunction.getter!!)
private fun irDefaultsInvalid() =
@@ -1869,7 +1894,9 @@
}
}
- private fun IrBody.asBodyAndResultVar(): Pair<IrContainerExpression, IrVariable?> {
+ private fun IrBody.asBodyAndResultVar(
+ expectedTarget: IrFunction? = null
+ ): Pair<IrContainerExpression, IrVariable?> {
val original = IrCompositeImpl(
startOffset,
endOffset,
@@ -1880,7 +1907,10 @@
var block: IrStatementContainer? = original
var expr: IrStatement? = block?.statements?.lastOrNull()
while (expr != null && block != null) {
- if (expr is IrReturn) {
+ if (
+ expr is IrReturn &&
+ (expectedTarget == null || expectedTarget == expr.returnTargetSymbol.owner)
+ ) {
block.statements.pop()
return if (expr.value.type.isUnitOrNullableUnit() ||
expr.value.type.isNothing() ||
@@ -2238,6 +2268,12 @@
return irMethodCall(irCurrentComposer(), endMovableFunction)
}
+ private fun irEndToMarker(marker: IrExpression): IrExpression {
+ return irMethodCall(irCurrentComposer(), endToMarkerFunction!!).apply {
+ putValueArgument(0, marker)
+ }
+ }
+
private fun irJoinKeyChain(keyExprs: List<IrExpression>): IrExpression {
return keyExprs.reduce { accumulator, value ->
irMethodCall(irCurrentComposer(), joinKeyFunction).apply {
@@ -2299,7 +2335,7 @@
}
}
- private fun irTemporary(
+ fun irTemporary(
value: IrExpression,
nameHint: String? = null,
irType: IrType = value.type,
@@ -2397,6 +2433,15 @@
}
}
+ private fun IrExpression.variablePrefix(variable: IrVariable) =
+ IrBlockImpl(
+ startOffset,
+ endOffset,
+ type,
+ null,
+ listOf(variable, this)
+ )
+
private fun IrExpression.wrap(
before: List<IrExpression> = emptyList(),
after: List<IrExpression> = emptyList()
@@ -2452,15 +2497,7 @@
)
}
- private fun mutableStatementContainer(): IrContainerExpression {
- // NOTE(lmr): It's important to use IrComposite here so that we don't introduce any new
- // scopes
- return IrCompositeImpl(
- UNDEFINED_OFFSET,
- UNDEFINED_OFFSET,
- context.irBuiltIns.unitType
- )
- }
+ private fun mutableStatementContainer() = mutableStatementContainer(context)
private fun encounteredComposableCall(withGroups: Boolean, isCached: Boolean) {
var scope: Scope? = currentScope
@@ -2546,6 +2583,9 @@
scope.markCoalescableGroup(coalescableScope, realizeGroup, makeEnd)
break@loop
}
+ is Scope.CallScope -> {
+ // Ignore
+ }
else -> error("Unexpected scope type")
}
scope = scope.parent
@@ -2557,16 +2597,43 @@
extraEndLocation: (IrExpression) -> Unit
) {
var scope: Scope? = currentScope
+ val blockScopeMarks = mutableListOf<Scope.BlockScope>()
+ var leavingInlinedLambda = false
loop@ while (scope != null) {
when (scope) {
is Scope.FunctionScope -> {
if (scope.function == symbol.owner) {
- scope.markReturn(extraEndLocation)
+ if (
+ !(
+ leavingInlinedLambda ||
+ (scope.isInlinedLambda && scope.inComposableCall)
+ ) ||
+ !rollbackGroupMarkerEnabled
+ ) {
+ blockScopeMarks.forEach {
+ it.markReturn(extraEndLocation)
+ }
+ scope.markReturn(extraEndLocation)
+ } else {
+ val functionScope = scope
+ if (functionScope.isInlinedLambda) {
+ val marker = irGet(functionScope.allocateMarker())
+ extraEndLocation(irEndToMarker(marker))
+ } else {
+ functionScope.markReturn {
+ val marker = irGet(functionScope.allocateMarker())
+ extraEndLocation(irEndToMarker(marker))
+ extraEndLocation(it)
+ }
+ }
+ }
+
break@loop
}
+ if (scope.isInlinedLambda) leavingInlinedLambda = true
}
is Scope.BlockScope -> {
- scope.markReturn(extraEndLocation)
+ blockScopeMarks.add(scope)
}
else -> {
/* Do nothing, continue traversing */
@@ -2816,9 +2883,14 @@
withGroups = !expression.symbol.owner.isReadonly(),
isCached = false
)
+
+ val callScope = Scope.CallScope(expression, this)
+
// it's important that we transform all of the parameters here since this will cause the
// IrGetValue's of remapped default parameters to point to the right variable.
- expression.transformChildrenVoid()
+ inScope(callScope) {
+ expression.transformChildrenVoid()
+ }
val ownerFn = expression.symbol.owner
val numValueParams = ownerFn.valueParameters.size
@@ -2940,7 +3012,9 @@
)
recordCallInSource(call = expression)
- return expression
+ return callScope.marker?.let {
+ expression.variablePrefix(it)
+ } ?: expression
}
private fun canElideRememberGroup(): Boolean {
@@ -3655,6 +3729,13 @@
val isInlinedLambda: Boolean
get() = transformer.inlineLambdaInfo.isInlineLambda(function)
+ val inComposableCall: Boolean
+ get() = (parent as? Scope.CallScope)?.expression?.let { call ->
+ with(transformer) {
+ call.isTransformedComposableCall() || call.isSyntheticComposableCall()
+ }
+ } == true
+
val metrics: FunctionMetrics = transformer.metricsFor(function)
private var lastTemporaryIndex: Int = 0
@@ -3688,6 +3769,21 @@
var dirty: IrChangedBitMaskValue? = null
+ val markerPreamble = mutableStatementContainer(transformer.context)
+ private var marker: IrVariable? = null
+
+ fun allocateMarker(): IrVariable = marker ?: run {
+ val parent = parent
+ return (if (isInlinedLambda && parent is Scope.CallScope) {
+ parent.allocateMarker()
+ } else transformer.irTemporary(
+ transformer.irCurrentMarker(),
+ getNameForTemporary("marker")
+ ).also { markerPreamble.statements.add(it) }).also {
+ marker = it
+ }
+ }
+
// Parameter information is an index from the sorted order of the parameters to the
// actual order. This is used to reorder the fields of the lambda class generated for
// restart lambdas into parameter order. If all the parameters are in sorted order
@@ -4124,6 +4220,25 @@
}
}
class ParametersScope : BlockScope("parameters")
+
+ class CallScope(
+ val expression: IrCall,
+ private val transformer: ComposableFunctionBodyTransformer
+ ) : Scope("call") {
+ var marker: IrVariable? = null
+ private set
+
+ fun allocateMarker(): IrVariable = marker
+ ?: transformer.irTemporary(
+ transformer.irCurrentMarker(),
+ getNameForTemporary("marker")
+ ).also { marker = it }
+
+ private fun getNameForTemporary(nameHint: String?) =
+ functionScope?.getNameForTemporary(nameHint)
+ ?: error("Expected to be in a function")
+ }
+
class ComposableLambdaScope : BlockScope("composableLambda") {
override fun calculateHasSourceInformation(sourceInformationEnabled: Boolean): Boolean {
return sourceInformationEnabled
@@ -4484,3 +4599,13 @@
value(property.name)
private fun <T> guardedLazy(initializer: () -> T) = GuardedLazy<T>(initializer)
+
+private fun mutableStatementContainer(context: IrPluginContext): IrContainerExpression {
+ // NOTE(lmr): It's important to use IrComposite here so that we don't introduce any new
+ // scopes
+ return IrCompositeImpl(
+ UNDEFINED_OFFSET,
+ UNDEFINED_OFFSET,
+ context.irBuiltIns.unitType
+ )
+}
diff --git a/compose/runtime/runtime/api/current.ignore b/compose/runtime/runtime/api/current.ignore
new file mode 100644
index 0000000..801ddf6
--- /dev/null
+++ b/compose/runtime/runtime/api/current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.runtime.Composer#endToMarker(int):
+ Added method androidx.compose.runtime.Composer.endToMarker(int)
+AddedAbstractMethod: androidx.compose.runtime.Composer#getCurrentMarker():
+ Added method androidx.compose.runtime.Composer.getCurrentMarker()
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index f4bfd21..4210434 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -120,11 +120,13 @@
method @androidx.compose.runtime.ComposeCompilerApi public void endReplaceableGroup();
method @androidx.compose.runtime.ComposeCompilerApi public androidx.compose.runtime.ScopeUpdateScope? endRestartGroup();
method @androidx.compose.runtime.ComposeCompilerApi public void endReusableGroup();
+ method @androidx.compose.runtime.ComposeCompilerApi public void endToMarker(int marker);
method public androidx.compose.runtime.Applier<?> getApplier();
method @org.jetbrains.annotations.TestOnly public kotlin.coroutines.CoroutineContext getApplyCoroutineContext();
method @org.jetbrains.annotations.TestOnly public androidx.compose.runtime.ControlledComposition getComposition();
method public androidx.compose.runtime.tooling.CompositionData getCompositionData();
method public int getCompoundKeyHash();
+ method public int getCurrentMarker();
method public boolean getDefaultsInvalid();
method public boolean getInserting();
method public androidx.compose.runtime.RecomposeScope? getRecomposeScope();
@@ -151,6 +153,7 @@
property @org.jetbrains.annotations.TestOnly public abstract androidx.compose.runtime.ControlledComposition composition;
property public abstract androidx.compose.runtime.tooling.CompositionData compositionData;
property public abstract int compoundKeyHash;
+ property public abstract int currentMarker;
property public abstract boolean defaultsInvalid;
property public abstract boolean inserting;
property public abstract androidx.compose.runtime.RecomposeScope? recomposeScope;
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index 6dca9cb..ce05da6 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -128,11 +128,13 @@
method @androidx.compose.runtime.ComposeCompilerApi public void endReplaceableGroup();
method @androidx.compose.runtime.ComposeCompilerApi public androidx.compose.runtime.ScopeUpdateScope? endRestartGroup();
method @androidx.compose.runtime.ComposeCompilerApi public void endReusableGroup();
+ method @androidx.compose.runtime.ComposeCompilerApi public void endToMarker(int marker);
method public androidx.compose.runtime.Applier<?> getApplier();
method @org.jetbrains.annotations.TestOnly public kotlin.coroutines.CoroutineContext getApplyCoroutineContext();
method @org.jetbrains.annotations.TestOnly public androidx.compose.runtime.ControlledComposition getComposition();
method public androidx.compose.runtime.tooling.CompositionData getCompositionData();
method public int getCompoundKeyHash();
+ method public int getCurrentMarker();
method public boolean getDefaultsInvalid();
method public boolean getInserting();
method public androidx.compose.runtime.RecomposeScope? getRecomposeScope();
@@ -164,6 +166,7 @@
property @org.jetbrains.annotations.TestOnly public abstract androidx.compose.runtime.ControlledComposition composition;
property public abstract androidx.compose.runtime.tooling.CompositionData compositionData;
property public abstract int compoundKeyHash;
+ property public abstract int currentMarker;
property public abstract boolean defaultsInvalid;
property public abstract boolean inserting;
property public abstract androidx.compose.runtime.RecomposeScope? recomposeScope;
diff --git a/compose/runtime/runtime/api/restricted_current.ignore b/compose/runtime/runtime/api/restricted_current.ignore
new file mode 100644
index 0000000..801ddf6
--- /dev/null
+++ b/compose/runtime/runtime/api/restricted_current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.runtime.Composer#endToMarker(int):
+ Added method androidx.compose.runtime.Composer.endToMarker(int)
+AddedAbstractMethod: androidx.compose.runtime.Composer#getCurrentMarker():
+ Added method androidx.compose.runtime.Composer.getCurrentMarker()
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 69872de..6fcdd3c 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -122,11 +122,13 @@
method @androidx.compose.runtime.ComposeCompilerApi public void endReplaceableGroup();
method @androidx.compose.runtime.ComposeCompilerApi public androidx.compose.runtime.ScopeUpdateScope? endRestartGroup();
method @androidx.compose.runtime.ComposeCompilerApi public void endReusableGroup();
+ method @androidx.compose.runtime.ComposeCompilerApi public void endToMarker(int marker);
method public androidx.compose.runtime.Applier<?> getApplier();
method @org.jetbrains.annotations.TestOnly public kotlin.coroutines.CoroutineContext getApplyCoroutineContext();
method @org.jetbrains.annotations.TestOnly public androidx.compose.runtime.ControlledComposition getComposition();
method public androidx.compose.runtime.tooling.CompositionData getCompositionData();
method public int getCompoundKeyHash();
+ method public int getCurrentMarker();
method public boolean getDefaultsInvalid();
method public boolean getInserting();
method public androidx.compose.runtime.RecomposeScope? getRecomposeScope();
@@ -153,6 +155,7 @@
property @org.jetbrains.annotations.TestOnly public abstract androidx.compose.runtime.ControlledComposition composition;
property public abstract androidx.compose.runtime.tooling.CompositionData compositionData;
property public abstract int compoundKeyHash;
+ property public abstract int currentMarker;
property public abstract boolean defaultsInvalid;
property public abstract boolean inserting;
property public abstract androidx.compose.runtime.RecomposeScope? recomposeScope;
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 318a06f..dcbc8dc 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
@@ -724,6 +724,25 @@
/**
* A Compose compiler plugin API. DO NOT call directly.
*
+ * Return a marker for the current group that can be used in a call to [endToMarker].
+ */
+ @ComposeCompilerApi
+ val currentMarker: Int
+
+ /**
+ * Compose compiler plugin API. DO NOT call directly.
+ *
+ * Ends all the groups up to but not including the group that is the parent group when
+ * [currentMarker] was called to produce [marker]. All groups ended must have been started with
+ * either [startReplaceableGroup] or [startMovableGroup]. Ending other groups can cause the
+ * state of the composer to become inconsistent.
+ */
+ @ComposeCompilerApi
+ fun endToMarker(marker: Int)
+
+ /**
+ * A Compose compiler plugin API. DO NOT call directly.
+ *
* Schedule [block] to called with [value]. This is intended to update the node generated by
* [createNode] to changes discovered by composition.
*
@@ -1627,6 +1646,18 @@
reusing = reusingGroup >= 0
}
+ override val currentMarker: Int
+ get() = if (inserting) -writer.parent else reader.parent
+
+ override fun endToMarker(marker: Int) {
+ if (marker < 0) {
+ val writerLocation = -marker
+ while (writer.parent > writerLocation) end(false)
+ } else {
+ while (reader.parent > marker) end(false)
+ }
+ }
+
/**
* Schedule a change to be applied to a node's property. This change will be applied to the
* node that is the current node in the tree which was either created by [createNode].
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
index 82ba839..285838a 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -3232,6 +3232,130 @@
advance()
assertEquals(state, functionInstance())
}
+
+ @Test
+ fun testNonLocalReturn_CM1_RetFunc_FalseTrue() = compositionTest {
+ var condition by mutableStateOf(false)
+
+ compose {
+ test_CM1_RetFun(condition)
+ }
+
+ validate {
+ this.test_CM1_RetFun(condition)
+ }
+
+ condition = true
+
+ expectChanges()
+
+ revalidate()
+ }
+
+ @Test
+ fun testNonLocalReturn_CM1_RetFunc_TrueFalse() = compositionTest {
+ var condition by mutableStateOf(true)
+
+ compose {
+ test_CM1_RetFun(condition)
+ }
+
+ validate {
+ this.test_CM1_RetFun(condition)
+ }
+
+ condition = false
+
+ expectChanges()
+
+ revalidate()
+ }
+
+ @Test
+ fun test_CM1_CCM1_RetFun_FalseTrue() = compositionTest {
+ var condition by mutableStateOf(false)
+
+ compose {
+ test_CM1_CCM1_RetFun(condition)
+ }
+
+ validate {
+ this.test_CM1_CCM1_RetFun(condition)
+ }
+
+ condition = true
+
+ expectChanges()
+
+ revalidate()
+ }
+
+ @Test
+ fun test_CM1_CCM1_RetFun_TrueFalse() = compositionTest {
+ var condition by mutableStateOf(true)
+
+ compose {
+ test_CM1_CCM1_RetFun(condition)
+ }
+
+ validate {
+ this.test_CM1_CCM1_RetFun(condition)
+ }
+
+ condition = false
+
+ expectChanges()
+
+ revalidate()
+ }
+}
+
+@Composable
+fun test_CM1_RetFun(condition: Boolean) {
+ Text("Root - before")
+ M1 {
+ Text("M1 - before")
+ if (condition) return
+ Text("M1 - after")
+ }
+ Text("Root - after")
+}
+
+fun MockViewValidator.test_CM1_RetFun(condition: Boolean) {
+ Text("Root - before")
+ Text("M1 - before")
+ if (condition) return
+ Text("M1 - after")
+ Text("Root - after")
+}
+
+@Composable
+fun test_CM1_CCM1_RetFun(condition: Boolean) {
+ Text("Root - before")
+ M1 {
+ Text("M1 - begin")
+ if (condition) {
+ Text("if - begin")
+ M1 {
+ Text("In CCM1")
+ return@test_CM1_CCM1_RetFun
+ }
+ }
+ Text("M1 - end")
+ }
+ Text("Root - end")
+}
+
+fun MockViewValidator.test_CM1_CCM1_RetFun(condition: Boolean) {
+ Text("Root - before")
+ Text("M1 - begin")
+ if (condition) {
+ Text("if - begin")
+ Text("In CCM1")
+ return
+ }
+ Text("M1 - end")
+ Text("Root - end")
}
var functionInstance: () -> Int = { 0 }
@@ -3391,4 +3515,16 @@
require(aIndex >= 0 && bIndex >= 0)
set(aIndex, b)
set(bIndex, a)
-}
\ No newline at end of file
+}
+
+@Composable
+private inline fun InlineWrapper(content: @Composable () -> Unit) = content()
+
+@Composable
+private inline fun M1(content: @Composable () -> Unit) = InlineWrapper { content() }
+
+@Composable
+private inline fun M2(content: @Composable () -> Unit) = M1 { content() }
+
+@Composable
+private inline fun M3(content: @Composable () -> Unit) = M2 { content() }
diff --git a/core/core/src/main/java/androidx/core/accessibilityservice/OWNERS b/core/core/src/main/java/androidx/core/accessibilityservice/OWNERS
index 0a785d6..d29d58d 100644
--- a/core/core/src/main/java/androidx/core/accessibilityservice/OWNERS
+++ b/core/core/src/main/java/androidx/core/accessibilityservice/OWNERS
@@ -1,2 +1 @@
[email protected]
[email protected]
\ No newline at end of file
+include ../view/accessibility/OWNERS
\ No newline at end of file
diff --git a/core/core/src/main/java/androidx/core/view/OWNERS b/core/core/src/main/java/androidx/core/view/OWNERS
index db3e50a..abaadb7 100644
--- a/core/core/src/main/java/androidx/core/view/OWNERS
+++ b/core/core/src/main/java/androidx/core/view/OWNERS
@@ -1 +1 @@
-per-file AccessibilityDelegateCompat.java = [email protected], [email protected]
\ No newline at end of file
+per-file AccessibilityDelegateCompat.java = file:accessibility/OWNERS
\ No newline at end of file
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
index 74a0ae7..7b38d7d 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
@@ -93,6 +93,28 @@
/**
* Represents the event of an application making an announcement.
+ *
+ * <p>
+ * Note: This event carries no semantic meaning and is appropriate only in exceptional
+ * situations. Apps can generally achieve correct behavior for accessibility by
+ * accurately supplying the semantics of their UI. They should not need to specify what
+ * exactly is announced to users.
+ *
+ * <p>
+ * In general, only announce transitions that can't be handled by the following preferred
+ * alternatives:
+ * <ul>
+ * <li>Label your controls concisely and precisely. Don’t generate a confirmation message
+ * for simple actions like a button press.</li>
+ * <li>For significant UI changes like window changes, use
+ * {@link android.app.Activity#setTitle(CharSequence)} and
+ * {@link androidx.core.view.ViewCompat#setAccessibilityPaneTitle(View, CharSequence)} )}.
+ * </li>
+ * <li>Use {@link androidx.core.view.ViewCompat#setAccessibilityLiveRegion(View, int)}
+ * to inform the user of changes to critical views within the user interface. These
+ * should still be used sparingly as they may generate announcements every time a View is
+ * updated.</li>
+ * </ul>
*/
public static final int TYPE_ANNOUNCEMENT = 0x00004000;
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
index 162df8f..5db4c9d 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
@@ -811,6 +811,10 @@
* A vertical tree is a hierarchical collection with one column and
* as many rows as the first level children.
* </p>
+ * <p>
+ * To be a valid list, a collection has 1 row and any number of columns or 1 column and any
+ * number of rows.
+ * </p>
*/
public static class CollectionInfoCompat {
/** Selection mode where items are not selectable. */
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/OWNERS b/core/core/src/main/java/androidx/core/view/accessibility/OWNERS
index 0a785d6..1c92d03 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/OWNERS
+++ b/core/core/src/main/java/androidx/core/view/accessibility/OWNERS
@@ -1,2 +1,9 @@
+# Bug component: 1110922
+
[email protected]
[email protected]
[email protected]
\ No newline at end of file
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt
index 1769116..3dc1b02 100644
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt
+++ b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt
@@ -185,7 +185,7 @@
// It will be triggered by same-process-write as well. Shared memory version check will
// prevent it from reading again. parameter `path` is relative to the observed directory
override fun onEvent(event: Int, path: String?) {
- if ((downstreamFlow.value !is Final) && (path!! == file.name)) {
+ if ((downstreamFlow.value !is Final) && (file.name == path)) {
readActor.offer(Message.Read(downstreamFlow.value))
}
}
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 92aa50e..a9e5ab6 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -230,129 +230,6 @@
WARN: Missing @param tag for parameter `text` of function androidx\.compose\.ui\.text/AnnotatedString\.Builder/append/\#androidx\.compose\.ui\.text\.AnnotatedString\#kotlin\.Int\#kotlin\.Int/PointingToDeclaration/
WARNING: link to @throws type Renderer\.GlesException does not resolve\. Is it from a package that the containing file does not import\? Is docs inherited to an un\-documented override function, but the exception class is not in scope in the inheriting class\? The general fix for these is to fully qualify the exception name, e\.g\.`@throws java\.io\.IOException under some conditions\. This was observed in Throws\(root=CustomDocTag\(children=\[P\(children=\[Text\(body=If any GL calls fail during initialization\., children=\[\], params=\{\}\)\], params=\{\}\)\], params=\{\}, name=MARKDOWN_FILE\), name=Renderer\.GlesException, exceptionAddress=null\)\.`
WARNING: link to @throws type ServiceStartFailureException does not resolve\. Is it from a package that the containing file does not import\? Is docs inherited to an un\-documented override function, but the exception class is not in scope in the inheriting class\? The general fix for these is to fully qualify the exception name, e\.g\.`@throws java\.io\.IOException under some conditions\. This was observed in Throws\(root=CustomDocTag\(children=\[P\(children=\[Text\(body=if the watchface dies during startup\., children=\[\], params=\{\}\)\], params=\{\}\)\], params=\{\}, name=MARKDOWN_FILE\), name=ServiceStartFailureException, exceptionAddress=null\)\.`
-# Wire proto generation, task :generateDebugProtos
-Writing .* to \$OUT_DIR/.*/build/generated/source/wire
-# > Task :compose:ui:ui-tooling:processDebugAndroidTestManifest
-\$SUPPORT/compose/ui/ui\-tooling/src/androidAndroidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-dagger\.lint\.DaggerIssueRegistry in .*/lint\.jar does not specify a vendor; see IssueRegistry#vendor
-# > Task :compose:foundation:foundation:androidReleaseSourcesJar
-Encountered duplicate path "android[a-zA-Z]*/.+" during copy operation configured with DuplicatesStrategy\.WARN
-# ./gradlew tasks warns as we have warnings present
-You can use \'\-\-warning\-mode all\' to show the individual deprecation warnings and determine if they come from your own scripts or plugins\.
-# > Task :emoji2:emoji2-bundled:processDebugAndroidTestManifest
-\$SUPPORT/emoji[0-9]+/emoji[0-9]+\-bundled/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-meta\-data\#androidx\.emoji[0-9]+\.text\.EmojiCompatInitializer was tagged at AndroidManifest\.xml:[0-9]+ to remove other declarations but no other declaration present
-# > Task :emoji2:emoji2-bundled:processDebugManifest
-\$SUPPORT/emoji[0-9]+/emoji[0-9]+\-bundled/src/main/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-# b/195025261
-To honour the JVM settings for this build a single\-use Daemon process will be forked.*
-# > Tasks configureCMakeRelWithDebInfo or configureCMakeDebug
-C/C\+\+: Building ver\.\: [0-9]+\.[0-9]+\.[0-9]+
-C/C\+\+: Packaging for\: (amd\-[0-9]+|armhf\-[0-9]+|x86\-[0-9]+)
-C/C\+\+: Compiling for ARM
-w: \[ksp\] Using @JvmName annotation on a function or accessor that will be overridden by Room is not supported\. If this is important for your use case, please file a bug at https://issuetracker\.google\.com/issues/new\?component=[0-9]+ with details\. \- androidx\.room\.androidx\.room\.integration\.kotlintestapp\.test\.JvmNameInDaoTest\.JvmNameDb\.jvmDao\(\)
-w: \[ksp\] \$SUPPORT/room/integration\-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/JvmNameInDaoTest\.kt:[0-9]+: Using @JvmName annotation on a function or accessor that will be overridden by Room is not supported\. If this is important for your use case, please file a bug at https://issuetracker\.google\.com/issues/new\?component=[0-9]+ with details\.
-# > Task :room:integration-tests:room-testapp-kotlin:kaptWithKaptDebugAndroidTestKotlin
-\$OUT_DIR/androidx/room/integration\-tests/room\-testapp\-kotlin/build/tmp/kapt[0-9]+/stubs/withKaptDebugAndroidTest/androidx/room/androidx/room/integration/kotlintestapp/test/JvmNameInDaoTest\.java:[0-9]+: warning: Using @JvmName annotation on a function or accessor that will be overridden by Room is not supported\. If this is important for your use case, please file a bug at https://issuetracker\.google\.com/issues/new\?component=[0-9]+ with details\.
-public abstract void jvmDelete\(T t\);
-public abstract void jvmInsert\(@org\.jetbrains\.annotations\.NotNull.*
-public abstract java\.util\.List<androidx\.room\.androidx\.room\.integration\.kotlintestapp\.test\.JvmNameInDaoTest\.JvmNameEntity> jvmQuery\(\);
-public abstract androidx\.room\.androidx\.room\.integration\.kotlintestapp\.test\.JvmNameInDaoTest\.JvmNameDao jvmDao\(\);
-\^
-# Gradle will log if you are not authenticated to upload scans
-A build scan was not published as you have not authenticated with server 'ge\.androidx\.dev'\.
-# Room unresolved type error messages
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$createFromAsset\(kotlin\.String\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$createFromAsset\(kotlin\.String\,\ androidx\.room\.RoomDatabase\.PrepackagedDatabaseCallback\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$createFromFile\(java\.io\.File\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$createFromFile\(java\.io\.File\,\ androidx\.room\.RoomDatabase\.PrepackagedDatabaseCallback\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$createFromInputStream\(java\.util\.concurrent\.Callable\(\(java\.io\.InputStream\)\)\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$createFromInputStream\(java\.util\.concurrent\.Callable\(\(java\.io\.InputStream\)\), androidx\.room\.RoomDatabase\.PrepackagedDatabaseCallback\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$openHelperFactory\(androidx\.sqlite\.db\.SupportSQLiteOpenHelper\.Factory\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$addMigrations\(kotlin\.Array\(\(androidx\.room\.migration\.Migration\)\)\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$addAutoMigrationSpec\(androidx\.room\.migration\.AutoMigrationSpec\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$allowMainThreadQueries\(\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$setJournalMode\(androidx\.room\.RoomDatabase\.JournalMode\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$setQueryExecutor\(java\.util\.concurrent\.Executor\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$setTransactionExecutor\(java\.util\.concurrent\.Executor\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$enableMultiInstanceInvalidation\(\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$setMultiInstanceInvalidationServiceIntent\(android\.content\.Intent\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$fallbackToDestructiveMigration\(\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$fallbackToDestructiveMigrationFrom\(kotlin\.IntArray\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$fallbackToDestructiveMigrationOnDowngrade\(\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$addCallback\(androidx\.room\.RoomDatabase\.Callback\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$setQueryCallback\(androidx\.room\.RoomDatabase\.QueryCallback, java\.util\.concurrent\.Executor\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$addTypeConverter\(kotlin\.Any\) \(RoomDatabase\.kt:[0-9]+\)
-Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$setAutoCloseTimeout\(kotlin\.Long, java\.util\.concurrent\.TimeUnit\) \(RoomDatabase\.kt:[0-9]+\)
-# > Task :compose:ui:ui:compileReleaseKotlin
-w: \$SUPPORT/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat\.android\.kt: \([0-9]+, [0-9]+\): Unnecessary non\-null assertion \(!!\) on a non\-null receiver of type LayoutNode
-# When konan is downloading a dependency from another file, don't warn about it.
-\(KonanProperies\) Downloading dependency: file:\.\./\.\./.*
-Please wait while Kotlin/Native compiler .* is being installed\.
-Unpack Kotlin/Native compiler to .*
-Download file:\.\./\.\./.*
-Downloading native dependencies \(LLVM, sysroot etc\)\. This is a one\-time action performed only on the first run of the compiler\.
-Extracting dependency: .*\.konan/cache.*
-# New memory model does not work with compiler caches yet:
-# https://github.com/JetBrains/kotlin/blob/master/kotlin-native/NEW_MM.md#known-bugs
-w: Cached libraries will not be used with experimental memory model
-# > Task :commonizeNativeDistribution
-# Linux builds cannot build mac, hence we get this warning.
-# see: https://github.com/JetBrains/kotlin/blob/master/native/commonizer/README.md
-# This warning is printed from: https://github.com/JetBrains/kotlin/blob/bc853e45e8982eff74e3263b0197c1af6086615d/native/commonizer/src/org/jetbrains/kotlin/commonizer/konan/LibraryCommonizer.kt#L41
-Warning\: No libraries found for target (macos|ios|ios_simulator)_(arm|x)[0-9]+\. This target will be excluded from commonization\.
-void androidx.tv.foundation.lazy.list.LazyListKt.LazyList(androidx.compose.ui.Modifier, androidx.tv.foundation.lazy.list.TvLazyListState, androidx.compose.foundation.layout.PaddingValues, boolean, boolean, boolean, androidx.tv.foundation.PivotOffsets, androidx.compose.ui.Alignment$Horizontal, androidx.compose.foundation.layout.Arrangement$Vertical, androidx.compose.ui.Alignment$Vertical, androidx.compose.foundation.layout.Arrangement$Horizontal, kotlin.jvm.functions.Function1, androidx.compose.runtime.Composer, int, int, int)
-void androidx.tv.foundation.lazy.grid.LazyGridKt.LazyGrid(androidx.compose.ui.Modifier, androidx.tv.foundation.lazy.grid.TvLazyGridState, kotlin.jvm.functions.Function2, androidx.compose.foundation.layout.PaddingValues, boolean, boolean, boolean, androidx.compose.foundation.layout.Arrangement$Vertical, androidx.compose.foundation.layout.Arrangement$Horizontal, androidx.tv.foundation.PivotOffsets, kotlin.jvm.functions.Function1, androidx.compose.runtime.Composer, int, int, int)
-# > Task :room:integration-tests:room-testapp:mergeDexWithExpandProjectionDebugAndroidTest
-WARNING:D[0-9]+: Application does not contain `androidx\.tracing\.Trace` as referenced in main\-dex\-list\.
-# > Task :hilt:hilt-compiler:kaptTestKotlin
-Annotation processors discovery from compile classpath is deprecated\.
-Set 'kapt\.include\.compile\.classpath=false' to disable discovery\.
-Run the build with '\-\-info' for more details\.
-# > Task :health:health-connect-client:testDebugUnitTest
-WARNING: An illegal reflective access operation has occurred
-WARNING: Illegal reflective access by org\.robolectric\.util\.ReflectionHelpers\$[0-9]+ \(file:\$CHECKOUT/prebuilts/androidx/external/org/robolectric/shadowapi/.*/shadowapi\-.*\.jar\) to field java\.io\.FileDescriptor\.fd
-WARNING: Illegal reflective access by org\.robolectric\.util\.ReflectionHelpers \(file:\$CHECKOUT/prebuilts/androidx/external/org/robolectric/shadowapi/.*/shadowapi\-.*\.jar\) to method java\.lang\.Class\.getDeclaredFields0\(boolean\)
-WARNING: Please consider reporting this to the maintainers of org\.robolectric\.util\.ReflectionHelpers.*
-WARNING: Illegal reflective access by org\.jetbrains\.kotlin\.kapt[0-9]+\.base\.javac\.KaptJavaFileManager \(file:\$CHECKOUT/prebuilts/androidx/external/org/jetbrains/kotlin/kotlin\-annotation\-processing\-embeddable/[0-9]+\.[0-9]+\.[0-9]+\-RC/kotlin\-annotation\-processing\-embeddable\-[0-9]+\.[0-9]+\.[0-9]+\-RC\.jar\) to method com\.sun\.tools\.javac\.file\.BaseFileManager\.handleOption\(com\.sun\.tools\.javac\.main\.Option,java\.lang\.String\)
-WARNING\: Illegal reflective access by org\.jetbrains\.kotlin\.kapt[0-9]+\.base\.javac\.KaptJavaFileManager \(file\:\$CHECKOUT\/prebuilts\/androidx\/external\/org\/jetbrains\/kotlin\/kotlin\-annotation\-processing\-embeddable\/[0-9]+\.[0-9]+\.[0-9]+\-Beta\/kotlin\-annotation\-processing\-embeddable\-[0-9]+\.[0-9]+\.[0-9]+\-Beta\.jar\) to method com\.sun\.tools\.javac\.file\.BaseFileManager\.handleOption\(com\.sun\.tools\.javac\.main\.Option\,java\.lang\.String\)
-WARNING: Illegal reflective access by org\.robolectric\.util\.ReflectionHelpers\$[0-9]+ \(file:\$CHECKOUT/prebuilts/androidx/external/org/robolectric/shadowapi/[0-9]+\.[0-9]+\.[0-9]+/shadowapi\-[0-9]+\.[0-9]+\.[0-9]+\.jar\) to field java\.io\.FileDescriptor\.fd
-WARNING: Use \-\-illegal\-access=warn to enable warnings of further illegal reflective access operations
-WARNING: All illegal access operations will be denied in a future release
-# > Task :room:room-compiler-processing-testing:test
-WARNING: Illegal reflective access by androidx\.room\.compiler\.processing\.javac\.JavacProcessingEnvMessager\$Companion \(file:\$OUT_DIR/androidx/room/room\-compiler\-processing/build/(classes/kotlin/main/|libs/room\-compiler\-processing\-[0-9]+\.[0-9]+\.[0-9]+(\-(alpha|beta|rc)[0-9]+)?\.jar)\) to field com\.sun\.tools\.javac\.code\.Symbol\.owner
-WARNING: Please consider reporting this to the maintainers of androidx\.room\.compiler\.processing\.javac\.JavacProcessingEnvMessager\$Companion
-# > Task :room:room-compiler:test
-WARNING: Illegal reflective access by androidx\.room\.compiler\.processing\.javac\.JavacProcessingEnvMessager\$Companion \(file:\$OUT_DIR/androidx/room/room\-compiler\-processing/build/(classes/kotlin/main/|libs/room\-compiler\-processing\-[0-9]+\.[0-9]+\.[0-9]+(\-(alpha|beta|rc)[0-9]+)?\.jar)\) to field com\.sun\.tools\.javac\.code\.Symbol\$ClassSymbol\.classfile
-# > Task :room:room-compiler-processing:test
-WARNING: Illegal reflective access by org\.jetbrains\.kotlin\.kapt[0-9]+\.base\.javac\.KaptJavaFileManager \(file:\$CHECKOUT/prebuilts/androidx/external/org/jetbrains/kotlin/kotlin\-annotation\-processing\-embeddable/[0-9]+\.[0-9]+\.[0-9]+/kotlin\-annotation\-processing\-embeddable\-[0-9]+\.[0-9]+\.[0-9]+\.jar\) to method com\.sun\.tools\.javac\.file\.BaseFileManager\.handleOption\(com\.sun\.tools\.javac\.main\.Option[,\.]java\.lang\.String\)
-WARNING: Please consider reporting this to the maintainers of org\.jetbrains\.kotlin\.kapt[0-9]+\.base\.javac\.KaptJavaFileManager
-# > Task :car:app:app-testing:testDebugUnitTest
-WARNING: Illegal reflective access by org\.robolectric\.interceptors\.AndroidInterceptors\$FileDescriptorInterceptor to field java\.io\.FileDescriptor\.fd
-WARNING: Please consider reporting this to the maintainers of org\.robolectric\.interceptors\.AndroidInterceptors\$FileDescriptorInterceptor
-# > Task :compose:compiler:compiler-hosted:integration-tests:testDebugUnitTest b/238915109
-WARNING: Illegal reflective access by androidx\.compose\.compiler\.plugins\.kotlin\.ComposeCodegenTestUtilsKt .* to method java\.lang\.ClassLoader\.defineClass.*
-WARNING: Please consider reporting this to the maintainers of androidx\.compose\.compiler\.plugins\.kotlin\.ComposeCodegenTestUtilsKt
-# > Task :buildSrc-tests:test
-WARNING: Illegal reflective access using Lookup on org\.gradle\.internal\.classloader\.ClassLoaderUtils\$AbstractClassLoaderLookuper .* to class java\.lang\.ClassLoader
-WARNING: Please consider reporting this to the maintainers of org\.gradle\.internal\.classloader\.ClassLoaderUtils\$AbstractClassLoaderLookuper
-# AGP warning about API usage we have no control over
-Values of variant API AnnotationProcessorOptions\.arguments are queried and may return non final values, this is unsupported
-# > Task :compose:ui:ui:testDebugUnitTest
-OpenJDK 64\-Bit Server VM warning:
-.*Sharing is only supported for boot loader classes because bootstrap classpath has been appended
-# > Task :concurrent:concurrent-futures:compileTestJava b/242311027
-\$SUPPORT/concurrent/concurrent\-futures/src/test/java/androidx/concurrent/futures/AbstractResolvableFutureTest\.java:[0-9]+: warning: \[removal\] (resume|suspend)\(\) in Thread has been deprecated and marked for removal
-thread\.(resume|suspend)\(\);
-2 warnings
-# AGP warning that will go away soon
-WARNING:Software Components will not be created automatically for Maven publishing from Android Gradle Plugin 8\.0\. To opt\-in to the future behavior, set the Gradle property android\.disableAutomaticComponentCreation=true in the `gradle\.properties` file or use the new publishing DSL\.
-# > Task :graphics:graphics-path:compileDebugKotlin
-w\: \$SUPPORT\/graphics\/graphics\-path\/src\/main\/java\/androidx\/graphics\/path\/Paths\.kt\: \([0-9]+\, [0-9]+\)\: Extension is shadowed by a member\: public open fun iterator\(\)\: PathIterator
-# > Task :core:core-splashscreen:core-splashscreen-samples:lintReportDebug
-Warning: Lint will treat :annotation:annotation as an external dependency and not analyze it\.
-Did you make a typo\? Are you trying to refer to something not visible to users\?
-\* Recommended Action: Apply the 'com\.android\.lint' plugin to java library project :annotation:annotation\. to enable lint to analyze those sources\.
WARN: Sources for .+ is empty
WARN: Missing @param tag for parameter `iterations` of function androidx\.benchmark\.macro\.junit[0-9]+/BaselineProfileRule/collectBaselineProfile/\#kotlin\.String\#kotlin\.Int\#kotlin\.collections\.List\[kotlin\.String\]\#kotlin\.Function[0-9]+\[androidx\.benchmark\.macro\.MacrobenchmarkScope,kotlin\.Unit\]/PointingToDeclaration/
WARN: Missing @param tag for parameter `priority` of function androidx\.camera\.camera[0-9]+\.interop/CaptureRequestOptions/retrieveOptionWithPriority/\#androidx\.camera\.core\.impl\.Config\.Option<ValueT>\#androidx\.camera\.core\.impl\.Config\.OptionPriority/PointingToDeclaration/
@@ -812,5 +689,101 @@
WARN: Missing @param tag for parameter `clipOp` of function androidx\.compose\.ui\.graphics/Canvas/clipRect/\#kotlin\.Float\#kotlin\.Float\#kotlin\.Float\#kotlin\.Float\#androidx\.compose\.ui\.graphics\.ClipOp/PointingToDeclaration/
WARN: Missing @param tag for parameter `paint` of function androidx\.compose\.ui\.graphics/Canvas/drawArc/\#kotlin\.Float\#kotlin\.Float\#kotlin\.Float\#kotlin\.Float\#kotlin\.Float\#kotlin\.Float\#kotlin\.Boolean\#androidx\.compose\.ui\.graphics\.Paint/PointingToDeclaration/
WARN: Missing @param tag for parameter `operation` of function androidx\.compose\.ui\.graphics/AndroidPath/op/\#androidx\.compose\.ui\.graphics\.Path\#androidx\.compose\.ui\.graphics\.Path\#androidx\.compose\.ui\.graphics\.PathOperation/PointingToDeclaration/
+# Wire proto generation, task :generateDebugProtos
+Writing .* to \$OUT_DIR/.*/build/generated/source/wire
+# > Task :compose:ui:ui-tooling:processDebugAndroidTestManifest
+\$SUPPORT/compose/ui/ui\-tooling/src/androidAndroidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
+dagger\.lint\.DaggerIssueRegistry in .*/lint\.jar does not specify a vendor; see IssueRegistry#vendor
+# > Task :compose:foundation:foundation:androidReleaseSourcesJar
+Encountered duplicate path "android[a-zA-Z]*/.+" during copy operation configured with DuplicatesStrategy\.WARN
+# ./gradlew tasks warns as we have warnings present
+You can use \'\-\-warning\-mode all\' to show the individual deprecation warnings and determine if they come from your own scripts or plugins\.
+# > Task :emoji2:emoji2-bundled:processDebugAndroidTestManifest
+\$SUPPORT/emoji[0-9]+/emoji[0-9]+\-bundled/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
+meta\-data\#androidx\.emoji[0-9]+\.text\.EmojiCompatInitializer was tagged at AndroidManifest\.xml:[0-9]+ to remove other declarations but no other declaration present
+# > Task :emoji2:emoji2-bundled:processDebugManifest
+\$SUPPORT/emoji[0-9]+/emoji[0-9]+\-bundled/src/main/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
+# b/195025261
+To honour the JVM settings for this build a single\-use Daemon process will be forked.*
+# > Tasks configureCMakeRelWithDebInfo or configureCMakeDebug
+C/C\+\+: Building ver\.\: [0-9]+\.[0-9]+\.[0-9]+
+C/C\+\+: Packaging for\: (amd\-[0-9]+|armhf\-[0-9]+|x86\-[0-9]+)
+C/C\+\+: Compiling for ARM
+w: \[ksp\] Using @JvmName annotation on a function or accessor that will be overridden by Room is not supported\. If this is important for your use case, please file a bug at https://issuetracker\.google\.com/issues/new\?component=[0-9]+ with details\. \- androidx\.room\.androidx\.room\.integration\.kotlintestapp\.test\.JvmNameInDaoTest\.JvmNameDb\.jvmDao\(\)
+w: \[ksp\] \$SUPPORT/room/integration\-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/JvmNameInDaoTest\.kt:[0-9]+: Using @JvmName annotation on a function or accessor that will be overridden by Room is not supported\. If this is important for your use case, please file a bug at https://issuetracker\.google\.com/issues/new\?component=[0-9]+ with details\.
+# > Task :room:integration-tests:room-testapp-kotlin:kaptWithKaptDebugAndroidTestKotlin
+\$OUT_DIR/androidx/room/integration\-tests/room\-testapp\-kotlin/build/tmp/kapt[0-9]+/stubs/withKaptDebugAndroidTest/androidx/room/androidx/room/integration/kotlintestapp/test/JvmNameInDaoTest\.java:[0-9]+: warning: Using @JvmName annotation on a function or accessor that will be overridden by Room is not supported\. If this is important for your use case, please file a bug at https://issuetracker\.google\.com/issues/new\?component=[0-9]+ with details\.
+public abstract void jvmDelete\(T t\);
+public abstract void jvmInsert\(@org\.jetbrains\.annotations\.NotNull.*
+public abstract java\.util\.List<androidx\.room\.androidx\.room\.integration\.kotlintestapp\.test\.JvmNameInDaoTest\.JvmNameEntity> jvmQuery\(\);
+public abstract androidx\.room\.androidx\.room\.integration\.kotlintestapp\.test\.JvmNameInDaoTest\.JvmNameDao jvmDao\(\);
+\^
+# Gradle will log if you are not authenticated to upload scans
+A build scan was not published as you have not authenticated with server 'ge\.androidx\.dev'\.
+# Room unresolved type error messages
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$createFromAsset\(kotlin\.String\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$createFromAsset\(kotlin\.String\,\ androidx\.room\.RoomDatabase\.PrepackagedDatabaseCallback\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$createFromFile\(java\.io\.File\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$createFromFile\(java\.io\.File\,\ androidx\.room\.RoomDatabase\.PrepackagedDatabaseCallback\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$createFromInputStream\(java\.util\.concurrent\.Callable\(\(java\.io\.InputStream\)\)\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$createFromInputStream\(java\.util\.concurrent\.Callable\(\(java\.io\.InputStream\)\), androidx\.room\.RoomDatabase\.PrepackagedDatabaseCallback\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$openHelperFactory\(androidx\.sqlite\.db\.SupportSQLiteOpenHelper\.Factory\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$addMigrations\(kotlin\.Array\(\(androidx\.room\.migration\.Migration\)\)\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$addAutoMigrationSpec\(androidx\.room\.migration\.AutoMigrationSpec\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$allowMainThreadQueries\(\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$setJournalMode\(androidx\.room\.RoomDatabase\.JournalMode\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$setQueryExecutor\(java\.util\.concurrent\.Executor\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$setTransactionExecutor\(java\.util\.concurrent\.Executor\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$enableMultiInstanceInvalidation\(\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$setMultiInstanceInvalidationServiceIntent\(android\.content\.Intent\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$fallbackToDestructiveMigration\(\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$fallbackToDestructiveMigrationFrom\(kotlin\.IntArray\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$fallbackToDestructiveMigrationOnDowngrade\(\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$addCallback\(androidx\.room\.RoomDatabase\.Callback\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$setQueryCallback\(androidx\.room\.RoomDatabase\.QueryCallback, java\.util\.concurrent\.Executor\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$addTypeConverter\(kotlin\.Any\) \(RoomDatabase\.kt:[0-9]+\)
+Found an unresolved type in androidx\.room\.RoomDatabase\.Builder\$setAutoCloseTimeout\(kotlin\.Long, java\.util\.concurrent\.TimeUnit\) \(RoomDatabase\.kt:[0-9]+\)
+# > Task :compose:ui:ui:compileReleaseKotlin
+w: \$SUPPORT/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat\.android\.kt: \([0-9]+, [0-9]+\): Unnecessary non\-null assertion \(!!\) on a non\-null receiver of type LayoutNode
+# When konan is downloading a dependency from another file, don't warn about it.
+\(KonanProperies\) Downloading dependency: file:\.\./\.\./.*
+Please wait while Kotlin/Native compiler .* is being installed\.
+Unpack Kotlin/Native compiler to .*
+Download file:\.\./\.\./.*
+Downloading native dependencies \(LLVM, sysroot etc\)\. This is a one\-time action performed only on the first run of the compiler\.
+Extracting dependency: .*\.konan/cache.*
+# New memory model does not work with compiler caches yet:
+# https://github.com/JetBrains/kotlin/blob/master/kotlin-native/NEW_MM.md#known-bugs
+w: Cached libraries will not be used with experimental memory model
+# > Task :commonizeNativeDistribution
+# Linux builds cannot build mac, hence we get this warning.
+# see: https://github.com/JetBrains/kotlin/blob/master/native/commonizer/README.md
+# This warning is printed from: https://github.com/JetBrains/kotlin/blob/bc853e45e8982eff74e3263b0197c1af6086615d/native/commonizer/src/org/jetbrains/kotlin/commonizer/konan/LibraryCommonizer.kt#L41
+Warning\: No libraries found for target (macos|ios|ios_simulator)_(arm|x)[0-9]+\. This target will be excluded from commonization\.
+void androidx.tv.foundation.lazy.list.LazyListKt.LazyList(androidx.compose.ui.Modifier, androidx.tv.foundation.lazy.list.TvLazyListState, androidx.compose.foundation.layout.PaddingValues, boolean, boolean, boolean, androidx.tv.foundation.PivotOffsets, androidx.compose.ui.Alignment$Horizontal, androidx.compose.foundation.layout.Arrangement$Vertical, androidx.compose.ui.Alignment$Vertical, androidx.compose.foundation.layout.Arrangement$Horizontal, kotlin.jvm.functions.Function1, androidx.compose.runtime.Composer, int, int, int)
+void androidx.tv.foundation.lazy.grid.LazyGridKt.LazyGrid(androidx.compose.ui.Modifier, androidx.tv.foundation.lazy.grid.TvLazyGridState, kotlin.jvm.functions.Function2, androidx.compose.foundation.layout.PaddingValues, boolean, boolean, boolean, androidx.compose.foundation.layout.Arrangement$Vertical, androidx.compose.foundation.layout.Arrangement$Horizontal, androidx.tv.foundation.PivotOffsets, kotlin.jvm.functions.Function1, androidx.compose.runtime.Composer, int, int, int)
+# > Task :room:integration-tests:room-testapp:mergeDexWithExpandProjectionDebugAndroidTest
+WARNING:D[0-9]+: Application does not contain `androidx\.tracing\.Trace` as referenced in main\-dex\-list\.
+# > Task :hilt:hilt-compiler:kaptTestKotlin
+Annotation processors discovery from compile classpath is deprecated\.
+Set 'kapt\.include\.compile\.classpath=false' to disable discovery\.
+Run the build with '\-\-info' for more details\.
+# AGP warning about API usage we have no control over
+Values of variant API AnnotationProcessorOptions\.arguments are queried and may return non final values, this is unsupported
+# > Task :compose:ui:ui:testDebugUnitTest
+OpenJDK 64\-Bit Server VM warning:
+.*Sharing is only supported for boot loader classes because bootstrap classpath has been appended
+# > Task :concurrent:concurrent-futures:compileTestJava b/242311027
+\$SUPPORT/concurrent/concurrent\-futures/src/test/java/androidx/concurrent/futures/AbstractResolvableFutureTest\.java:[0-9]+: warning: \[removal\] (resume|suspend)\(\) in Thread has been deprecated and marked for removal
+thread\.(resume|suspend)\(\);
+2 warnings
+# AGP warning that will go away soon
+WARNING:Software Components will not be created automatically for Maven publishing from Android Gradle Plugin 8\.0\. To opt\-in to the future behavior, set the Gradle property android\.disableAutomaticComponentCreation=true in the `gradle\.properties` file or use the new publishing DSL\.
+# > Task :graphics:graphics-path:compileDebugKotlin
+w\: \$SUPPORT\/graphics\/graphics\-path\/src\/main\/java\/androidx\/graphics\/path\/Paths\.kt\: \([0-9]+\, [0-9]+\)\: Extension is shadowed by a member\: public open fun iterator\(\)\: PathIterator
+# > Task :core:core-splashscreen:core-splashscreen-samples:lintReportDebug
+Warning: Lint will treat :annotation:annotation as an external dependency and not analyze it\.
+Did you make a typo\? Are you trying to refer to something not visible to users\?
+\* Recommended Action: Apply the 'com\.android\.lint' plugin to java library project :annotation:annotation\. to enable lint to analyze those sources\.
# > Task :linkDebugTestIosX64 b/253041601
w: Cached libraries will not be used with std allocator
diff --git a/emoji2/emoji2-emojipicker/build.gradle b/emoji2/emoji2-emojipicker/build.gradle
index 008a65a..53a6ddb 100644
--- a/emoji2/emoji2-emojipicker/build.gradle
+++ b/emoji2/emoji2-emojipicker/build.gradle
@@ -26,6 +26,8 @@
api(libs.kotlinStdlib)
implementation("androidx.core:core-ktx:1.8.0")
implementation project(path: ':emoji2:emoji2')
+ implementation project(path: ':core:core')
+
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testCore)
}
diff --git a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt
index 00f13b4..cc5b603 100644
--- a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt
+++ b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt
@@ -17,6 +17,7 @@
package androidx.emoji2.emojipicker
import android.content.Context
+import androidx.emoji2.emojipicker.utils.FileCache
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
@@ -30,15 +31,52 @@
private val emojiCompatMetadata = EmojiPickerView.EmojiCompatMetadata(null, false)
@Test
- fun testGetCategorizedEmojiData_loaded() {
+ fun testGetCategorizedEmojiData_loaded_writeToCache() {
+ // delete cache dir first
+ val fileCache = FileCache.getInstance(context)
+ fileCache.emojiPickerCacheDir.deleteRecursively()
+ assertFalse(fileCache.emojiPickerCacheDir.exists())
+
BundledEmojiListLoader.load(context, emojiCompatMetadata)
assertTrue(BundledEmojiListLoader.categorizedEmojiData.isNotEmpty())
+
+ // emoji_picker/osVersion|appVersion/ folder should be created
+ val propertyFolder = fileCache.emojiPickerCacheDir.listFiles()!![0]
+ assertTrue(propertyFolder!!.isDirectory)
+
+ // Number of cache files should match the size of categorizedEmojiData
+ val cacheFiles = propertyFolder.listFiles()
+ assertTrue(
+ cacheFiles!!.size == BundledEmojiListLoader.categorizedEmojiData.size
+ )
+ }
+
+ @Test
+ fun testGetCategorizedEmojiData_loaded_readFromCache() {
+ // delete cache and load again
+ val fileCache = FileCache.getInstance(context)
+ fileCache.emojiPickerCacheDir.deleteRecursively()
+ BundledEmojiListLoader.load(context, emojiCompatMetadata)
+
+ val cacheFileName = fileCache.emojiPickerCacheDir.listFiles()!![0].listFiles()!![0].name
+ val emptyDefaultValue = listOf<BundledEmojiListLoader.EmojiData>()
+ // Read from cache instead of using default value
+ var output = fileCache.getOrPut(cacheFileName) { emptyDefaultValue }
+ assertTrue(output.isNotEmpty())
+
+ // Remove cache, write default value to cache
+ fileCache.emojiPickerCacheDir.deleteRecursively()
+ output = fileCache.getOrPut(cacheFileName) { emptyDefaultValue }
+ assertTrue(output.isEmpty())
}
@Test
@SdkSuppress(minSdkVersion = 21)
fun testGetEmojiVariantsLookup_loaded() {
+ // delete cache and load again
+ FileCache.getInstance(context).emojiPickerCacheDir.deleteRecursively()
BundledEmojiListLoader.load(context, emojiCompatMetadata)
+
// 👃 has variants (👃,👃,👃🏻,👃🏼,👃🏽,👃🏾,👃🏿)
assertTrue(
BundledEmojiListLoader
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
index 9d1b492..8795edf 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
@@ -18,6 +18,7 @@
import android.content.Context
import androidx.core.content.res.use
+import androidx.emoji2.emojipicker.utils.FileCache
import androidx.emoji2.emojipicker.utils.UnicodeRenderableManager
/**
@@ -30,26 +31,25 @@
* emoji. This allows faster variants lookup.
*/
internal object BundledEmojiListLoader {
-
private var _categorizedEmojiData: List<EmojiDataCategory>? = null
private var _emojiVariantsLookup: Map<String, List<String>>? = null
internal fun load(context: Context, emojiCompatMetadata: EmojiPickerView.EmojiCompatMetadata) {
- // TODO(chelseahao): load from cache.
val categoryNames = context.resources.getStringArray(R.array.category_names)
_categorizedEmojiData = context.resources
.obtainTypedArray(R.array.emoji_by_category_raw_resources)
.use { ta ->
+ val emojiFileCache = FileCache.getInstance(context)
(0 until ta.length()).map {
- EmojiDataCategory(
- categoryNames[it],
+ val cacheFileName = getCacheFileName(it, emojiCompatMetadata)
+ emojiFileCache.getOrPut(cacheFileName) {
loadSingleCategory(
context,
emojiCompatMetadata,
ta.getResourceId(it, 0)
)
- )
+ }.let { data -> EmojiDataCategory(categoryNames[it], data) }
}.toList()
}
@@ -82,6 +82,18 @@
.filter { it.isNotEmpty() }
.map { EmojiData(it.first(), it.drop(1)) }
+ private fun getCacheFileName(
+ categoryIndex: Int,
+ emojiCompatMetadata: EmojiPickerView.EmojiCompatMetadata
+ ) = StringBuilder().append("emoji.v1.")
+ .append(emojiCompatMetadata.hashCode())
+ .append(".")
+ .append(categoryIndex)
+ .append(".")
+ .append(
+ if (UnicodeRenderableManager.isEmoji12Supported(emojiCompatMetadata)) 1 else 0
+ ).toString()
+
/**
* To eliminate 'Tofu' (the fallback glyph when an emoji is not renderable), check the
* renderability of emojis and keep only when they are renderable on the current device.
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt
new file mode 100644
index 0000000..6c1d57d
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.emoji2.emojipicker.utils
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.ContextCompat
+import androidx.emoji2.emojipicker.BundledEmojiListLoader
+import java.io.File
+
+/**
+ * A class that manages cache files for the emoji picker. All cache files are stored in DE
+ * (Device Encryption) storage (N+), and will be invalidated if device OS version or App version is
+ * updated.
+ *
+ * Currently this class is only used by [BundledEmojiListLoader]. All renderable emojis will be
+ * cached by categories under
+ * /app.package.name/cache/emoji_picker/<osVersion|appVersion>
+ * /emoji.<emojiPickerVersion>.<emojiCompatMetadataHashCode>.<categoryIndex>.<ifEmoji12Supported>
+ */
+internal class FileCache(context: Context) {
+
+ @VisibleForTesting
+ internal val emojiPickerCacheDir: File
+ private val currentProperty: String
+
+ init {
+ val osVersion = "${Build.VERSION.SDK_INT}_${Build.TIME}"
+ val appVersion = getVersionCode(context)
+ currentProperty = "$osVersion|$appVersion"
+ emojiPickerCacheDir =
+ File(getDeviceProtectedStorageContext(context).cacheDir, EMOJI_PICKER_FOLDER)
+ }
+
+ /** Get cache for a given file name, or write to a new file using the [defaultValue] factory. */
+ internal fun getOrPut(
+ key: String,
+ defaultValue: () -> List<BundledEmojiListLoader.EmojiData>
+ ): List<BundledEmojiListLoader.EmojiData> {
+ val targetDir = File(emojiPickerCacheDir, currentProperty)
+ // No matching cache folder for current property, clear stale cache directory if any
+ if (!targetDir.exists()) {
+ emojiPickerCacheDir.listFiles()?.forEach { it.deleteRecursively() }
+ targetDir.mkdirs()
+ }
+
+ val targetFile = File(targetDir, key)
+ return readFrom(targetFile) ?: writeTo(targetFile, defaultValue)
+ }
+
+ private fun readFrom(targetFile: File): List<BundledEmojiListLoader.EmojiData>? {
+ if (!targetFile.isFile)
+ return null
+ return targetFile.bufferedReader()
+ .useLines { it.toList() }
+ .map { it.split(",") }
+ .map { BundledEmojiListLoader.EmojiData(it.first(), it.drop(1)) }
+ }
+
+ private fun writeTo(
+ targetFile: File,
+ defaultValue: () -> List<BundledEmojiListLoader.EmojiData>
+ ): List<BundledEmojiListLoader.EmojiData> {
+ val data = defaultValue.invoke()
+ targetFile.bufferedWriter()
+ .use { out ->
+ for (emoji in data) {
+ out.write(emoji.primary)
+ emoji.variants.forEach { out.write(",$it") }
+ out.newLine()
+ }
+ }
+ return data
+ }
+
+ /** Returns a new [context] for accessing device protected storage. */
+ private fun getDeviceProtectedStorageContext(context: Context) =
+ context.takeIf {
+ ContextCompat.isDeviceProtectedStorage(it)
+ } ?: run { ContextCompat.createDeviceProtectedStorageContext(context) } ?: context
+
+ /** Gets the version code for a package. */
+ @Suppress("DEPRECATION")
+ private fun getVersionCode(context: Context): Long = try {
+ if (Build.VERSION.SDK_INT >= 33)
+ Api33Impl.getAppVersionCode(context)
+ else if (Build.VERSION.SDK_INT >= 28)
+ Api28Impl.getAppVersionCode(context)
+ else context.packageManager.getPackageInfo(context.packageName, 0).versionCode.toLong()
+ } catch (e: PackageManager.NameNotFoundException) {
+ // Default version to 1
+ 1
+ }
+
+ companion object {
+ @Volatile
+ private var instance: FileCache? = null
+
+ internal fun getInstance(context: Context): FileCache =
+ instance ?: synchronized(this) {
+ instance ?: FileCache(context).also { instance = it }
+ }
+
+ private const val EMOJI_PICKER_FOLDER = "emoji_picker"
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ internal object Api33Impl {
+ fun getAppVersionCode(context: Context) =
+ context.packageManager.getPackageInfo(
+ context.packageName,
+ PackageManager.PackageInfoFlags.of(0)
+ ).longVersionCode
+ }
+
+ @Suppress("DEPRECATION")
+ @RequiresApi(Build.VERSION_CODES.P)
+ internal object Api28Impl {
+ fun getAppVersionCode(context: Context) =
+ context.packageManager.getPackageInfo(
+ context.packageName,
+ /* flags= */ 0
+ ).longVersionCode
+ }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/UnicodeRenderableManager.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/UnicodeRenderableManager.kt
index 5f6386a..b8bd327 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/UnicodeRenderableManager.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/UnicodeRenderableManager.kt
@@ -30,6 +30,8 @@
private const val VARIATION_SELECTOR = "\uFE0F"
+ private const val YAWNING_FACE_EMOJI = "\uD83E\uDD71"
+
private val paint = TextPaint()
/**
@@ -64,6 +66,13 @@
EmojiCompat.get().getEmojiMatch(emoji, this) > 0
} ?: (getClosestRenderable(emoji) != null)
+ internal fun isEmoji12Supported(
+ emojiCompatMetaData: EmojiPickerView.EmojiCompatMetadata
+ ) =
+ // Yawning face is added in emoji 12 which is the first version starts to support gender
+ // inclusive emojis.
+ isEmojiRenderable(YAWNING_FACE_EMOJI, emojiCompatMetaData)
+
@VisibleForTesting
fun getClosestRenderable(emoji: String): String? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
diff --git a/glance/glance-appwidget/api/current.txt b/glance/glance-appwidget/api/current.txt
index 18f7836..bf3e575 100644
--- a/glance/glance-appwidget/api/current.txt
+++ b/glance/glance-appwidget/api/current.txt
@@ -25,9 +25,9 @@
public final class CheckBoxKt {
method @androidx.compose.runtime.Composable public static void CheckBox(boolean checked, androidx.glance.action.Action? onCheckedChange, optional androidx.glance.GlanceModifier modifier, optional String text, optional androidx.glance.text.TextStyle? style, optional androidx.glance.appwidget.CheckBoxColors colors, optional int maxLines);
- method public static androidx.glance.appwidget.CheckBoxColors CheckBoxColors(long checkedColor, long uncheckedColor);
- method public static androidx.glance.appwidget.CheckBoxColors CheckBoxColors(optional androidx.glance.unit.ColorProvider? checkedColor, optional androidx.glance.unit.ColorProvider? uncheckedColor);
- method public static androidx.glance.appwidget.CheckBoxColors CheckBoxColors(@ColorRes int checkBoxColor);
+ method @androidx.compose.runtime.Composable public static androidx.glance.appwidget.CheckBoxColors checkBoxColors(long checkedColor, long uncheckedColor);
+ method @androidx.compose.runtime.Composable public static androidx.glance.appwidget.CheckBoxColors checkBoxColors(androidx.glance.unit.ColorProvider checkedColor, androidx.glance.unit.ColorProvider uncheckedColor);
+ method @androidx.compose.runtime.Composable public static androidx.glance.appwidget.CheckBoxColors checkBoxColors();
}
public final class CircularProgressIndicatorKt {
@@ -124,9 +124,9 @@
public final class RadioButtonKt {
method @androidx.compose.runtime.Composable public static void RadioButton(boolean checked, androidx.glance.action.Action? onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional String text, optional androidx.glance.text.TextStyle? style, optional androidx.glance.appwidget.RadioButtonColors colors, optional int maxLines);
- method public static androidx.glance.appwidget.RadioButtonColors RadioButtonColors(androidx.glance.unit.ColorProvider? checkedColor, androidx.glance.unit.ColorProvider? uncheckedColor);
- method public static androidx.glance.appwidget.RadioButtonColors RadioButtonColors(optional @ColorRes int color);
- method public static androidx.glance.appwidget.RadioButtonColors RadioButtonColors(long checkedColor, long uncheckedColor);
+ method public static androidx.glance.appwidget.RadioButtonColors radioButtonColors(androidx.glance.unit.ColorProvider checkedColor, androidx.glance.unit.ColorProvider uncheckedColor);
+ method public static androidx.glance.appwidget.RadioButtonColors radioButtonColors(long checkedColor, long uncheckedColor);
+ method @androidx.compose.runtime.Composable public static androidx.glance.appwidget.RadioButtonColors radioButtonColors();
method public static androidx.glance.GlanceModifier selectableGroup(androidx.glance.GlanceModifier);
}
@@ -153,16 +153,10 @@
public abstract sealed class SwitchColors {
}
- public final class SwitchDefaults {
- method public androidx.glance.appwidget.SwitchColors getColors();
- property public final androidx.glance.appwidget.SwitchColors colors;
- field public static final androidx.glance.appwidget.SwitchDefaults INSTANCE;
- }
-
public final class SwitchKt {
method @androidx.compose.runtime.Composable public static void Switch(boolean checked, androidx.glance.action.Action? onCheckedChange, optional androidx.glance.GlanceModifier modifier, optional String text, optional androidx.glance.text.TextStyle? style, optional androidx.glance.appwidget.SwitchColors colors, optional int maxLines);
- method public static androidx.glance.appwidget.SwitchColors SwitchColors(optional androidx.glance.unit.ColorProvider? checkedThumbColor, optional androidx.glance.unit.ColorProvider? uncheckedThumbColor, optional androidx.glance.unit.ColorProvider? checkedTrackColor, optional androidx.glance.unit.ColorProvider? uncheckedTrackColor);
- method public static androidx.glance.appwidget.SwitchColors SwitchColors(@ColorRes int thumbColor, optional @ColorRes int trackColor);
+ method @androidx.compose.runtime.Composable public static androidx.glance.appwidget.SwitchColors switchColors(androidx.glance.unit.ColorProvider checkedThumbColor, androidx.glance.unit.ColorProvider uncheckedThumbColor, androidx.glance.unit.ColorProvider checkedTrackColor, androidx.glance.unit.ColorProvider uncheckedTrackColor);
+ method @androidx.compose.runtime.Composable public static androidx.glance.appwidget.SwitchColors switchColors();
}
public final class UtilsKt {
diff --git a/glance/glance-appwidget/api/public_plus_experimental_current.txt b/glance/glance-appwidget/api/public_plus_experimental_current.txt
index 1591d4d..6934f35 100644
--- a/glance/glance-appwidget/api/public_plus_experimental_current.txt
+++ b/glance/glance-appwidget/api/public_plus_experimental_current.txt
@@ -25,9 +25,9 @@
public final class CheckBoxKt {
method @androidx.compose.runtime.Composable public static void CheckBox(boolean checked, androidx.glance.action.Action? onCheckedChange, optional androidx.glance.GlanceModifier modifier, optional String text, optional androidx.glance.text.TextStyle? style, optional androidx.glance.appwidget.CheckBoxColors colors, optional int maxLines);
- method public static androidx.glance.appwidget.CheckBoxColors CheckBoxColors(long checkedColor, long uncheckedColor);
- method public static androidx.glance.appwidget.CheckBoxColors CheckBoxColors(optional androidx.glance.unit.ColorProvider? checkedColor, optional androidx.glance.unit.ColorProvider? uncheckedColor);
- method public static androidx.glance.appwidget.CheckBoxColors CheckBoxColors(@ColorRes int checkBoxColor);
+ method @androidx.compose.runtime.Composable public static androidx.glance.appwidget.CheckBoxColors checkBoxColors(long checkedColor, long uncheckedColor);
+ method @androidx.compose.runtime.Composable public static androidx.glance.appwidget.CheckBoxColors checkBoxColors(androidx.glance.unit.ColorProvider checkedColor, androidx.glance.unit.ColorProvider uncheckedColor);
+ method @androidx.compose.runtime.Composable public static androidx.glance.appwidget.CheckBoxColors checkBoxColors();
}
public final class CircularProgressIndicatorKt {
@@ -132,9 +132,9 @@
public final class RadioButtonKt {
method @androidx.compose.runtime.Composable public static void RadioButton(boolean checked, androidx.glance.action.Action? onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional String text, optional androidx.glance.text.TextStyle? style, optional androidx.glance.appwidget.RadioButtonColors colors, optional int maxLines);
- method public static androidx.glance.appwidget.RadioButtonColors RadioButtonColors(androidx.glance.unit.ColorProvider? checkedColor, androidx.glance.unit.ColorProvider? uncheckedColor);
- method public static androidx.glance.appwidget.RadioButtonColors RadioButtonColors(optional @ColorRes int color);
- method public static androidx.glance.appwidget.RadioButtonColors RadioButtonColors(long checkedColor, long uncheckedColor);
+ method public static androidx.glance.appwidget.RadioButtonColors radioButtonColors(androidx.glance.unit.ColorProvider checkedColor, androidx.glance.unit.ColorProvider uncheckedColor);
+ method public static androidx.glance.appwidget.RadioButtonColors radioButtonColors(long checkedColor, long uncheckedColor);
+ method @androidx.compose.runtime.Composable public static androidx.glance.appwidget.RadioButtonColors radioButtonColors();
method public static androidx.glance.GlanceModifier selectableGroup(androidx.glance.GlanceModifier);
}
@@ -167,16 +167,10 @@
public abstract sealed class SwitchColors {
}
- public final class SwitchDefaults {
- method public androidx.glance.appwidget.SwitchColors getColors();
- property public final androidx.glance.appwidget.SwitchColors colors;
- field public static final androidx.glance.appwidget.SwitchDefaults INSTANCE;
- }
-
public final class SwitchKt {
method @androidx.compose.runtime.Composable public static void Switch(boolean checked, androidx.glance.action.Action? onCheckedChange, optional androidx.glance.GlanceModifier modifier, optional String text, optional androidx.glance.text.TextStyle? style, optional androidx.glance.appwidget.SwitchColors colors, optional int maxLines);
- method public static androidx.glance.appwidget.SwitchColors SwitchColors(optional androidx.glance.unit.ColorProvider? checkedThumbColor, optional androidx.glance.unit.ColorProvider? uncheckedThumbColor, optional androidx.glance.unit.ColorProvider? checkedTrackColor, optional androidx.glance.unit.ColorProvider? uncheckedTrackColor);
- method public static androidx.glance.appwidget.SwitchColors SwitchColors(@ColorRes int thumbColor, optional @ColorRes int trackColor);
+ method @androidx.compose.runtime.Composable public static androidx.glance.appwidget.SwitchColors switchColors(androidx.glance.unit.ColorProvider checkedThumbColor, androidx.glance.unit.ColorProvider uncheckedThumbColor, androidx.glance.unit.ColorProvider checkedTrackColor, androidx.glance.unit.ColorProvider uncheckedTrackColor);
+ method @androidx.compose.runtime.Composable public static androidx.glance.appwidget.SwitchColors switchColors();
}
public final class UtilsKt {
diff --git a/glance/glance-appwidget/api/restricted_current.txt b/glance/glance-appwidget/api/restricted_current.txt
index 18f7836..bf3e575 100644
--- a/glance/glance-appwidget/api/restricted_current.txt
+++ b/glance/glance-appwidget/api/restricted_current.txt
@@ -25,9 +25,9 @@
public final class CheckBoxKt {
method @androidx.compose.runtime.Composable public static void CheckBox(boolean checked, androidx.glance.action.Action? onCheckedChange, optional androidx.glance.GlanceModifier modifier, optional String text, optional androidx.glance.text.TextStyle? style, optional androidx.glance.appwidget.CheckBoxColors colors, optional int maxLines);
- method public static androidx.glance.appwidget.CheckBoxColors CheckBoxColors(long checkedColor, long uncheckedColor);
- method public static androidx.glance.appwidget.CheckBoxColors CheckBoxColors(optional androidx.glance.unit.ColorProvider? checkedColor, optional androidx.glance.unit.ColorProvider? uncheckedColor);
- method public static androidx.glance.appwidget.CheckBoxColors CheckBoxColors(@ColorRes int checkBoxColor);
+ method @androidx.compose.runtime.Composable public static androidx.glance.appwidget.CheckBoxColors checkBoxColors(long checkedColor, long uncheckedColor);
+ method @androidx.compose.runtime.Composable public static androidx.glance.appwidget.CheckBoxColors checkBoxColors(androidx.glance.unit.ColorProvider checkedColor, androidx.glance.unit.ColorProvider uncheckedColor);
+ method @androidx.compose.runtime.Composable public static androidx.glance.appwidget.CheckBoxColors checkBoxColors();
}
public final class CircularProgressIndicatorKt {
@@ -124,9 +124,9 @@
public final class RadioButtonKt {
method @androidx.compose.runtime.Composable public static void RadioButton(boolean checked, androidx.glance.action.Action? onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional String text, optional androidx.glance.text.TextStyle? style, optional androidx.glance.appwidget.RadioButtonColors colors, optional int maxLines);
- method public static androidx.glance.appwidget.RadioButtonColors RadioButtonColors(androidx.glance.unit.ColorProvider? checkedColor, androidx.glance.unit.ColorProvider? uncheckedColor);
- method public static androidx.glance.appwidget.RadioButtonColors RadioButtonColors(optional @ColorRes int color);
- method public static androidx.glance.appwidget.RadioButtonColors RadioButtonColors(long checkedColor, long uncheckedColor);
+ method public static androidx.glance.appwidget.RadioButtonColors radioButtonColors(androidx.glance.unit.ColorProvider checkedColor, androidx.glance.unit.ColorProvider uncheckedColor);
+ method public static androidx.glance.appwidget.RadioButtonColors radioButtonColors(long checkedColor, long uncheckedColor);
+ method @androidx.compose.runtime.Composable public static androidx.glance.appwidget.RadioButtonColors radioButtonColors();
method public static androidx.glance.GlanceModifier selectableGroup(androidx.glance.GlanceModifier);
}
@@ -153,16 +153,10 @@
public abstract sealed class SwitchColors {
}
- public final class SwitchDefaults {
- method public androidx.glance.appwidget.SwitchColors getColors();
- property public final androidx.glance.appwidget.SwitchColors colors;
- field public static final androidx.glance.appwidget.SwitchDefaults INSTANCE;
- }
-
public final class SwitchKt {
method @androidx.compose.runtime.Composable public static void Switch(boolean checked, androidx.glance.action.Action? onCheckedChange, optional androidx.glance.GlanceModifier modifier, optional String text, optional androidx.glance.text.TextStyle? style, optional androidx.glance.appwidget.SwitchColors colors, optional int maxLines);
- method public static androidx.glance.appwidget.SwitchColors SwitchColors(optional androidx.glance.unit.ColorProvider? checkedThumbColor, optional androidx.glance.unit.ColorProvider? uncheckedThumbColor, optional androidx.glance.unit.ColorProvider? checkedTrackColor, optional androidx.glance.unit.ColorProvider? uncheckedTrackColor);
- method public static androidx.glance.appwidget.SwitchColors SwitchColors(@ColorRes int thumbColor, optional @ColorRes int trackColor);
+ method @androidx.compose.runtime.Composable public static androidx.glance.appwidget.SwitchColors switchColors(androidx.glance.unit.ColorProvider checkedThumbColor, androidx.glance.unit.ColorProvider uncheckedThumbColor, androidx.glance.unit.ColorProvider checkedTrackColor, androidx.glance.unit.ColorProvider uncheckedTrackColor);
+ method @androidx.compose.runtime.Composable public static androidx.glance.appwidget.SwitchColors switchColors();
}
public final class UtilsKt {
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/CompoundButtonAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/CompoundButtonAppWidget.kt
index e8739cb..eb0d0c2 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/CompoundButtonAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/CompoundButtonAppWidget.kt
@@ -28,14 +28,14 @@
import androidx.glance.action.ActionParameters
import androidx.glance.action.actionParametersOf
import androidx.glance.appwidget.CheckBox
-import androidx.glance.appwidget.CheckBoxColors
+import androidx.glance.appwidget.checkBoxColors
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.glance.appwidget.RadioButton
-import androidx.glance.appwidget.RadioButtonColors
+import androidx.glance.appwidget.radioButtonColors
import androidx.glance.appwidget.SizeMode
import androidx.glance.appwidget.Switch
-import androidx.glance.appwidget.SwitchColors
+import androidx.glance.appwidget.switchColors
import androidx.glance.appwidget.selectableGroup
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.action.ToggleableStateKey
@@ -114,7 +114,7 @@
text = "Checkbox 2",
style = textStyle,
modifier = fillModifier,
- colors = CheckBoxColors(
+ colors = checkBoxColors(
checkedColor = ColorProvider(day = Color.Red, night = Color.Cyan),
uncheckedColor = ColorProvider(day = Color.Green, night = Color.Magenta)
)
@@ -125,7 +125,6 @@
actionParametersOf(EventTargetKey to Buttons.CHECK_3.name)
),
text = "Checkbox 3",
- colors = CheckBoxColors(R.color.my_checkbox_colors)
)
Switch(
checked = switch1Checked,
@@ -133,7 +132,7 @@
actionParametersOf(EventTargetKey to Buttons.SWITCH_1.name)
),
text = "Switch 1",
- colors = SwitchColors(
+ colors = switchColors(
checkedThumbColor = ColorProvider(day = Color.Red, night = Color.Cyan),
uncheckedThumbColor = ColorProvider(day = Color.Green, night = Color.Magenta),
checkedTrackColor = ColorProvider(day = Color.Blue, night = Color.Yellow),
@@ -156,7 +155,7 @@
actionParametersOf(EventTargetKey to Radios.RADIO_1.name)
),
text = "Radio 1",
- colors = RadioButtonColors(
+ colors = radioButtonColors(
checkedColor = ColorProvider(day = Color.Red, night = Color.Cyan),
uncheckedColor = ColorProvider(day = Color.Green, night = Color.Magenta)
),
@@ -167,7 +166,7 @@
actionParametersOf(EventTargetKey to Radios.RADIO_2.name)
),
text = "Radio 2",
- colors = RadioButtonColors(
+ colors = radioButtonColors(
checkedColor = ColorProvider(day = Color.Cyan, night = Color.Yellow),
uncheckedColor = ColorProvider(day = Color.Red, night = Color.Blue)
),
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/GlanceAppWidgetDemoActivity.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/GlanceAppWidgetDemoActivity.kt
index fb88455..801f260 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/GlanceAppWidgetDemoActivity.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/GlanceAppWidgetDemoActivity.kt
@@ -80,7 +80,8 @@
if (!GlanceAppWidgetReceiver::class.java.isAssignableFrom(receiverClass)) {
return@mapNotNull null
}
- val receiver = receiverClass.newInstance() as GlanceAppWidgetReceiver
+ val receiver = receiverClass.getDeclaredConstructor()
+ .newInstance() as GlanceAppWidgetReceiver
val provider = receiver.glanceAppWidget.javaClass
ProviderData(
provider = provider,
@@ -112,7 +113,8 @@
scope.launch {
manager.requestPinGlanceAppWidget(
receiver = it.receiver,
- preview = it.provider.newInstance(),
+ preview = it.provider.getDeclaredConstructor()
+ .newInstance(),
previewState = emptyPreferences()
)
}
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt
index 75e246a..2ffe9a5 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt
@@ -224,7 +224,7 @@
}
@Test
- fun checkButtonTextAlignement() {
+ fun checkButtonTextAlignment() {
TestGlanceAppWidget.uiDefinition = {
Column(modifier = GlanceModifier.fillMaxSize()) {
Row(modifier = GlanceModifier.defaultWeight().fillMaxWidth()) {
@@ -636,8 +636,9 @@
fontWeight = FontWeight.Bold,
fontStyle = FontStyle.Normal,
),
- colors = CheckBoxColors(
- checkedColor = ColorProvider(day = Color.Magenta, night = Color.Yellow)
+ colors = checkBoxColors(
+ checkedColor = ColorProvider(day = Color.Magenta, night = Color.Yellow),
+ uncheckedColor = ColorProvider(day = Color.Black, night = Color.Gray)
)
)
@@ -651,7 +652,7 @@
fontWeight = FontWeight.Medium,
fontStyle = FontStyle.Italic,
),
- colors = CheckBoxColors(checkedColor = Color.Red, uncheckedColor = Color.Green)
+ colors = checkBoxColors(checkedColor = Color.Red, uncheckedColor = Color.Green)
)
}
}
@@ -668,21 +669,29 @@
fontWeight = FontWeight.Bold,
fontStyle = FontStyle.Normal,
),
- colors = SwitchColors(
+ colors = switchColors(
checkedThumbColor = ColorProvider(day = Color.Blue, night = Color.Red),
+ uncheckedThumbColor = ColorProvider(Color.Magenta),
checkedTrackColor = ColorProvider(day = Color.Green, night = Color.Yellow),
+ uncheckedTrackColor = ColorProvider(Color.Magenta)
)
)
Switch(
checked = false,
onCheckedChange = null,
- text = "Hello Unchecked Switch",
+ text = "Hello Unchecked Switch. day: thumb magenta / track cyan, night: thumb cyan",
style = TextStyle(
color = ColorProvider(day = Color.Black, night = Color.White),
textDecoration = TextDecoration.Underline,
fontWeight = FontWeight.Medium,
fontStyle = FontStyle.Italic,
+ ),
+ colors = switchColors(
+ checkedThumbColor = ColorProvider(Color.Blue),
+ uncheckedThumbColor = ColorProvider(day = Color.Magenta, night = Color.Cyan),
+ checkedTrackColor = ColorProvider(Color.Blue),
+ uncheckedTrackColor = ColorProvider(day = Color.Cyan, night = Color.Magenta)
)
)
}
@@ -703,7 +712,7 @@
fontWeight = FontWeight.Bold,
fontStyle = FontStyle.Normal,
),
- colors = RadioButtonColors(
+ colors = radioButtonColors(
checkedColor = ColorProvider(day = Color.Magenta, night = Color.Yellow),
uncheckedColor = ColorProvider(day = Color.Yellow, night = Color.Magenta)
)
@@ -719,7 +728,7 @@
fontWeight = FontWeight.Medium,
fontStyle = FontStyle.Italic,
),
- colors = RadioButtonColors(checkedColor = Color.Red, uncheckedColor = Color.Green)
+ colors = radioButtonColors(checkedColor = Color.Red, uncheckedColor = Color.Green)
)
}
}
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
index d307271..20c2755 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
@@ -480,7 +480,8 @@
TestGlanceAppWidget.uiDefinition = {
val context = LocalContext.current
val bitmap =
- (context.resources.getDrawable(R.drawable.compose, null) as BitmapDrawable).bitmap
+ (context.resources.getDrawable(R.drawable.compose, null) as BitmapDrawable)
+ .bitmap
Text(
"Some useful text",
modifier = GlanceModifier.fillMaxSize()
@@ -636,13 +637,17 @@
Text(
"text1",
modifier = GlanceModifier.clickable(
- actionRunCallback<CallbackTest>(actionParametersOf(CallbackTest.key to 1))
+ actionRunCallback<CallbackTest>(
+ actionParametersOf(CallbackTest.key to 1)
+ )
)
)
Text(
"text2",
modifier = GlanceModifier.clickable(
- actionRunCallback<CallbackTest>(actionParametersOf(CallbackTest.key to 2))
+ actionRunCallback<CallbackTest>(
+ actionParametersOf(CallbackTest.key to 2)
+ )
)
)
}
@@ -653,9 +658,13 @@
CallbackTest.received.set(emptyList())
CallbackTest.latch = CountDownLatch(2)
mHostRule.onHostView { root ->
- checkNotNull(root.findChild<TextView> { it.text == "text1" }?.parent as? View)
+ checkNotNull(
+ root.findChild<TextView> { it.text.toString() == "text1" }?.parent as? View
+ )
.performClick()
- checkNotNull(root.findChild<TextView> { it.text == "text2" }?.parent as? View)
+ checkNotNull(
+ root.findChild<TextView> { it.text.toString() == "text2" }?.parent as? View
+ )
.performClick()
}
assertThat(CallbackTest.latch.await(5, TimeUnit.SECONDS)).isTrue()
@@ -668,9 +677,13 @@
Text(
"text1",
modifier = GlanceModifier.clickable(
- actionRunCallback<CallbackTest>(actionParametersOf(CallbackTest.key to 1))
+ actionRunCallback<CallbackTest>(
+ actionParametersOf(CallbackTest.key to 1)
+ )
).clickable(
- actionRunCallback<CallbackTest>(actionParametersOf(CallbackTest.key to 2))
+ actionRunCallback<CallbackTest>(
+ actionParametersOf(CallbackTest.key to 2)
+ )
)
)
}
@@ -680,7 +693,9 @@
CallbackTest.received.set(emptyList())
CallbackTest.latch = CountDownLatch(1)
mHostRule.onHostView { root ->
- checkNotNull(root.findChild<TextView> { it.text == "text1" }?.parent as? View)
+ checkNotNull(
+ root.findChild<TextView> { it.text.toString() == "text1" }?.parent as? View
+ )
.performClick()
}
assertThat(CallbackTest.latch.await(5, TimeUnit.SECONDS)).isTrue()
@@ -710,12 +725,12 @@
val targetHeight = (column.height.pixelsToDp(displayMetrics) - 16.dp) / 2
val targetWidth = column.width.pixelsToDp(displayMetrics) - 16.dp
- val text1 = checkNotNull(column.findChild<TextView> { it.text == "Text 1" })
+ val text1 = checkNotNull(column.findChild<TextView> { it.text.toString() == "Text 1" })
val row1 = text1.getParentView<FrameLayout>().getParentView<LinearLayout>()
assertThat(row1.orientation).isEqualTo(LinearLayout.HORIZONTAL)
assertViewSize(row1, DpSize(targetWidth, targetHeight))
- val text2 = checkNotNull(column.findChild<TextView> { it.text == "Text 2" })
+ val text2 = checkNotNull(column.findChild<TextView> { it.text.toString() == "Text 2" })
val row2 = text2.getParentView<FrameLayout>().getParentView<LinearLayout>()
assertThat(row2.orientation).isEqualTo(LinearLayout.HORIZONTAL)
assertThat(row2.height).isGreaterThan(20.dp.toPixels(context))
@@ -752,9 +767,9 @@
CompoundButtonActionTest.received.set(emptyList())
CompoundButtonActionTest.latch = CountDownLatch(2)
mHostRule.onHostView { root ->
- checkNotNull(root.findChild<TextView> { it.text == checkbox })
+ checkNotNull(root.findChild<TextView> { it.text.toString() == checkbox })
.performCompoundButtonClick()
- checkNotNull(root.findChild<TextView> { it.text == switch })
+ checkNotNull(root.findChild<TextView> { it.text.toString() == switch })
.performCompoundButtonClick()
}
CompoundButtonActionTest.latch.await(5, TimeUnit.SECONDS)
@@ -764,6 +779,44 @@
}
@Test
+ fun canCreateCheckableColorProvider() {
+ TestGlanceAppWidget.uiDefinition = {
+ Switch(
+ checked = true,
+ onCheckedChange = null,
+ text = "Hello Checked Switch (day: Blue/Green, night: Red/Yellow)",
+ style = TextStyle(
+ color = androidx.glance.appwidget.unit.ColorProvider(
+ day = Color.Black,
+ night = Color.White
+ ),
+ fontWeight = FontWeight.Bold,
+ fontStyle = FontStyle.Normal,
+ ),
+ colors = switchColors(
+ checkedThumbColor = androidx.glance.appwidget.unit.ColorProvider(
+ day = Color.Blue,
+ night = Color.Red
+ ),
+ checkedTrackColor = androidx.glance.appwidget.unit.ColorProvider(
+ day = Color.Green,
+ night = Color.Yellow
+ ),
+ uncheckedThumbColor = ColorProvider(Color.Magenta),
+ uncheckedTrackColor = ColorProvider(Color.Magenta),
+ )
+ )
+ }
+
+ mHostRule.startHost()
+ runBlocking {
+ mHostRule.updateAppWidget()
+ }
+
+ // if no crash, we're good
+ }
+
+ @Test
fun radioActionCallback() {
TestGlanceAppWidget.uiDefinition = {
RadioButton(
@@ -780,7 +833,7 @@
CallbackTest.received.set(emptyList())
CallbackTest.latch = CountDownLatch(1)
mHostRule.onHostView { root ->
- checkNotNull(root.findChild<TextView> { it.text == "text1" })
+ checkNotNull(root.findChild<TextView> { it.text.toString() == "text1" })
.performCompoundButtonClick()
}
assertThat(CallbackTest.latch.await(5, TimeUnit.SECONDS)).isTrue()
@@ -795,7 +848,9 @@
"text1",
modifier = if (enabled) {
GlanceModifier.clickable(
- actionRunCallback<CallbackTest>(actionParametersOf(CallbackTest.key to 1))
+ actionRunCallback<CallbackTest>(
+ actionParametersOf(CallbackTest.key to 1)
+ )
)
} else GlanceModifier
)
@@ -805,7 +860,9 @@
mHostRule.onHostView { root ->
val view =
- checkNotNull(root.findChild<TextView> { it.text == "text1" }?.parent as? View)
+ checkNotNull(
+ root.findChild<TextView> { it.text.toString() == "text1" }?.parent as? View
+ )
assertThat(view.hasOnClickListeners()).isTrue()
}
@@ -818,7 +875,9 @@
mHostRule.onHostView { root ->
val view =
- checkNotNull(root.findChild<TextView> { it.text == "text1" }?.parent as? View)
+ checkNotNull(
+ root.findChild<TextView> { it.text.toString() == "text1" }?.parent as? View
+ )
assertThat(view.hasOnClickListeners()).isFalse()
}
}
@@ -843,7 +902,7 @@
CompoundButtonActionTest.received.set(emptyList())
CompoundButtonActionTest.latch = CountDownLatch(1)
mHostRule.onHostView { root ->
- checkNotNull(root.findChild<TextView> { it.text == "checkbox" })
+ checkNotNull(root.findChild<TextView> { it.text.toString() == "checkbox" })
.performCompoundButtonClick()
}
CompoundButtonActionTest.latch.await(5, TimeUnit.SECONDS)
@@ -861,7 +920,7 @@
CompoundButtonActionTest.received.set(emptyList())
CompoundButtonActionTest.latch = CountDownLatch(1)
mHostRule.onHostView { root ->
- checkNotNull(root.findChild<TextView> { it.text == "checkbox" })
+ checkNotNull(root.findChild<TextView> { it.text.toString() == "checkbox" })
.performCompoundButtonClick()
}
assertThat(CompoundButtonActionTest.latch.await(5, TimeUnit.SECONDS)).isFalse()
@@ -889,7 +948,8 @@
mHostRule.startHost()
mHostRule.onHostView { root ->
- val checkbox = checkNotNull(root.findChild<CompoundButton> { it.text == "checkbox" })
+ val checkbox =
+ checkNotNull(root.findChild<CompoundButton> { it.text.toString() == "checkbox" })
assertThat(checkbox.hasOnClickListeners()).isFalse()
}
}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/CheckBox.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/CheckBox.kt
index a09f2f3..d27cadf 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/CheckBox.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/CheckBox.kt
@@ -16,18 +16,19 @@
package androidx.glance.appwidget
-import androidx.annotation.ColorRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.glance.Emittable
import androidx.glance.GlanceModifier
import androidx.glance.GlanceNode
+import androidx.glance.GlanceTheme
import androidx.glance.action.Action
import androidx.glance.action.ActionModifier
import androidx.glance.appwidget.action.CompoundButtonAction
import androidx.glance.appwidget.unit.CheckableColorProvider
import androidx.glance.appwidget.unit.CheckedUncheckedColorProvider.Companion.createCheckableColorProvider
import androidx.glance.appwidget.unit.ResourceCheckableColorProvider
+import androidx.glance.color.DynamicThemeColorProviders
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
import androidx.glance.unit.FixedColorProvider
@@ -42,17 +43,18 @@
) : CheckBoxColors()
/**
- * [CheckBoxColors] that uses [checkedColor] or [uncheckedColor] depending ons the checked state of the
+ * [checkBoxColors] that uses [checkedColor] or [uncheckedColor] depending ons the checked state of the
* CheckBox.
*
* @param checkedColor the [Color] to use when the CheckBox is checked
* @param uncheckedColor the [Color] to use when the CheckBox is not checked
*/
-fun CheckBoxColors(checkedColor: Color, uncheckedColor: Color): CheckBoxColors =
- CheckBoxColors(FixedColorProvider(checkedColor), FixedColorProvider(uncheckedColor))
+@Composable
+fun checkBoxColors(checkedColor: Color, uncheckedColor: Color): CheckBoxColors =
+ checkBoxColors(FixedColorProvider(checkedColor), FixedColorProvider(uncheckedColor))
/**
- * [CheckBoxColors] that uses [checkedColor] or [uncheckedColor] depending on the checked state of
+ * [checkBoxColors] that uses [checkedColor] or [uncheckedColor] depending on the checked state of
* the CheckBox.
*
* None of the [ColorProvider] parameters to this function can be created from resource ids. To use
@@ -63,35 +65,35 @@
* @param uncheckedColor the [ColorProvider] to use when the check box is not checked, or null to
* use the default tint
*/
-fun CheckBoxColors(
- checkedColor: ColorProvider? = null,
- uncheckedColor: ColorProvider? = null
+@Composable
+fun checkBoxColors(
+ checkedColor: ColorProvider,
+ uncheckedColor: ColorProvider
): CheckBoxColors =
CheckBoxColorsImpl(
createCheckableColorProvider(
source = "CheckBoxColors",
checked = checkedColor,
unchecked = uncheckedColor,
- fallback = R.color.glance_default_check_box
)
)
-/**
- * [CheckBoxColors] set to the color resource [checkBoxColor].
- *
- * This may be a fixed color or a color selector that selects color depending on
- * [android.R.attr.state_checked].
- *
- * @param checkBoxColor the resource to use to tint the check box. If an invalid resource id is
- * provided, the default check box colors will be used.
- */
-fun CheckBoxColors(@ColorRes checkBoxColor: Int): CheckBoxColors =
- CheckBoxColorsImpl(
- ResourceCheckableColorProvider(
- resId = checkBoxColor,
- fallback = R.color.glance_default_check_box
+@Composable
+fun checkBoxColors(): CheckBoxColors {
+ val colorProvider = if (GlanceTheme.colors == DynamicThemeColorProviders) {
+ // If using the m3 dynamic color theme, we need to create a color provider from xml
+ // because resource backed ColorStateLists cannot be created programmatically
+ ResourceCheckableColorProvider(R.color.glance_default_check_box)
+ } else {
+ createCheckableColorProvider(
+ source = "CheckBoxColors",
+ checked = GlanceTheme.colors.primary,
+ unchecked = GlanceTheme.colors.onSurface
)
- )
+ }
+
+ return CheckBoxColorsImpl(colorProvider)
+}
/**
* Adds a check box view to the glance view.
@@ -115,7 +117,7 @@
modifier: GlanceModifier = GlanceModifier,
text: String = "",
style: TextStyle? = null,
- colors: CheckBoxColors = CheckBoxColors(),
+ colors: CheckBoxColors = checkBoxColors(),
maxLines: Int = Int.MAX_VALUE,
) {
val finalModifier = if (onCheckedChange != null) {
@@ -124,7 +126,7 @@
modifier
}
GlanceNode(
- factory = ::EmittableCheckBox,
+ factory = { EmittableCheckBox(colors) },
update = {
this.set(checked) { this.checked = it }
this.set(text) { this.text = it }
@@ -136,20 +138,20 @@
)
}
-internal class EmittableCheckBox : Emittable {
+internal class EmittableCheckBox(
+ var colors: CheckBoxColors
+) : Emittable {
override var modifier: GlanceModifier = GlanceModifier
var checked: Boolean = false
var text: String = ""
var style: TextStyle? = null
- var colors: CheckBoxColors = CheckBoxColors()
var maxLines: Int = Int.MAX_VALUE
- override fun copy(): Emittable = EmittableCheckBox().also {
+ override fun copy(): Emittable = EmittableCheckBox(colors = colors).also {
it.modifier = modifier
it.checked = checked
it.text = text
it.style = style
- it.colors = colors
it.maxLines = maxLines
}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RadioButton.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RadioButton.kt
index 006f600..e4d818e 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RadioButton.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RadioButton.kt
@@ -16,17 +16,18 @@
package androidx.glance.appwidget
-import androidx.annotation.ColorRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.glance.Emittable
import androidx.glance.GlanceModifier
import androidx.glance.GlanceNode
+import androidx.glance.GlanceTheme
import androidx.glance.action.Action
import androidx.glance.action.clickable
import androidx.glance.appwidget.unit.CheckableColorProvider
import androidx.glance.appwidget.unit.CheckedUncheckedColorProvider.Companion.createCheckableColorProvider
import androidx.glance.appwidget.unit.ResourceCheckableColorProvider
+import androidx.glance.color.DynamicThemeColorProviders
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
import androidx.glance.unit.FixedColorProvider
@@ -45,65 +46,60 @@
* @param uncheckedColor the tint to apply to the radio button when it is not checked,
* or null to use the default tint.
*/
-fun RadioButtonColors(
- checkedColor: ColorProvider?,
- uncheckedColor: ColorProvider?,
+fun radioButtonColors(
+ checkedColor: ColorProvider,
+ uncheckedColor: ColorProvider,
): RadioButtonColors {
return RadioButtonColors(
radio = createCheckableColorProvider(
- source = "RadioButtonColors",
- checked = checkedColor,
- unchecked = uncheckedColor,
- fallback = R.color.glance_default_radio_button
+ source = "RadioButtonColors", checked = checkedColor, unchecked = uncheckedColor
)
)
}
/**
- * [RadioButtonColors] set to color resources.
- *
- * These may be fixed colors or a color selector that selects color depending on
- * [android.R.attr.state_checked].
- *
- * @param color the resource to use to tint the radio button. If an invalid resource id is provided,
- * the default colors will be used.
- */
-fun RadioButtonColors(
- @ColorRes color: Int = R.color.glance_default_radio_button
-): RadioButtonColors =
- RadioButtonColors(
- radio = ResourceCheckableColorProvider(
- resId = color,
- fallback = R.color.glance_default_radio_button
- )
- )
-
-/**
- * [RadioButtonColors] that uses [checkedColor] or [uncheckedColor] depending on the checked state
+ * [radioButtonColors] that uses [checkedColor] or [uncheckedColor] depending on the checked state
* of the RadioButton.
*
* @param checkedColor the [Color] to use when the RadioButton is checked
* @param uncheckedColor the [Color] to use when the RadioButton is not checked
*/
-fun RadioButtonColors(checkedColor: Color, uncheckedColor: Color): RadioButtonColors =
- RadioButtonColors(FixedColorProvider(checkedColor), FixedColorProvider(uncheckedColor))
+fun radioButtonColors(checkedColor: Color, uncheckedColor: Color): RadioButtonColors =
+ radioButtonColors(FixedColorProvider(checkedColor), FixedColorProvider(uncheckedColor))
-internal class EmittableRadioButton : Emittable {
+@Composable
+fun radioButtonColors(): RadioButtonColors {
+ val colorProvider = if (GlanceTheme.colors == DynamicThemeColorProviders) {
+ // If using the m3 dynamic color theme, we need to create a color provider from xml
+ // because resource backed ColorStateLists cannot be created programmatically
+ ResourceCheckableColorProvider(R.color.glance_default_radio_button)
+ } else {
+ createCheckableColorProvider(
+ source = "CheckBoxColors",
+ checked = GlanceTheme.colors.primary,
+ unchecked = GlanceTheme.colors.onSurfaceVariant
+ )
+ }
+
+ return RadioButtonColors(colorProvider)
+}
+
+internal class EmittableRadioButton(
+ var colors: RadioButtonColors
+) : Emittable {
override var modifier: GlanceModifier = GlanceModifier
var checked: Boolean = false
var enabled: Boolean = true
var text: String = ""
var style: TextStyle? = null
- var colors: RadioButtonColors = RadioButtonColors()
var maxLines: Int = Int.MAX_VALUE
- override fun copy(): Emittable = EmittableRadioButton().also {
+ override fun copy(): Emittable = EmittableRadioButton(colors = colors).also {
it.modifier = modifier
it.checked = checked
it.enabled = enabled
it.text = text
it.style = style
- it.colors = colors
it.maxLines = maxLines
}
@@ -144,22 +140,19 @@
enabled: Boolean = true,
text: String = "",
style: TextStyle? = null,
- colors: RadioButtonColors = RadioButtonColors(),
+ colors: RadioButtonColors = radioButtonColors(),
maxLines: Int = Int.MAX_VALUE,
) {
val finalModifier = if (enabled && onClick != null) modifier.clickable(onClick) else modifier
- GlanceNode(
- factory = ::EmittableRadioButton,
- update = {
- this.set(checked) { this.checked = it }
- this.set(finalModifier) { this.modifier = it }
- this.set(enabled) { this.enabled = it }
- this.set(text) { this.text = it }
- this.set(style) { this.style = it }
- this.set(colors) { this.colors = it }
- this.set(maxLines) { this.maxLines = it }
- }
- )
+ GlanceNode(factory = { EmittableRadioButton(colors) }, update = {
+ this.set(checked) { this.checked = it }
+ this.set(finalModifier) { this.modifier = it }
+ this.set(enabled) { this.enabled = it }
+ this.set(text) { this.text = it }
+ this.set(style) { this.style = it }
+ this.set(colors) { this.colors = it }
+ this.set(maxLines) { this.maxLines = it }
+ })
}
/**
@@ -170,8 +163,7 @@
* another is selected. When this modifier is used, an error will be thrown if more than one
* RadioButton has their "checked" value set to true.
*/
-fun GlanceModifier.selectableGroup(): GlanceModifier =
- this.then(SelectableGroupModifier)
+fun GlanceModifier.selectableGroup(): GlanceModifier = this.then(SelectableGroupModifier)
internal object SelectableGroupModifier : GlanceModifier.Element
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/Switch.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/Switch.kt
index bec7ff6..cdc36f6 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/Switch.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/Switch.kt
@@ -16,17 +16,18 @@
package androidx.glance.appwidget
-import androidx.annotation.ColorRes
import androidx.compose.runtime.Composable
import androidx.glance.Emittable
import androidx.glance.GlanceModifier
import androidx.glance.GlanceNode
+import androidx.glance.GlanceTheme
import androidx.glance.action.Action
import androidx.glance.action.ActionModifier
import androidx.glance.appwidget.action.CompoundButtonAction
import androidx.glance.appwidget.unit.CheckableColorProvider
import androidx.glance.appwidget.unit.CheckedUncheckedColorProvider.Companion.createCheckableColorProvider
import androidx.glance.appwidget.unit.ResourceCheckableColorProvider
+import androidx.glance.color.DynamicThemeColorProviders
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
@@ -44,70 +45,52 @@
/**
* SwitchColors to tint the thumb and track of the [Switch] according to the checked state.
*
- * None of the [ColorProvider] parameters to this function can be created from resource ids. To use
- * resources to tint the switch color, use `SwitchColors(Int, Int)` instead.
+ * None of the [ColorProvider] parameters to this function can be created from resource ids.
*
- * @param checkedThumbColor the tint to apply to the thumb of the switch when it is checked, or null
- * to use the default tint
- * @param uncheckedThumbColor the tint to apply to the thumb of the switch when it is not checked,
- * or null to use the default tint
- * @param checkedTrackColor the tint to apply to the track of the switch when it is checked, or null
- * to use the default tints
- * @param uncheckedTrackColor the tint to apply to the track of the switch when it is not checked,
- * or null to use the default tint
+ * @param checkedThumbColor the tint to apply to the thumb of the switch when it is checked
+ * @param uncheckedThumbColor the tint to apply to the thumb of the switch when it is not checked
+ * @param checkedTrackColor the tint to apply to the track of the switch when it is checked
+ * @param uncheckedTrackColor the tint to apply to the track of the switch when it is not checked
*/
-fun SwitchColors(
- checkedThumbColor: ColorProvider? = null,
- uncheckedThumbColor: ColorProvider? = null,
- checkedTrackColor: ColorProvider? = null,
- uncheckedTrackColor: ColorProvider? = null,
+@Composable
+fun switchColors(
+ checkedThumbColor: ColorProvider,
+ uncheckedThumbColor: ColorProvider,
+ checkedTrackColor: ColorProvider,
+ uncheckedTrackColor: ColorProvider,
): SwitchColors {
return SwitchColorsImpl(
thumb = createCheckableColorProvider(
source = "SwitchColors",
checked = checkedThumbColor,
unchecked = uncheckedThumbColor,
- fallback = R.color.glance_default_switch_thumb
),
track = createCheckableColorProvider(
source = "SwitchColors",
checked = checkedTrackColor,
unchecked = uncheckedTrackColor,
- fallback = R.color.glance_default_switch_track
)
)
}
/**
- * [SwitchColors] set to color resources.
- *
- * These may be fixed colors or a color selector that selects color depending on
- * [android.R.attr.state_checked].
- *
- * @param thumbColor the resource to use to tint the thumb. If an invalid resource id is provided,
- * the default switch colors will be used.
- * @param trackColor the resource to use to tint the track. If an invalid resource id is provided,
- * the default switch colors will be used.
+ * Create a default set of SwitchColors.
*/
-fun SwitchColors(
- @ColorRes thumbColor: Int,
- @ColorRes trackColor: Int = R.color.glance_default_switch_track
-): SwitchColors =
- SwitchColorsImpl(
- thumb = ResourceCheckableColorProvider(
- resId = thumbColor,
- fallback = R.color.glance_default_switch_thumb
- ),
- track = ResourceCheckableColorProvider(
- resId = trackColor,
- fallback = R.color.glance_default_switch_track
+@Composable
+fun switchColors(): SwitchColors {
+ return if (GlanceTheme.colors == DynamicThemeColorProviders) {
+ SwitchColorsImpl(
+ thumb = ResourceCheckableColorProvider(R.color.glance_default_switch_thumb),
+ track = ResourceCheckableColorProvider(R.color.glance_default_switch_track)
)
- )
-
-/** Defaults for the [Switch]. */
-object SwitchDefaults {
- /** Default [SwitchColors] to apply. */
- val colors: SwitchColors = SwitchColors()
+ } else {
+ switchColors(
+ checkedThumbColor = GlanceTheme.colors.onPrimary,
+ uncheckedThumbColor = GlanceTheme.colors.outline,
+ checkedTrackColor = GlanceTheme.colors.primary,
+ uncheckedTrackColor = GlanceTheme.colors.surfaceVariant,
+ )
+ }
}
/**
@@ -132,7 +115,7 @@
modifier: GlanceModifier = GlanceModifier,
text: String = "",
style: TextStyle? = null,
- colors: SwitchColors = SwitchDefaults.colors,
+ colors: SwitchColors = switchColors(),
maxLines: Int = Int.MAX_VALUE,
) {
val finalModifier = if (onCheckedChange != null) {
@@ -141,7 +124,7 @@
modifier
}
GlanceNode(
- factory = ::EmittableSwitch,
+ factory = { EmittableSwitch(colors) },
update = {
this.set(checked) { this.checked = it }
this.set(text) { this.text = it }
@@ -149,24 +132,23 @@
this.set(style) { this.style = it }
this.set(colors) { this.colors = it }
this.set(maxLines) { this.maxLines = it }
- }
- )
+ })
}
-internal class EmittableSwitch : Emittable {
+internal class EmittableSwitch(
+ var colors: SwitchColors
+) : Emittable {
override var modifier: GlanceModifier = GlanceModifier
var checked: Boolean = false
var text: String = ""
var style: TextStyle? = null
- var colors: SwitchColors = SwitchDefaults.colors
var maxLines: Int = Int.MAX_VALUE
- override fun copy(): Emittable = EmittableSwitch().also {
+ override fun copy(): Emittable = EmittableSwitch(colors = colors).also {
it.modifier = modifier
it.checked = checked
it.text = text
it.style = style
- it.colors = colors
it.maxLines = maxLines
}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/RunCallbackAction.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/RunCallbackAction.kt
index 229bb4d..5aeeade 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/RunCallbackAction.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/RunCallbackAction.kt
@@ -40,7 +40,7 @@
error("Provided class must implement ActionCallback.")
}
- val actionCallback = workClass.newInstance() as ActionCallback
+ val actionCallback = workClass.getDeclaredConstructor().newInstance() as ActionCallback
actionCallback.onAction(context, glanceId, parameters)
}
}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslator.kt
index 2806c87..06c9f14 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslator.kt
@@ -63,16 +63,14 @@
is ResourceCheckableColorProvider -> {
setCompoundButtonTintList(checkBoxId, colors.resId)
}
- }
+ }.let { }
} else {
val iconId = inflateViewStub(translationContext, R.id.checkBoxIcon)
textViewId = inflateViewStub(translationContext, R.id.checkBoxText)
actionTargetId = viewDef.mainViewId
setViewEnabled(iconId, element.checked)
- setImageViewColorFilter(
- iconId,
- element.colors.checkBox.getColor(translationContext.context, element.checked)
- )
+ val color = element.colors.checkBox.getColor(translationContext.context, element.checked)
+ setImageViewColorFilter(iconId, color)
}
setText(
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/CompoundButtonTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/CompoundButtonTranslator.kt
index 5cdca85..9b167e2 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/CompoundButtonTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/CompoundButtonTranslator.kt
@@ -29,6 +29,8 @@
import androidx.glance.appwidget.unit.isNightMode
import androidx.glance.appwidget.unit.resolveCheckedColor
+internal val checkableColorProviderFallbackColor = Color.Black
+
private fun CheckedUncheckedColorProvider.toColorStateList(
context: Context,
isNightMode: Boolean
@@ -59,14 +61,13 @@
)
}
-internal fun CheckableColorProvider.getColor(context: Context, isChecked: Boolean) = when (this) {
- is CheckedUncheckedColorProvider -> getColor(context, context.isNightMode, isChecked)
- is ResourceCheckableColorProvider -> {
- // If the user provided res id does not resolve, then resolve using the fallback
- // (which cannot fail).
- resolveCheckedColor(context, resId, isChecked)
- ?: resolveCheckedColor(context, fallback, isChecked)!!
- }
+internal fun CheckableColorProvider.getColor(context: Context, isChecked: Boolean): Color {
+ return when (this) {
+ is CheckedUncheckedColorProvider -> getColor(context, context.isNightMode, isChecked)
+ is ResourceCheckableColorProvider -> {
+ resolveCheckedColor(context, resId, isChecked)
+ }
+ } ?: checkableColorProviderFallbackColor
}
/**
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslator.kt
index e20a8e8..336e3ea 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslator.kt
@@ -67,7 +67,8 @@
textViewId = inflateViewStub(translationContext, R.id.radioText)
val iconId = inflateViewStub(translationContext, R.id.radioIcon)
setViewEnabled(iconId, element.checked)
- setImageViewColorFilter(iconId, element.colors.radio.getColor(context, element.checked))
+ val color = element.colors.radio.getColor(context, element.checked)
+ setImageViewColorFilter(iconId, color)
}
setText(
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/SwitchTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/SwitchTranslator.kt
index d17ff2d..d1d9afc 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/SwitchTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/SwitchTranslator.kt
@@ -78,8 +78,12 @@
val trackId = inflateViewStub(translationContext, R.id.switchTrack)
setViewEnabled(thumbId, element.checked)
setViewEnabled(trackId, element.checked)
- setImageViewColorFilter(thumbId, element.colors.thumb.getColor(context, element.checked))
- setImageViewColorFilter(trackId, element.colors.track.getColor(context, element.checked))
+
+ val thumbColor = element.colors.thumb.getColor(context, element.checked)
+ setImageViewColorFilter(thumbId, thumbColor)
+
+ val trackColor = element.colors.track.getColor(context, element.checked)
+ setImageViewColorFilter(trackId, trackColor)
}
setText(
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/TextTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/TextTranslator.kt
index 78dd121..7e9c958 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/TextTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/TextTranslator.kt
@@ -151,7 +151,6 @@
setTextColor(resId, colorProvider.getColor(translationContext.context).toArgb())
}
}
- null -> {}
else -> Log.w(GlanceAppWidgetTag, "Unexpected text color: $colorProvider")
}
}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/unit/ColorProvider.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/unit/ColorProvider.kt
index e6c1775..0e848d0 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/unit/ColorProvider.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/unit/ColorProvider.kt
@@ -24,6 +24,7 @@
import androidx.compose.ui.graphics.Color
import androidx.core.content.ContextCompat
import androidx.glance.appwidget.GlanceAppWidgetTag
+import androidx.glance.appwidget.R
import androidx.glance.unit.ColorProvider
import androidx.glance.unit.FixedColorProvider
import androidx.glance.unit.ResourceColorProvider
@@ -43,16 +44,14 @@
}
internal val Context.isNightMode: Boolean
- get() =
- resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
- Configuration.UI_MODE_NIGHT_YES
+ get() = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
+ Configuration.UI_MODE_NIGHT_YES
/** Provider of different colors depending on a checked state. */
internal sealed interface CheckableColorProvider
internal data class ResourceCheckableColorProvider(
@ColorRes val resId: Int,
- @ColorRes val fallback: Int
) : CheckableColorProvider
/**
@@ -61,9 +60,8 @@
*/
internal data class CheckedUncheckedColorProvider private constructor(
private val source: String,
- private val checked: ColorProvider?,
- private val unchecked: ColorProvider?,
- @ColorRes private val fallback: Int
+ private val checked: ColorProvider,
+ private val unchecked: ColorProvider,
) : CheckableColorProvider {
init {
@@ -72,43 +70,29 @@
}
}
- private fun ColorProvider?.toDayNightColorProvider(
- context: Context,
- isChecked: Boolean
- ) = when (this) {
- is DayNightColorProvider -> this
- is FixedColorProvider -> DayNightColorProvider(color, color)
- else -> {
- if (this != null) {
- Log.w(GlanceAppWidgetTag, "Unexpected ColorProvider for $source: $this")
- }
- val day = resolveCheckedColor(context, fallback, isChecked, isNightMode = false)!!
- val night = resolveCheckedColor(context, fallback, isChecked, isNightMode = true)!!
- DayNightColorProvider(day = day, night = night)
- }
- }
-
/**
* Resolves the [CheckedUncheckedColorProvider] to a single [Color] given the night mode and
* checked states.
*/
fun getColor(context: Context, isNightMode: Boolean, isChecked: Boolean) = when {
- isChecked -> checked.toDayNightColorProvider(context, isChecked).getColor(isNightMode)
- else -> unchecked.toDayNightColorProvider(context, isChecked).getColor(isNightMode)
+ isChecked -> getColor(colorProvider = checked, isNightMode = isNightMode, context = context)
+ else -> getColor(colorProvider = unchecked, isNightMode = isNightMode, context = context)
}
+ private fun getColor(colorProvider: ColorProvider, isNightMode: Boolean, context: Context) =
+ if (colorProvider is DayNightColorProvider) {
+ colorProvider.getColor(isNightMode)
+ } else {
+ colorProvider.getColor(context)
+ }
+
companion object {
fun createCheckableColorProvider(
source: String,
- checked: ColorProvider?,
- unchecked: ColorProvider?,
- @ColorRes fallback: Int
+ checked: ColorProvider,
+ unchecked: ColorProvider,
): CheckableColorProvider {
- return if (checked == null && unchecked == null) {
- ResourceCheckableColorProvider(fallback, fallback)
- } else {
- CheckedUncheckedColorProvider(source, checked, unchecked, fallback)
- }
+ return CheckedUncheckedColorProvider(source, checked, unchecked)
}
}
}
@@ -138,8 +122,7 @@
}
return Color(
colorStateList.getColorForState(
- if (isChecked) CheckedStateSet else UncheckedStateSet,
- colorStateList.defaultColor
+ if (isChecked) CheckedStateSet else UncheckedStateSet, colorStateList.defaultColor
)
)
}
diff --git a/glance/glance-appwidget/src/androidMain/res/color/glance_default_check_box.xml b/glance/glance-appwidget/src/androidMain/res/color/glance_default_check_box.xml
index 637d59c..00289b5 100644
--- a/glance/glance-appwidget/src/androidMain/res/color/glance_default_check_box.xml
+++ b/glance/glance-appwidget/src/androidMain/res/color/glance_default_check_box.xml
@@ -16,5 +16,5 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?android:attr/colorControlActivated" android:state_checked="true"/>
- <item android:color="?android:attr/colorControlNormal" android:alpha="0.6"/>
+ <item android:color="?android:attr/colorControlNormal" />
</selector>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceAppWidgetTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceAppWidgetTest.kt
index 3979e10..1aa10a4 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceAppWidgetTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceAppWidgetTest.kt
@@ -37,6 +37,7 @@
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import org.mockito.kotlin.mock
+import kotlin.test.assertIs
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestScope
@@ -46,7 +47,6 @@
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
-import kotlin.test.assertIs
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
@@ -97,7 +97,7 @@
val view = context.applyRemoteViews(rv)
assertIs<TextView>(view)
- assertThat(view.text).isEqualTo("40.0.dp x 50.0.dp")
+ assertThat(view.text.toString()).isEqualTo("40.0.dp x 50.0.dp")
}
@Test
@@ -121,7 +121,7 @@
val view = context.applyRemoteViews(rv)
assertIs<TextView>(view)
- assertThat(view.text).isEqualTo("FOUND")
+ assertThat(view.text.toString()).isEqualTo("FOUND")
}
@Test
@@ -144,7 +144,7 @@
val view = context.applyRemoteViews(rv)
assertIs<TextView>(view)
- assertThat(view.text).isEqualTo("AppWidgetId(appWidgetId=1)")
+ assertThat(view.text.toString()).isEqualTo("AppWidgetId(appWidgetId=1)")
}
@Test
@@ -176,7 +176,7 @@
val view = context.applyRemoteViews(rv)
assertIs<TextView>(view)
- assertThat(view.text).isEqualTo("40.0.dp x 50.0.dp")
+ assertThat(view.text.toString()).isEqualTo("40.0.dp x 50.0.dp")
}
@Config(sdk = [30])
@@ -201,11 +201,11 @@
val portraitView = createPortraitContext().applyRemoteViews(rv)
assertIs<TextView>(portraitView)
- assertThat(portraitView.text).isEqualTo("50.0.dp x 100.0.dp")
+ assertThat(portraitView.text.toString()).isEqualTo("50.0.dp x 100.0.dp")
val landscapeView = createLandscapeContext().applyRemoteViews(rv)
assertIs<TextView>(landscapeView)
- assertThat(landscapeView.text).isEqualTo("100.0.dp x 50.0.dp")
+ assertThat(landscapeView.text.toString()).isEqualTo("100.0.dp x 50.0.dp")
}
@Config(sdk = [30])
@@ -237,11 +237,11 @@
val portraitView = createPortraitContext().applyRemoteViews(rv)
assertIs<TextView>(portraitView)
- assertThat(portraitView.text).isEqualTo("60.0.dp x 80.0.dp")
+ assertThat(portraitView.text.toString()).isEqualTo("60.0.dp x 80.0.dp")
val landscapeView = createLandscapeContext().applyRemoteViews(rv)
assertIs<TextView>(landscapeView)
- assertThat(landscapeView.text).isEqualTo("100.0.dp x 70.0.dp")
+ assertThat(landscapeView.text.toString()).isEqualTo("100.0.dp x 70.0.dp")
}
@Test
@@ -273,11 +273,11 @@
val portraitView = createPortraitContext().applyRemoteViews(rv)
assertIs<TextView>(portraitView)
- assertThat(portraitView.text).isEqualTo("40.0.dp x 50.0.dp")
+ assertThat(portraitView.text.toString()).isEqualTo("40.0.dp x 50.0.dp")
val landscapeView = createLandscapeContext().applyRemoteViews(rv)
assertIs<TextView>(landscapeView)
- assertThat(landscapeView.text).isEqualTo("40.0.dp x 50.0.dp")
+ assertThat(landscapeView.text.toString()).isEqualTo("40.0.dp x 50.0.dp")
}
}
@@ -307,11 +307,11 @@
val portraitView = createPortraitContext().applyRemoteViews(rv)
assertIs<TextView>(portraitView)
- assertThat(portraitView.text).isEqualTo("60.0.dp x 80.0.dp")
+ assertThat(portraitView.text.toString()).isEqualTo("60.0.dp x 80.0.dp")
val landscapeView = createLandscapeContext().applyRemoteViews(rv)
assertIs<TextView>(landscapeView)
- assertThat(landscapeView.text).isEqualTo("60.0.dp x 80.0.dp")
+ assertThat(landscapeView.text.toString()).isEqualTo("60.0.dp x 80.0.dp")
}
@Test
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceRemoteViewsTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceRemoteViewsTest.kt
index 40a463c..e080178 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceRemoteViewsTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceRemoteViewsTest.kt
@@ -65,7 +65,7 @@
val view = context.applyRemoteViews(rv)
assertIs<TextView>(view)
- Truth.assertThat(view.text).isEqualTo("40.0.dp x 50.0.dp")
+ Truth.assertThat(view.text.toString()).isEqualTo("40.0.dp x 50.0.dp")
}
@Test
@@ -85,7 +85,7 @@
val view = context.applyRemoteViews(rv)
assertIs<TextView>(view)
- Truth.assertThat(view.text).isEqualTo("FOUND")
+ Truth.assertThat(view.text.toString()).isEqualTo("FOUND")
}
@Test
@@ -100,6 +100,6 @@
val view = context.applyRemoteViews(rv)
assertIs<TextView>(view)
- Truth.assertThat(view.text).isEqualTo("No error thrown")
+ Truth.assertThat(view.text.toString()).isEqualTo("No error thrown")
}
}
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
index bf5d369..66cce3c 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
@@ -47,9 +47,9 @@
import androidx.glance.appwidget.LinearLayoutSubject.Companion.assertThat
import androidx.glance.appwidget.TextViewSubject.Companion.assertThat
import androidx.glance.appwidget.ViewSubject.Companion.assertThat
+import androidx.glance.appwidget.lazy.GridCells
import androidx.glance.appwidget.lazy.LazyColumn
import androidx.glance.appwidget.lazy.LazyVerticalGrid
-import androidx.glance.appwidget.lazy.GridCells
import androidx.glance.appwidget.lazy.ReservedItemIdRangeEnd
import androidx.glance.appwidget.test.R
import androidx.glance.background
@@ -66,6 +66,8 @@
import androidx.glance.visibility
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import kotlin.test.assertIs
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@@ -76,8 +78,6 @@
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.shadows.ShadowLog
-import kotlin.test.assertFailsWith
-import kotlin.test.assertIs
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
@@ -874,9 +874,9 @@
val view = context.applyRemoteViews(rv)
- val firstText = checkNotNull(view.findView<TextView> { it.text == "first" })
- val secondText = checkNotNull(view.findView<TextView> { it.text == "second" })
- val thirdText = checkNotNull(view.findView<TextView> { it.text == "third" })
+ val firstText = checkNotNull(view.findView<TextView> { it.text.toString() == "first" })
+ val secondText = checkNotNull(view.findView<TextView> { it.text.toString() == "second" })
+ val thirdText = checkNotNull(view.findView<TextView> { it.text.toString() == "third" })
assertThat(firstText.visibility).isEqualTo(View.INVISIBLE)
assertThat(secondText.visibility).isEqualTo(View.GONE)
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslatorTest.kt
index c380403..b3073b3 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslatorTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslatorTest.kt
@@ -22,7 +22,7 @@
import android.widget.ImageView
import androidx.compose.ui.graphics.Color
import androidx.glance.appwidget.CheckBox
-import androidx.glance.appwidget.CheckBoxColors
+import androidx.glance.appwidget.checkBoxColors
import androidx.glance.appwidget.ImageViewSubject.Companion.assertThat
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.action.actionRunCallback
@@ -30,10 +30,11 @@
import androidx.glance.appwidget.configurationContext
import androidx.glance.appwidget.findViewByType
import androidx.glance.appwidget.runAndTranslate
-import androidx.glance.appwidget.test.R
import androidx.glance.appwidget.unit.ColorProvider
+import androidx.glance.unit.FixedColorProvider
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertIs
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@@ -42,7 +43,6 @@
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
-import kotlin.test.assertIs
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
@@ -66,7 +66,7 @@
checked = false,
onCheckedChange = null,
text = "Check",
- colors = CheckBoxColors(checkedColor = Color.Red, uncheckedColor = Color.Blue)
+ colors = checkBoxColors(checkedColor = Color.Red, uncheckedColor = Color.Blue)
)
}
@@ -83,7 +83,7 @@
checked = true,
onCheckedChange = null,
text = "Check",
- colors = CheckBoxColors(checkedColor = Color.Red, uncheckedColor = Color.Blue)
+ colors = checkBoxColors(checkedColor = Color.Red, uncheckedColor = Color.Blue)
)
}
@@ -100,7 +100,7 @@
checked = false,
onCheckedChange = null,
text = "Check",
- colors = CheckBoxColors(
+ colors = checkBoxColors(
checkedColor = ColorProvider(day = Color.Red, night = Color.Blue),
uncheckedColor = ColorProvider(day = Color.Yellow, night = Color.Green)
)
@@ -120,7 +120,7 @@
checked = false,
onCheckedChange = null,
text = "Check",
- colors = CheckBoxColors(
+ colors = checkBoxColors(
checkedColor = ColorProvider(day = Color.Red, night = Color.Blue),
uncheckedColor = ColorProvider(day = Color.Yellow, night = Color.Green)
)
@@ -140,7 +140,7 @@
checked = true,
onCheckedChange = null,
text = "Check",
- colors = CheckBoxColors(
+ colors = checkBoxColors(
checkedColor = ColorProvider(day = Color.Red, night = Color.Blue),
uncheckedColor = ColorProvider(day = Color.Yellow, night = Color.Green)
)
@@ -160,7 +160,7 @@
checked = true,
onCheckedChange = null,
text = "Check",
- colors = CheckBoxColors(
+ colors = checkBoxColors(
checkedColor = ColorProvider(day = Color.Red, night = Color.Blue),
uncheckedColor = ColorProvider(day = Color.Yellow, night = Color.Green)
)
@@ -180,13 +180,16 @@
checked = true,
onCheckedChange = null,
text = "Check",
- colors = CheckBoxColors(R.color.my_checkbox_colors)
+ colors = checkBoxColors(
+ checkedColor = FixedColorProvider(Color.Red),
+ uncheckedColor = FixedColorProvider(Color.Blue)
+ )
)
}
val checkBoxRoot = assertIs<ViewGroup>(context.applyRemoteViews(rv))
val icon = checkBoxRoot.findViewByType<ImageView>()
- assertThat(icon).hasColorFilter("#FF0000")
+ assertThat(icon).hasColorFilter(Color.Red)
}
@Config(sdk = [29])
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CompoundButtonTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CompoundButtonTranslatorTest.kt
index 0ccc124..7b0067b 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CompoundButtonTranslatorTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CompoundButtonTranslatorTest.kt
@@ -35,29 +35,26 @@
fun resolveColorResource_0_shouldReturnFallback() {
val colorProvider = ResourceCheckableColorProvider(
resId = 0,
- fallback = R.color.my_color
)
assertThat(colorProvider.getColor(lightContext, isChecked = true))
- .isSameColorAs("#EEEEEE")
+ .isSameColorAs(checkableColorProviderFallbackColor)
}
@Test
fun resolveColorResource_invalid_shouldReturnFallback() {
val colorProvider = ResourceCheckableColorProvider(
resId = -1,
- fallback = R.color.my_color
)
assertThat(colorProvider.getColor(lightContext, isChecked = true))
- .isSameColorAs("#EEEEEE")
+ .isSameColorAs(checkableColorProviderFallbackColor)
}
@Test
fun resolveColorResource_valid_day_shouldReturnResolvedColor() {
val colorProvider = ResourceCheckableColorProvider(
resId = R.color.my_checkbox_colors,
- fallback = R.color.my_color
)
assertThat(colorProvider.getColor(lightContext, isChecked = true))
@@ -68,7 +65,6 @@
fun resolveColorResource_valid_night_shouldReturnResolvedColor() {
val colorProvider = ResourceCheckableColorProvider(
resId = R.color.my_checkbox_colors,
- fallback = R.color.my_color
)
assertThat(colorProvider.getColor(darkContext, isChecked = true))
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslatorTest.kt
index 40fb745..8c645ee 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslatorTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslatorTest.kt
@@ -24,7 +24,7 @@
import androidx.compose.ui.graphics.Color
import androidx.glance.appwidget.ImageViewSubject.Companion.assertThat
import androidx.glance.appwidget.RadioButton
-import androidx.glance.appwidget.RadioButtonColors
+import androidx.glance.appwidget.radioButtonColors
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.appwidget.applyRemoteViews
@@ -32,7 +32,6 @@
import androidx.glance.appwidget.findView
import androidx.glance.appwidget.findViewByType
import androidx.glance.appwidget.runAndTranslate
-import androidx.glance.appwidget.test.R
import androidx.glance.appwidget.unit.ColorProvider
import androidx.glance.unit.ColorProvider
import androidx.test.core.app.ApplicationProvider
@@ -70,7 +69,7 @@
checked = false,
onClick = null,
text = "RadioButton",
- colors = RadioButtonColors(
+ colors = radioButtonColors(
checkedColor = ColorProvider(Color.Blue),
uncheckedColor = ColorProvider(Color.Red),
)
@@ -88,7 +87,7 @@
checked = true,
onClick = null,
text = "RadioButton",
- colors = RadioButtonColors(
+ colors = radioButtonColors(
checkedColor = ColorProvider(Color.Blue),
uncheckedColor = ColorProvider(Color.Red),
)
@@ -106,7 +105,7 @@
checked = false,
onClick = null,
text = "RadioButton",
- colors = RadioButtonColors(
+ colors = radioButtonColors(
checkedColor = ColorProvider(day = Color.Blue, night = Color.Red),
uncheckedColor = ColorProvider(day = Color.Green, night = Color.Yellow),
)
@@ -124,7 +123,7 @@
checked = false,
onClick = null,
text = "RadioButton",
- colors = RadioButtonColors(
+ colors = radioButtonColors(
checkedColor = ColorProvider(day = Color.Blue, night = Color.Red),
uncheckedColor = ColorProvider(day = Color.Green, night = Color.Yellow),
)
@@ -142,7 +141,7 @@
checked = true,
onClick = null,
text = "RadioButton",
- colors = RadioButtonColors(
+ colors = radioButtonColors(
checkedColor = ColorProvider(day = Color.Blue, night = Color.Red),
uncheckedColor = ColorProvider(day = Color.Green, night = Color.Yellow),
)
@@ -160,7 +159,7 @@
checked = true,
onClick = null,
text = "RadioButton",
- colors = RadioButtonColors(
+ colors = radioButtonColors(
checkedColor = ColorProvider(day = Color.Blue, night = Color.Red),
uncheckedColor = ColorProvider(day = Color.Green, night = Color.Yellow),
)
@@ -178,14 +177,15 @@
checked = false,
onClick = null,
text = "RadioButton",
- colors = RadioButtonColors(
- color = R.color.my_switch_thumb_colors,
+ colors = radioButtonColors(
+ checkedColor = ColorProvider(day = Color.Blue, night = Color.Red),
+ uncheckedColor = ColorProvider(day = Color.Green, night = Color.Yellow),
)
)
}
val radioButtonRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
- assertThat(radioButtonRoot.radioImageView).hasColorFilter("#110FF0FF")
+ assertThat(radioButtonRoot.radioImageView).hasColorFilter(Color.Green)
}
@Test
@@ -195,14 +195,15 @@
checked = true,
onClick = null,
text = "RadioButton",
- colors = RadioButtonColors(
- color = R.color.my_switch_thumb_colors,
+ colors = radioButtonColors(
+ checkedColor = ColorProvider(day = Color.Blue, night = Color.Red),
+ uncheckedColor = ColorProvider(day = Color.Green, night = Color.Yellow),
)
)
}
val radioButtonRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
- assertThat(radioButtonRoot.radioImageView).hasColorFilter("#11040040")
+ assertThat(radioButtonRoot.radioImageView).hasColorFilter(Color.Blue)
}
@Test
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchTranslatorTest.kt
index 8cbe211..89d3739 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchTranslatorTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchTranslatorTest.kt
@@ -23,18 +23,18 @@
import androidx.compose.ui.graphics.Color
import androidx.glance.appwidget.ImageViewSubject.Companion.assertThat
import androidx.glance.appwidget.Switch
-import androidx.glance.appwidget.SwitchColors
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.appwidget.applyRemoteViews
import androidx.glance.appwidget.configurationContext
import androidx.glance.appwidget.findView
import androidx.glance.appwidget.runAndTranslate
-import androidx.glance.appwidget.test.R
+import androidx.glance.appwidget.switchColors
import androidx.glance.appwidget.unit.ColorProvider
import androidx.glance.unit.ColorProvider
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertIs
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@@ -44,7 +44,6 @@
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
-import kotlin.test.assertIs
@Config(sdk = [29])
@OptIn(ExperimentalCoroutinesApi::class)
@@ -68,7 +67,7 @@
checked = false,
onCheckedChange = null,
text = "Switch",
- colors = SwitchColors(
+ colors = switchColors(
checkedThumbColor = ColorProvider(Color.Blue),
uncheckedThumbColor = ColorProvider(Color.Red),
checkedTrackColor = ColorProvider(Color.Green),
@@ -89,7 +88,7 @@
checked = true,
onCheckedChange = null,
text = "Switch",
- colors = SwitchColors(
+ colors = switchColors(
checkedThumbColor = ColorProvider(Color.Blue),
uncheckedThumbColor = ColorProvider(Color.Red),
checkedTrackColor = ColorProvider(Color.Green),
@@ -105,98 +104,102 @@
@Test
fun canTranslateSwitch_dayNight_unchecked_day() = fakeCoroutineScope.runTest {
+ val thumbColor = Color.Blue
+ val trackColor = Color.Cyan
+ val otherColor = Color.White
+
val rv = lightContext.runAndTranslate {
Switch(
checked = false,
onCheckedChange = null,
text = "Switch",
- colors = SwitchColors(
- checkedThumbColor = ColorProvider(day = Color.Blue, night = Color.Red),
- uncheckedThumbColor = ColorProvider(day = Color.Green, night = Color.Yellow),
- checkedTrackColor = ColorProvider(day = Color.White, night = Color.Black),
- uncheckedTrackColor = ColorProvider(
- day = Color.DarkGray,
- night = Color.LightGray
- )
+ colors = switchColors(
+ checkedThumbColor = ColorProvider(otherColor),
+ uncheckedThumbColor = ColorProvider(day = thumbColor, night = otherColor),
+ checkedTrackColor = ColorProvider(otherColor),
+ uncheckedTrackColor = ColorProvider(day = trackColor, night = otherColor)
)
)
}
val switchRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
- assertThat(switchRoot.thumbImageView).hasColorFilter(Color.Green)
- assertThat(switchRoot.trackImageView).hasColorFilter(Color.DarkGray)
+ assertThat(switchRoot.thumbImageView).hasColorFilter(thumbColor)
+ assertThat(switchRoot.trackImageView).hasColorFilter(trackColor)
}
@Test
fun canTranslateSwitch_dayNight_unchecked_night() = fakeCoroutineScope.runTest {
+ val thumbColor = Color.Blue
+ val trackColor = Color.Cyan
+ val otherColor = Color.White
+
val rv = darkContext.runAndTranslate {
Switch(
checked = false,
onCheckedChange = null,
text = "Switch",
- colors = SwitchColors(
- checkedThumbColor = ColorProvider(day = Color.Blue, night = Color.Red),
- uncheckedThumbColor = ColorProvider(day = Color.Green, night = Color.Yellow),
- checkedTrackColor = ColorProvider(day = Color.White, night = Color.Black),
- uncheckedTrackColor = ColorProvider(
- day = Color.DarkGray,
- night = Color.LightGray
- )
+ colors = switchColors(
+ checkedThumbColor = ColorProvider(otherColor),
+ uncheckedThumbColor = ColorProvider(day = otherColor, night = thumbColor),
+ checkedTrackColor = ColorProvider(otherColor),
+ uncheckedTrackColor = ColorProvider(day = otherColor, night = trackColor)
)
)
}
val switchRoot = assertIs<ViewGroup>(darkContext.applyRemoteViews(rv))
- assertThat(switchRoot.thumbImageView).hasColorFilter(Color.Yellow)
- assertThat(switchRoot.trackImageView).hasColorFilter(Color.LightGray)
+ assertThat(switchRoot.thumbImageView).hasColorFilter(thumbColor)
+ assertThat(switchRoot.trackImageView).hasColorFilter(trackColor)
}
@Test
fun canTranslateSwitch_dayNight_checked_day() = fakeCoroutineScope.runTest {
+ val thumbColor = Color.Blue
+ val trackColor = Color.Cyan
+ val otherColor = Color.White
+
val rv = lightContext.runAndTranslate {
Switch(
checked = true,
onCheckedChange = null,
text = "Switch",
- colors = SwitchColors(
- checkedThumbColor = ColorProvider(day = Color.Blue, night = Color.Red),
- uncheckedThumbColor = ColorProvider(day = Color.Green, night = Color.Yellow),
- checkedTrackColor = ColorProvider(day = Color.White, night = Color.Black),
- uncheckedTrackColor = ColorProvider(
- day = Color.DarkGray,
- night = Color.LightGray
- )
+ colors = switchColors(
+ checkedThumbColor = ColorProvider(day = thumbColor, night = otherColor),
+ uncheckedThumbColor = ColorProvider(day = otherColor, night = otherColor),
+ checkedTrackColor = ColorProvider(day = trackColor, night = otherColor),
+ uncheckedTrackColor = ColorProvider(day = otherColor, night = otherColor)
)
)
}
val switchRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
- assertThat(switchRoot.thumbImageView).hasColorFilter(Color.Blue)
- assertThat(switchRoot.trackImageView).hasColorFilter(Color.White)
+ assertThat(switchRoot.thumbImageView).hasColorFilter(thumbColor)
+ assertThat(switchRoot.trackImageView).hasColorFilter(trackColor)
}
@Test
fun canTranslateSwitch_dayNight_checked_night() = fakeCoroutineScope.runTest {
+ val thumbColor = Color.Blue
+ val trackColor = Color.Cyan
+ val otherColor = Color.White
+
val rv = darkContext.runAndTranslate {
Switch(
checked = true,
onCheckedChange = null,
text = "Switch",
- colors = SwitchColors(
- checkedThumbColor = ColorProvider(day = Color.Blue, night = Color.Red),
- uncheckedThumbColor = ColorProvider(day = Color.Green, night = Color.Yellow),
- checkedTrackColor = ColorProvider(day = Color.White, night = Color.Black),
- uncheckedTrackColor = ColorProvider(
- day = Color.DarkGray,
- night = Color.LightGray
- )
+ colors = switchColors(
+ checkedThumbColor = ColorProvider(day = otherColor, night = thumbColor),
+ uncheckedThumbColor = ColorProvider(otherColor),
+ checkedTrackColor = ColorProvider(day = otherColor, night = trackColor),
+ uncheckedTrackColor = ColorProvider(otherColor)
)
)
}
val switchRoot = assertIs<ViewGroup>(darkContext.applyRemoteViews(rv))
- assertThat(switchRoot.thumbImageView).hasColorFilter(Color.Red)
- assertThat(switchRoot.trackImageView).hasColorFilter(Color.Black)
+ assertThat(switchRoot.thumbImageView).hasColorFilter(thumbColor)
+ assertThat(switchRoot.trackImageView).hasColorFilter(trackColor)
}
@Test
@@ -206,16 +209,18 @@
checked = false,
onCheckedChange = null,
text = "Switch",
- colors = SwitchColors(
- thumbColor = R.color.my_switch_thumb_colors,
- trackColor = R.color.my_switch_track_colors
+ colors = switchColors(
+ checkedThumbColor = ColorProvider(Color.Magenta),
+ uncheckedThumbColor = ColorProvider(Color.Red),
+ checkedTrackColor = ColorProvider(Color.Magenta),
+ uncheckedTrackColor = ColorProvider(Color.Blue)
)
)
}
val switchRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
- assertThat(switchRoot.thumbImageView).hasColorFilter("#110FF0FF")
- assertThat(switchRoot.trackImageView).hasColorFilter("#220FF0FF")
+ assertThat(switchRoot.thumbImageView).hasColorFilter(Color.Red)
+ assertThat(switchRoot.trackImageView).hasColorFilter(Color.Blue)
}
@Test
@@ -225,16 +230,18 @@
checked = true,
onCheckedChange = null,
text = "Switch",
- colors = SwitchColors(
- thumbColor = R.color.my_switch_thumb_colors,
- trackColor = R.color.my_switch_track_colors
+ colors = switchColors(
+ checkedThumbColor = ColorProvider(Color.Red),
+ uncheckedThumbColor = ColorProvider(Color.Magenta),
+ checkedTrackColor = ColorProvider(Color.Blue),
+ uncheckedTrackColor = ColorProvider(Color.Magenta),
)
)
}
val switchRoot = assertIs<ViewGroup>(lightContext.applyRemoteViews(rv))
- assertThat(switchRoot.thumbImageView).hasColorFilter("#11040040")
- assertThat(switchRoot.trackImageView).hasColorFilter("#22040040")
+ assertThat(switchRoot.thumbImageView).hasColorFilter(Color.Red)
+ assertThat(switchRoot.trackImageView).hasColorFilter(Color.Blue)
}
@Test
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/unit/ColorProviderTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/unit/ColorProviderTest.kt
index b2a5637..d52ed7e 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/unit/ColorProviderTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/unit/ColorProviderTest.kt
@@ -37,65 +37,11 @@
private val context = ApplicationProvider.getApplicationContext<Context>()
@Test
- fun createCheckableColorProvider_checkedNull_uncheckedNull_shouldFallBackToSingleResource() {
- val provider = createCheckableColorProvider(
- source = "ColorProviderTest",
- checked = null,
- unchecked = null,
- fallback = R.color.my_checkbox_colors
- )
-
- assertIs<ResourceCheckableColorProvider>(provider)
- assertThat(provider.resId).isEqualTo(R.color.my_checkbox_colors)
- }
-
- @Test
- fun createCheckableColorProvider_checkedNull_uncheckedNotNull_shouldUseCheckedFromFallback() {
- val provider = createCheckableColorProvider(
- source = "ColorProviderTest",
- checked = null,
- unchecked = ColorProvider(day = Color.Red, night = Color.Yellow),
- fallback = R.color.my_checkbox_colors
- )
-
- assertIs<CheckedUncheckedColorProvider>(provider)
- assertThat(provider.getColor(context, isNightMode = false, isChecked = true))
- .isSameColorAs("#FF0000")
- assertThat(provider.getColor(context, isNightMode = true, isChecked = true))
- .isSameColorAs("#FFFF00")
- assertThat(provider.getColor(context, isNightMode = false, isChecked = false))
- .isSameColorAs(Color.Red)
- assertThat(provider.getColor(context, isNightMode = true, isChecked = false))
- .isSameColorAs(Color.Yellow)
- }
-
- @Test
- fun createCheckableColorProvider_checkedNotNull_uncheckedNull_shouldUseUncheckedFromFallback() {
- val provider = createCheckableColorProvider(
- source = "ColorProviderTest",
- checked = ColorProvider(day = Color.Blue, night = Color.Green),
- unchecked = null,
- fallback = R.color.my_checkbox_colors
- )
-
- assertIs<CheckedUncheckedColorProvider>(provider)
- assertThat(provider.getColor(context, isNightMode = false, isChecked = true))
- .isSameColorAs(Color.Blue)
- assertThat(provider.getColor(context, isNightMode = true, isChecked = true))
- .isSameColorAs(Color.Green)
- assertThat(provider.getColor(context, isNightMode = false, isChecked = false))
- .isSameColorAs("#0000FF")
- assertThat(provider.getColor(context, isNightMode = true, isChecked = false))
- .isSameColorAs("#00FFFF")
- }
-
- @Test
fun createCheckableColorProvider_checkedNotNull_uncheckedNotNull_shouldNotUseFallback() {
val provider = createCheckableColorProvider(
source = "ColorProviderTest",
checked = ColorProvider(day = Color.Blue, night = Color.Green),
unchecked = ColorProvider(day = Color.Red, night = Color.Yellow),
- fallback = R.color.my_checkbox_colors
)
assertIs<CheckedUncheckedColorProvider>(provider)
@@ -115,7 +61,6 @@
source = "ColorProviderTest",
checked = ColorProvider(Color.Blue),
unchecked = ColorProvider(Color.Red),
- fallback = R.color.my_checkbox_colors
)
assertIs<CheckedUncheckedColorProvider>(provider)
@@ -136,7 +81,6 @@
source = "ColorProviderTest",
checked = ColorProvider(day = Color.Blue, night = Color.Green),
unchecked = ColorProvider(R.color.my_checkbox_colors),
- fallback = R.color.my_checkbox_colors
)
}
}
@@ -148,7 +92,6 @@
source = "ColorProviderTest",
checked = ColorProvider(R.color.my_checkbox_colors),
unchecked = ColorProvider(Color.Blue),
- fallback = R.color.my_checkbox_colors
)
}
}
diff --git a/glance/glance-wear-tiles/src/androidAndroidTest/kotlin/androidx/glance/wear/tiles/ScreenshotTests.kt b/glance/glance-wear-tiles/src/androidAndroidTest/kotlin/androidx/glance/wear/tiles/ScreenshotTests.kt
index bc60ddf..13e0b39 100644
--- a/glance/glance-wear-tiles/src/androidAndroidTest/kotlin/androidx/glance/wear/tiles/ScreenshotTests.kt
+++ b/glance/glance-wear-tiles/src/androidAndroidTest/kotlin/androidx/glance/wear/tiles/ScreenshotTests.kt
@@ -83,6 +83,10 @@
@OptIn(ExperimentalCoroutinesApi::class)
class ScreenshotTests {
+
+ private val defaultTextColor = ColorProvider(Color.White)
+ private val defaultTextStyle = TextStyle(defaultTextColor)
+
@get:Rule
var screenshotRule = AndroidXScreenshotTestRule("glance/glance-wear-tiles")
@@ -157,13 +161,16 @@
@Test
fun basicText() = runSingleGoldenTest("basic-text") {
Column {
- Text(text = "Normal")
- Text(text = "Bold", style = TextStyle(fontWeight = FontWeight.Bold))
- Text(text = "Italic", style = TextStyle(fontStyle = FontStyle.Italic))
- Text(text = "Underline", style = TextStyle(textDecoration = TextDecoration.Underline))
+ Text(text = "Normal", style = defaultTextStyle)
+ Text(text = "Bold", style = defaultTextStyle.copy(fontWeight = FontWeight.Bold))
+ Text(text = "Italic", style = defaultTextStyle.copy(fontStyle = FontStyle.Italic))
+ Text(
+ text = "Underline",
+ style = defaultTextStyle.copy(textDecoration = TextDecoration.Underline)
+ )
Text(
text = "Everything",
- style = TextStyle(
+ style = defaultTextStyle.copy(
fontWeight = FontWeight.Bold,
fontStyle = FontStyle.Italic,
textDecoration = TextDecoration.Underline
@@ -178,23 +185,25 @@
Column {
Text(
text = "Hello World",
+ style = defaultTextStyle,
modifier = GlanceModifier.width(95.dp).height(60.dp).background(Color.Green)
)
Text(
text = "Hello World",
- style = TextStyle(textAlign = TextAlign.Start),
+ style = defaultTextStyle.copy(textAlign = TextAlign.Start),
modifier = GlanceModifier.width(95.dp).height(60.dp).background(Color.Blue)
)
Text(
text = "Hello World",
- style = TextStyle(textAlign = TextAlign.End),
+ style = defaultTextStyle.copy(textAlign = TextAlign.End),
modifier = GlanceModifier.width(95.dp).height(60.dp).background(Color.Red)
)
}
Column {
Text(
text = "Hello World! This is a multiline test",
+ style = defaultTextStyle,
maxLines = 3,
modifier = GlanceModifier.width(100.dp).height(60.dp).background(Color.Red)
)
@@ -202,7 +211,7 @@
Text(
text = "Hello World! This is a multiline test",
maxLines = 3,
- style = TextStyle(
+ style = defaultTextStyle.copy(
textAlign = TextAlign.Start
),
modifier = GlanceModifier.width(100.dp).height(60.dp).background(Color.Green)
@@ -211,7 +220,7 @@
Text(
text = "Hello World! This is a multiline test",
maxLines = 3,
- style = TextStyle(
+ style = defaultTextStyle.copy(
textAlign = TextAlign.End
),
modifier = GlanceModifier.width(100.dp).height(60.dp).background(Color.Blue)
@@ -331,7 +340,7 @@
@Test
fun visibility() = runSingleGoldenTest("visibility") {
Column(modifier = GlanceModifier.fillMaxSize().background(Color.DarkGray)) {
- Text("First", style = TextStyle(color = ColorProvider(Color.Red)))
+ Text("First", style = defaultTextStyle.copy(color = ColorProvider(Color.Red)))
Text("gone", modifier = GlanceModifier.visibility(Visibility.Gone))
Row {
Text(
@@ -339,15 +348,15 @@
modifier = GlanceModifier.visibility(Visibility.Invisible)
.background(ColorProvider(Color.Red))
)
- Text("after")
+ Text("after", style = defaultTextStyle)
}
- Text("Third")
+ Text("Third", style = defaultTextStyle)
Row(
modifier = GlanceModifier.visibility(Visibility.Invisible).background(Color.Green)
) {
Spacer(modifier = GlanceModifier.size(10.dp).background(Color.Red))
}
- Text("Last")
+ Text("Last", style = defaultTextStyle)
}
}
@@ -418,8 +427,6 @@
)
// Blit it to a bitmap for further testing.
-
- // Blit it to a bitmap for further testing.
val bmp = Bitmap.createBitmap(
SCREEN_WIDTH,
SCREEN_HEIGHT,
diff --git a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
index 141f83b..cec3b08 100644
--- a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
+++ b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
@@ -406,7 +406,7 @@
): LayoutElementBuilders.FontStyle {
val fontStyleBuilder = LayoutElementBuilders.FontStyle.Builder()
- style.color?.let { fontStyleBuilder.setColor(argb(it.getColorAsArgb(context))) }
+ style.color.let { fontStyleBuilder.setColor(argb(it.getColorAsArgb(context))) }
// TODO(b/203656358): Can we support Em here too?
style.fontSize?.let {
if (!it.isSp) {
diff --git a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/action/RunCallbackAction.kt b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/action/RunCallbackAction.kt
index 0e0a597..88293fe 100644
--- a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/action/RunCallbackAction.kt
+++ b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/action/RunCallbackAction.kt
@@ -36,7 +36,7 @@
error("Provided class must implement ActionCallback.")
}
- val actionCallback = workClass.newInstance() as ActionCallback
+ val actionCallback = workClass.getDeclaredConstructor().newInstance() as ActionCallback
actionCallback.onAction(context, glanceId)
}
}
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index 6bf32f6..b854d30 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -73,6 +73,16 @@
@androidx.glance.GlanceComposable public final class GlanceNodeKt {
}
+ public final class GlanceTheme {
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable @androidx.glance.GlanceComposable public androidx.glance.color.ColorProviders getColors();
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable @androidx.glance.GlanceComposable public final androidx.glance.color.ColorProviders colors;
+ field public static final androidx.glance.GlanceTheme INSTANCE;
+ }
+
+ public final class GlanceThemeKt {
+ method @androidx.compose.runtime.Composable public static void GlanceTheme(optional androidx.glance.color.ColorProviders colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
public final class ImageKt {
method @androidx.compose.runtime.Composable public static void Image(androidx.glance.ImageProvider provider, String? contentDescription, optional androidx.glance.GlanceModifier modifier, optional int contentScale);
method public static androidx.glance.ImageProvider ImageProvider(@DrawableRes int resId);
@@ -712,20 +722,27 @@
property public final int Underline;
}
+ public final class TextDefaults {
+ method public androidx.glance.text.TextStyle defaultTextStyle();
+ method public androidx.glance.unit.ColorProvider getDefaultTextColor();
+ property public final androidx.glance.unit.ColorProvider defaultTextColor;
+ field public static final androidx.glance.text.TextDefaults INSTANCE;
+ }
+
public final class TextKt {
- method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.glance.GlanceModifier modifier, optional androidx.glance.text.TextStyle? style, optional int maxLines);
+ method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.glance.GlanceModifier modifier, optional androidx.glance.text.TextStyle style, optional int maxLines);
}
@androidx.compose.runtime.Immutable public final class TextStyle {
- ctor public TextStyle(optional androidx.glance.unit.ColorProvider? color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
- method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider? color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
- method public androidx.glance.unit.ColorProvider? getColor();
+ ctor public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
+ method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
+ method public androidx.glance.unit.ColorProvider getColor();
method public androidx.compose.ui.unit.TextUnit? getFontSize();
method public androidx.glance.text.FontStyle? getFontStyle();
method public androidx.glance.text.FontWeight? getFontWeight();
method public androidx.glance.text.TextAlign? getTextAlign();
method public androidx.glance.text.TextDecoration? getTextDecoration();
- property public final androidx.glance.unit.ColorProvider? color;
+ property public final androidx.glance.unit.ColorProvider color;
property public final androidx.compose.ui.unit.TextUnit? fontSize;
property public final androidx.glance.text.FontStyle? fontStyle;
property public final androidx.glance.text.FontWeight? fontWeight;
diff --git a/glance/glance/api/public_plus_experimental_current.txt b/glance/glance/api/public_plus_experimental_current.txt
index 6bf32f6..b854d30 100644
--- a/glance/glance/api/public_plus_experimental_current.txt
+++ b/glance/glance/api/public_plus_experimental_current.txt
@@ -73,6 +73,16 @@
@androidx.glance.GlanceComposable public final class GlanceNodeKt {
}
+ public final class GlanceTheme {
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable @androidx.glance.GlanceComposable public androidx.glance.color.ColorProviders getColors();
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable @androidx.glance.GlanceComposable public final androidx.glance.color.ColorProviders colors;
+ field public static final androidx.glance.GlanceTheme INSTANCE;
+ }
+
+ public final class GlanceThemeKt {
+ method @androidx.compose.runtime.Composable public static void GlanceTheme(optional androidx.glance.color.ColorProviders colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
public final class ImageKt {
method @androidx.compose.runtime.Composable public static void Image(androidx.glance.ImageProvider provider, String? contentDescription, optional androidx.glance.GlanceModifier modifier, optional int contentScale);
method public static androidx.glance.ImageProvider ImageProvider(@DrawableRes int resId);
@@ -712,20 +722,27 @@
property public final int Underline;
}
+ public final class TextDefaults {
+ method public androidx.glance.text.TextStyle defaultTextStyle();
+ method public androidx.glance.unit.ColorProvider getDefaultTextColor();
+ property public final androidx.glance.unit.ColorProvider defaultTextColor;
+ field public static final androidx.glance.text.TextDefaults INSTANCE;
+ }
+
public final class TextKt {
- method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.glance.GlanceModifier modifier, optional androidx.glance.text.TextStyle? style, optional int maxLines);
+ method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.glance.GlanceModifier modifier, optional androidx.glance.text.TextStyle style, optional int maxLines);
}
@androidx.compose.runtime.Immutable public final class TextStyle {
- ctor public TextStyle(optional androidx.glance.unit.ColorProvider? color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
- method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider? color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
- method public androidx.glance.unit.ColorProvider? getColor();
+ ctor public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
+ method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
+ method public androidx.glance.unit.ColorProvider getColor();
method public androidx.compose.ui.unit.TextUnit? getFontSize();
method public androidx.glance.text.FontStyle? getFontStyle();
method public androidx.glance.text.FontWeight? getFontWeight();
method public androidx.glance.text.TextAlign? getTextAlign();
method public androidx.glance.text.TextDecoration? getTextDecoration();
- property public final androidx.glance.unit.ColorProvider? color;
+ property public final androidx.glance.unit.ColorProvider color;
property public final androidx.compose.ui.unit.TextUnit? fontSize;
property public final androidx.glance.text.FontStyle? fontStyle;
property public final androidx.glance.text.FontWeight? fontWeight;
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index 6bf32f6..b854d30 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -73,6 +73,16 @@
@androidx.glance.GlanceComposable public final class GlanceNodeKt {
}
+ public final class GlanceTheme {
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable @androidx.glance.GlanceComposable public androidx.glance.color.ColorProviders getColors();
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable @androidx.glance.GlanceComposable public final androidx.glance.color.ColorProviders colors;
+ field public static final androidx.glance.GlanceTheme INSTANCE;
+ }
+
+ public final class GlanceThemeKt {
+ method @androidx.compose.runtime.Composable public static void GlanceTheme(optional androidx.glance.color.ColorProviders colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
public final class ImageKt {
method @androidx.compose.runtime.Composable public static void Image(androidx.glance.ImageProvider provider, String? contentDescription, optional androidx.glance.GlanceModifier modifier, optional int contentScale);
method public static androidx.glance.ImageProvider ImageProvider(@DrawableRes int resId);
@@ -712,20 +722,27 @@
property public final int Underline;
}
+ public final class TextDefaults {
+ method public androidx.glance.text.TextStyle defaultTextStyle();
+ method public androidx.glance.unit.ColorProvider getDefaultTextColor();
+ property public final androidx.glance.unit.ColorProvider defaultTextColor;
+ field public static final androidx.glance.text.TextDefaults INSTANCE;
+ }
+
public final class TextKt {
- method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.glance.GlanceModifier modifier, optional androidx.glance.text.TextStyle? style, optional int maxLines);
+ method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.glance.GlanceModifier modifier, optional androidx.glance.text.TextStyle style, optional int maxLines);
}
@androidx.compose.runtime.Immutable public final class TextStyle {
- ctor public TextStyle(optional androidx.glance.unit.ColorProvider? color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
- method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider? color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
- method public androidx.glance.unit.ColorProvider? getColor();
+ ctor public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
+ method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
+ method public androidx.glance.unit.ColorProvider getColor();
method public androidx.compose.ui.unit.TextUnit? getFontSize();
method public androidx.glance.text.FontStyle? getFontStyle();
method public androidx.glance.text.FontWeight? getFontWeight();
method public androidx.glance.text.TextAlign? getTextAlign();
method public androidx.glance.text.TextDecoration? getTextDecoration();
- property public final androidx.glance.unit.ColorProvider? color;
+ property public final androidx.glance.unit.ColorProvider color;
property public final androidx.compose.ui.unit.TextUnit? fontSize;
property public final androidx.glance.text.FontStyle? fontStyle;
property public final androidx.glance.text.FontWeight? fontWeight;
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/Button.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/Button.kt
index 7e7b696..a41cba2 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/Button.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/Button.kt
@@ -43,7 +43,7 @@
modifier: GlanceModifier = GlanceModifier,
enabled: Boolean = true,
style: TextStyle? = null,
- colors: ButtonColors = DEFAULT_COLORS,
+ colors: ButtonColors = defaultButtonColors(),
maxLines: Int = Int.MAX_VALUE,
) {
var finalModifier = if (enabled) modifier.clickable(onClick) else modifier
@@ -70,7 +70,7 @@
override var modifier: GlanceModifier = GlanceModifier
var text: String = ""
var style: TextStyle? = null
- var colors: ButtonColors = DEFAULT_COLORS
+ var colors: ButtonColors? = null
var enabled: Boolean = true
var maxLines: Int = Int.MAX_VALUE
@@ -117,7 +117,8 @@
}
}
-internal val DEFAULT_COLORS = ButtonColors(
- ColorProvider(R.color.glance_colorPrimary),
- ColorProvider(R.color.glance_colorOnPrimary)
+@Composable
+internal fun defaultButtonColors() = ButtonColors(
+ GlanceTheme.colors.primary,
+ GlanceTheme.colors.onPrimary
)
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/CompositionLocals.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/CompositionLocals.kt
index d2d086e..39a8849 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/CompositionLocals.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/CompositionLocals.kt
@@ -18,11 +18,14 @@
import android.content.Context
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.State
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.unit.DpSize
import androidx.datastore.preferences.core.Preferences
+import androidx.glance.color.ColorProviders
+import androidx.glance.color.DynamicThemeColorProviders
import androidx.glance.state.GlanceStateDefinition
/**
@@ -73,3 +76,11 @@
@Composable
inline fun <reified T> currentState(key: Preferences.Key<T>): T? =
currentState<Preferences>()[key]
+
+/**
+ * The colors currently provided by [GlanceTheme]. If no overrides are provided, it will provide a
+ * standard set of Material3 colors, see [DynamicThemeColorProviders]. This should usually be
+ * accessed through [GlanceTheme.colors] rather than directly.
+ */
+internal val LocalColors: ProvidableCompositionLocal<ColorProviders> =
+ staticCompositionLocalOf { DynamicThemeColorProviders }
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/GlanceTheme.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/GlanceTheme.kt
new file mode 100644
index 0000000..9541979
--- /dev/null
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/GlanceTheme.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.glance
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.glance.color.ColorProviders
+
+/**
+ * Object that provides access to the current theme.
+ */
+object GlanceTheme {
+ val colors: ColorProviders
+ @GlanceComposable @Composable
+ @ReadOnlyComposable
+ get() = LocalColors.current
+}
+
+/**
+ * A top level theme for Glance code. Unlike a standard compose theme, this only provides
+ * color. Like a standard compose theme, it should be at the top level of a compose hierarchy with
+ * all themed UI provided as [content].
+ */
+@Composable
+fun GlanceTheme(
+ colors: ColorProviders = LocalColors.current,
+ content: @GlanceComposable @Composable () -> Unit
+) {
+ CompositionLocalProvider(
+ LocalColors provides colors,
+ content = content
+ )
+}
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/text/Text.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/text/Text.kt
index 387bd5b..6878e55 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/text/Text.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/text/Text.kt
@@ -19,9 +19,12 @@
import androidx.annotation.RestrictTo
import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
import androidx.glance.Emittable
import androidx.glance.GlanceModifier
import androidx.glance.GlanceNode
+import androidx.glance.text.TextDefaults.defaultTextStyle
+import androidx.glance.unit.ColorProvider
/**
* Adds a text view to the glance view.
@@ -36,7 +39,7 @@
fun Text(
text: String,
modifier: GlanceModifier = GlanceModifier,
- style: TextStyle? = null,
+ style: TextStyle = defaultTextStyle(),
maxLines: Int = Int.MAX_VALUE,
) {
GlanceNode(
@@ -50,6 +53,11 @@
)
}
+object TextDefaults {
+ val defaultTextColor = ColorProvider(Color.Black)
+ fun defaultTextStyle(): TextStyle = TextStyle(color = defaultTextColor)
+}
+
/** @suppress */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class EmittableText : Emittable {
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/text/TextStyle.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/text/TextStyle.kt
index be3f8b2..f8c01f9 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/text/TextStyle.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/text/TextStyle.kt
@@ -21,11 +21,11 @@
import androidx.glance.unit.ColorProvider
/**
- * Description of a text style for the [androidx.glance.layout.Text] composable.
+ * Description of a text style for the [androidx.glance.text.Text] composable.
*/
@Immutable
class TextStyle(
- val color: ColorProvider? = null,
+ val color: ColorProvider = TextDefaults.defaultTextColor,
val fontSize: TextUnit? = null,
val fontWeight: FontWeight? = null,
val fontStyle: FontStyle? = null,
@@ -33,7 +33,7 @@
val textDecoration: TextDecoration? = null,
) {
fun copy(
- color: ColorProvider? = this.color,
+ color: ColorProvider = this.color,
fontSize: TextUnit? = this.fontSize,
fontWeight: FontWeight? = this.fontWeight,
fontStyle: FontStyle? = this.fontStyle,
diff --git a/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt b/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
index a106652..8bc9546 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
@@ -31,6 +31,7 @@
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@@ -70,6 +71,7 @@
WorkManager.getInstance(context).cancelAllWork()
}
+ @Ignore // b/254048913
@Test
fun startSession() = runTest {
assertThat(sessionManager.isSessionRunning(context, key)).isFalse()
@@ -78,6 +80,7 @@
assertThat(sessionManager.getSession(key)).isSameInstanceAs(session)
}
+ @Ignore // b/254048913
@Test
fun closeSession() = runTest {
sessionManager.startSession(context, session)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 71cbc7c..9eb5e55 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -82,6 +82,7 @@
apacheAnt = { module = "org.apache.ant:ant", version = "1.10.11" }
apacheCommonsCodec = { module = "commons-codec:commons-codec", version = "1.15" }
apacheCommonIo = { module = "commons-io:commons-io", version = "2.4" }
+apacheCommonsMath = { module = "org.apache.commons:commons-math3", version = "3.6.1" }
assertj = { module = "org.assertj:assertj-core", version = "3.23.1" }
asm = { module = "org.ow2.asm:asm", version.ref = "asm"}
asmCommons = { module = "org.ow2.asm:asm-commons", version.ref = "asm"}
diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys
index 6334dd7..787b19f 100644
--- a/gradle/verification-keyring.keys
+++ b/gradle/verification-keyring.keys
@@ -4496,6 +4496,42 @@
-----END PGP PUBLIC KEY BLOCK-----
+pub 64A16FAAEC16A4BE
+uid Evan Ward <[email protected]>
+
+sub 1E8F1D57A4450BCB
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQENBFbqsT0BCADwERe1Rc9qNWwXOvwZHsjauVDy0TpqNVY8I3S+OYm4rX1dkjyh
++6bTEH1ys6bKevvR+PLhYzTGKboHnMT0RIINY/DQQSzHr/GRyCiiRlRvULbt9Fnz
+kJJDgH2BcbNSmWJlrCqYk+E3GAyQial+szkEZED+02wXHsbs0z3vozjQGHy0RVOj
+Gc5Shwk7Hr/F3vw9EQKa1nNffWmcGEx9B+WcC9ALPVd/fpQVXvlqfbi+kaIbqv2x
+NHQr7BL8j3SpN6vhfZM/3zeghlxQ5HYWER983XwkmvbNdMxt5HWsMKWZ0utt4ocK
+TnQP8NFGlPWEQhPvRRFNb9BI0wvGD0NUb1gjABEBAAG0H0V2YW4gV2FyZCA8ZXZh
+bndhcmRAYXBhY2hlLm9yZz6JATkEEwECACMFAlbqsT0CGwMHCwkIBwMCAQYVCAIJ
+CgsEFgIDAQIeAQIXgAAKCRBkoW+q7BakvnygB/41oiYgfDqkG5srQ4nC7jE0Pe5V
+MnuLVHqsfJBGPvt2tz5+Z1ciIFFwUi/xsafX5DhC+FVOOGdeEnkKnskPBOI7uMFh
+v/s90lbhNV62LfwcS9hptE4qn0JTg7mYiiL0Zue99mlkeP105+GlMmvH5q54X2Le
+hIDBVR8DehL8ZqZCvNEVK1ftpx45mvF/4yh0YK0oVuCAAzwF9+6OxeWTCUTRHTZC
+4CWjtXKUHMq4nTRSp0wGdqd5UV0VbMn0bKTkhgRNCJAKyFw6lJ0FZWwmuG28T0s+
+bKuRAJHTAZmSM7UmBnKo22t9whNcozcqxWhK1lcS4OWEArXpCKxAx4kXmbwnuQEN
+BFbqsT0BCADj5dHK9K27rmkFscDY6x53w4L/X6AYKmVu7Qol7VhR+1WtZXgxZaLM
+xDxj5RK4sNOIqh6R3vEMlAVS+iYbzahI/A0fcSciCoLCgjJKCR3STnTu2k0D/MO0
+la+wF/bGPa0UADGIJLRCjalkl5uv4c7zZbyLnRl8a9XSc81cp2WJTxafZJlJkFOU
+4cyewwFuH0pwMvc9Wmwhkh5IeBF6w8Asj77M5bzJINXYxtKMGYA506609HrvN0+r
+obfgx4Aqy4hGKsqXMsSZiuPDvbdtH3gIRV8NPdYRq+dQg/gv222Y3G1xprDVbl1A
+1CCHlaUqT2lIFPovjoB2O2SBeX9xKRJzABEBAAGJAR8EGAECAAkFAlbqsT0CGwwA
+CgkQZKFvquwWpL4yawf/WDI4VqLkR9RqaX3am/kS8481pZPWZUlCCL7jONB7X7eG
+Bit/FjmQWzfL5nWAEB5qhm2qqCgvgtPmVxCrQLECVmaGmDFmhGIFh8TQsYvQJPK6
+HZDxZj97lUKsG/ojOY4ZArvZnsXBU6C963QUZF+P5UL52n1pE/ByMV1R3enEfrYI
+X+wZslOx5uRFOR8dgUpG/ohh2vkFCaKD6KJQHm6C5lGBgUNqGMFxp1nknKJaNqYq
+jvippm6KcocWARfTHx6Xm3mBqxigmpsalUKAGpjcsxsIEY6jnnN/5i5y1XeokTY8
+6fqEt2OSFSkWiApqq6lxMRluTiq33bSquTxSomKfQQ==
+=PImz
+-----END PGP PUBLIC KEY BLOCK-----
+
+
pub 6525FD70CC303655
uid Stephane Nicoll <[email protected]>
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index ca84657..c11dc85e 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -146,6 +146,7 @@
<trusted-key id="3f36885c24df4b75" group="org.mozilla"/>
<trusted-key id="3faad2cd5ecbb314" group="org.apache.commons"/>
<trusted-key id="41321490758aad6f" group="org.codehaus.groovy"/>
+ <trusted-key id="41a1a08c62fca78b79d3081164a16faaec16a4be" group="org.apache.commons" name="commons-math3"/>
<trusted-key id="41cd49b4ef5876f9e9f691dabac30622339994c4">
<trusting group="com.google.testing.compile"/>
<trusting group="com.google.truth"/>
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index b526a23..1943714 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -293,33 +293,32 @@
}
public final class CervicalMucusRecord implements androidx.health.connect.client.records.Record {
- ctor public CervicalMucusRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional String? appearance, optional String? sensation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public String? getAppearance();
+ ctor public CervicalMucusRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional int appearance, optional int sensation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public int getAppearance();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public String? getSensation();
+ method public int getSensation();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
- property public final String? appearance;
+ property public final int appearance;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public final String? sensation;
+ property public final int sensation;
property public java.time.Instant time;
property public java.time.ZoneOffset? zoneOffset;
+ field public static final int APPEARANCE_CREAMY = 3; // 0x3
+ field public static final int APPEARANCE_DRY = 1; // 0x1
+ field public static final int APPEARANCE_EGG_WHITE = 5; // 0x5
+ field public static final int APPEARANCE_STICKY = 2; // 0x2
+ field public static final int APPEARANCE_UNKNOWN = 0; // 0x0
+ field public static final int APPEARANCE_UNUSUAL = 6; // 0x6
+ field public static final int APPEARANCE_WATERY = 4; // 0x4
+ field public static final androidx.health.connect.client.records.CervicalMucusRecord.Companion Companion;
+ field public static final int SENSATION_HEAVY = 3; // 0x3
+ field public static final int SENSATION_LIGHT = 1; // 0x1
+ field public static final int SENSATION_MEDIUM = 2; // 0x2
+ field public static final int SENSATION_UNKNOWN = 0; // 0x0
}
- public static final class CervicalMucusRecord.Appearance {
- field public static final String CLEAR = "clear";
- field public static final String CREAMY = "creamy";
- field public static final String DRY = "dry";
- field public static final androidx.health.connect.client.records.CervicalMucusRecord.Appearance INSTANCE;
- field public static final String STICKY = "sticky";
- field public static final String WATERY = "watery";
- }
-
- public static final class CervicalMucusRecord.Sensation {
- field public static final String HEAVY = "heavy";
- field public static final androidx.health.connect.client.records.CervicalMucusRecord.Sensation INSTANCE;
- field public static final String LIGHT = "light";
- field public static final String MEDIUM = "medium";
+ public static final class CervicalMucusRecord.Companion {
}
public final class CyclingPedalingCadenceRecord implements androidx.health.connect.client.records.Record {
diff --git a/health/connect/connect-client/api/public_plus_experimental_current.txt b/health/connect/connect-client/api/public_plus_experimental_current.txt
index b526a23..1943714 100644
--- a/health/connect/connect-client/api/public_plus_experimental_current.txt
+++ b/health/connect/connect-client/api/public_plus_experimental_current.txt
@@ -293,33 +293,32 @@
}
public final class CervicalMucusRecord implements androidx.health.connect.client.records.Record {
- ctor public CervicalMucusRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional String? appearance, optional String? sensation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public String? getAppearance();
+ ctor public CervicalMucusRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional int appearance, optional int sensation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public int getAppearance();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public String? getSensation();
+ method public int getSensation();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
- property public final String? appearance;
+ property public final int appearance;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public final String? sensation;
+ property public final int sensation;
property public java.time.Instant time;
property public java.time.ZoneOffset? zoneOffset;
+ field public static final int APPEARANCE_CREAMY = 3; // 0x3
+ field public static final int APPEARANCE_DRY = 1; // 0x1
+ field public static final int APPEARANCE_EGG_WHITE = 5; // 0x5
+ field public static final int APPEARANCE_STICKY = 2; // 0x2
+ field public static final int APPEARANCE_UNKNOWN = 0; // 0x0
+ field public static final int APPEARANCE_UNUSUAL = 6; // 0x6
+ field public static final int APPEARANCE_WATERY = 4; // 0x4
+ field public static final androidx.health.connect.client.records.CervicalMucusRecord.Companion Companion;
+ field public static final int SENSATION_HEAVY = 3; // 0x3
+ field public static final int SENSATION_LIGHT = 1; // 0x1
+ field public static final int SENSATION_MEDIUM = 2; // 0x2
+ field public static final int SENSATION_UNKNOWN = 0; // 0x0
}
- public static final class CervicalMucusRecord.Appearance {
- field public static final String CLEAR = "clear";
- field public static final String CREAMY = "creamy";
- field public static final String DRY = "dry";
- field public static final androidx.health.connect.client.records.CervicalMucusRecord.Appearance INSTANCE;
- field public static final String STICKY = "sticky";
- field public static final String WATERY = "watery";
- }
-
- public static final class CervicalMucusRecord.Sensation {
- field public static final String HEAVY = "heavy";
- field public static final androidx.health.connect.client.records.CervicalMucusRecord.Sensation INSTANCE;
- field public static final String LIGHT = "light";
- field public static final String MEDIUM = "medium";
+ public static final class CervicalMucusRecord.Companion {
}
public final class CyclingPedalingCadenceRecord implements androidx.health.connect.client.records.Record {
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 4910c88..86bb039 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -293,33 +293,32 @@
}
public final class CervicalMucusRecord implements androidx.health.connect.client.records.InstantaneousRecord {
- ctor public CervicalMucusRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional String? appearance, optional String? sensation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public String? getAppearance();
+ ctor public CervicalMucusRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional int appearance, optional int sensation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public int getAppearance();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public String? getSensation();
+ method public int getSensation();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
- property public final String? appearance;
+ property public final int appearance;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public final String? sensation;
+ property public final int sensation;
property public java.time.Instant time;
property public java.time.ZoneOffset? zoneOffset;
+ field public static final int APPEARANCE_CREAMY = 3; // 0x3
+ field public static final int APPEARANCE_DRY = 1; // 0x1
+ field public static final int APPEARANCE_EGG_WHITE = 5; // 0x5
+ field public static final int APPEARANCE_STICKY = 2; // 0x2
+ field public static final int APPEARANCE_UNKNOWN = 0; // 0x0
+ field public static final int APPEARANCE_UNUSUAL = 6; // 0x6
+ field public static final int APPEARANCE_WATERY = 4; // 0x4
+ field public static final androidx.health.connect.client.records.CervicalMucusRecord.Companion Companion;
+ field public static final int SENSATION_HEAVY = 3; // 0x3
+ field public static final int SENSATION_LIGHT = 1; // 0x1
+ field public static final int SENSATION_MEDIUM = 2; // 0x2
+ field public static final int SENSATION_UNKNOWN = 0; // 0x0
}
- public static final class CervicalMucusRecord.Appearance {
- field public static final String CLEAR = "clear";
- field public static final String CREAMY = "creamy";
- field public static final String DRY = "dry";
- field public static final androidx.health.connect.client.records.CervicalMucusRecord.Appearance INSTANCE;
- field public static final String STICKY = "sticky";
- field public static final String WATERY = "watery";
- }
-
- public static final class CervicalMucusRecord.Sensation {
- field public static final String HEAVY = "heavy";
- field public static final androidx.health.connect.client.records.CervicalMucusRecord.Sensation INSTANCE;
- field public static final String LIGHT = "light";
- field public static final String MEDIUM = "medium";
+ public static final class CervicalMucusRecord.Companion {
}
public final class CyclingPedalingCadenceRecord implements androidx.health.connect.client.records.SeriesRecord<androidx.health.connect.client.records.CyclingPedalingCadenceRecord.Sample> {
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
index 5101275..4e62508 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
@@ -28,6 +28,8 @@
import androidx.health.connect.client.records.BodyWaterMassRecord
import androidx.health.connect.client.records.BoneMassRecord
import androidx.health.connect.client.records.CervicalMucusRecord
+import androidx.health.connect.client.records.CervicalMucusRecord.Companion.APPEARANCE_STRING_TO_INT_MAP
+import androidx.health.connect.client.records.CervicalMucusRecord.Companion.SENSATION_STRING_TO_INT_MAP
import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
import androidx.health.connect.client.records.DistanceRecord
import androidx.health.connect.client.records.ElevationGainedRecord
@@ -155,8 +157,18 @@
)
"CervicalMucus" ->
CervicalMucusRecord(
- appearance = getEnum("texture"),
- sensation = getEnum("amount"),
+ appearance =
+ mapEnum(
+ "texture",
+ APPEARANCE_STRING_TO_INT_MAP,
+ CervicalMucusRecord.APPEARANCE_UNKNOWN
+ ),
+ sensation =
+ mapEnum(
+ "amount",
+ SENSATION_STRING_TO_INT_MAP,
+ CervicalMucusRecord.SENSATION_UNKNOWN
+ ),
time = time,
zoneOffset = zoneOffset,
metadata = metadata
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordUtils.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordUtils.kt
index 25f805d..9894ddb 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordUtils.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordUtils.kt
@@ -65,6 +65,16 @@
return valuesMap[key]?.enumVal
}
+/** Maps a string enum field to public API integers. */
+internal fun DataPointOrBuilder.mapEnum(
+ key: String,
+ stringToIntMap: Map<String, Int>,
+ default: Int
+): Int {
+ val value = getEnum(key) ?: return default
+ return stringToIntMap.getOrDefault(value, default)
+}
+
internal fun SeriesValueOrBuilder.getLong(key: String, defaultVal: Long = 0): Long =
valuesMap[key]?.longVal ?: defaultVal
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
index e00d9d2..5f85f66 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
@@ -28,6 +28,8 @@
import androidx.health.connect.client.records.BodyWaterMassRecord
import androidx.health.connect.client.records.BoneMassRecord
import androidx.health.connect.client.records.CervicalMucusRecord
+import androidx.health.connect.client.records.CervicalMucusRecord.Companion.APPEARANCE_INT_TO_STRING_MAP
+import androidx.health.connect.client.records.CervicalMucusRecord.Companion.SENSATION_INT_TO_STRING_MAP
import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
import androidx.health.connect.client.records.DistanceRecord
import androidx.health.connect.client.records.ElevationGainedRecord
@@ -136,8 +138,12 @@
instantaneousProto()
.setDataType(protoDataType("CervicalMucus"))
.apply {
- appearance?.let { putValues("texture", enumVal(it)) }
- sensation?.let { putValues("amount", enumVal(it)) }
+ enumValFromInt(appearance, APPEARANCE_INT_TO_STRING_MAP)?.let {
+ putValues("texture", it)
+ }
+ enumValFromInt(sensation, SENSATION_INT_TO_STRING_MAP)?.let {
+ putValues("amount", it)
+ }
}
.build()
is CyclingPedalingCadenceRecord ->
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ValueExt.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ValueExt.kt
index d32fdfb..b2b38cad 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ValueExt.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ValueExt.kt
@@ -36,3 +36,7 @@
internal fun enumVal(value: String): DataProto.Value =
DataProto.Value.newBuilder().setEnumVal(value).build()
+
+internal fun enumValFromInt(value: Int, intToStringMap: Map<Int, String>): DataProto.Value? {
+ return intToStringMap[value]?.let(::enumVal)
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/CervicalMucusRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/CervicalMucusRecord.kt
index ccd7386..c1cc1d0 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/CervicalMucusRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/CervicalMucusRecord.kt
@@ -13,10 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+@file:RestrictTo(RestrictTo.Scope.LIBRARY)
+
package androidx.health.connect.client.records
+import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
-import androidx.annotation.StringDef
import androidx.health.connect.client.records.CervicalMucusRecord.Appearances
import androidx.health.connect.client.records.CervicalMucusRecord.Sensations
import androidx.health.connect.client.records.metadata.Metadata
@@ -31,46 +33,64 @@
public class CervicalMucusRecord(
override val time: Instant,
override val zoneOffset: ZoneOffset?,
- /**
- * The consistency of the user's cervical mucus. Optional field. Allowed values: [Appearance].
- *
- * @see Appearance
- */
- @property:Appearances public val appearance: String? = null,
- /**
- * The feel of the user's cervical mucus. Optional field. Allowed values: [Sensation].
- *
- * @see Sensation
- */
- @property:Sensations public val sensation: String? = null,
+ /** The consistency of the user's cervical mucus. */
+ @property:Appearances public val appearance: Int = APPEARANCE_UNKNOWN,
+ /** The feel of the user's cervical mucus. */
+ @property:Sensations public val sensation: Int = SENSATION_UNKNOWN,
override val metadata: Metadata = Metadata.EMPTY,
) : InstantaneousRecord {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is CervicalMucusRecord) return false
+ companion object {
+ const val APPEARANCE_UNKNOWN = 0
+ const val APPEARANCE_DRY = 1
+ const val APPEARANCE_STICKY = 2
+ const val APPEARANCE_CREAMY = 3
+ const val APPEARANCE_WATERY = 4
+ /** A constant describing clear or egg white like looking cervical mucus. */
+ const val APPEARANCE_EGG_WHITE = 5
+ /** A constant describing an unusual (worth attention) kind of cervical mucus. */
+ const val APPEARANCE_UNUSUAL = 6
- if (appearance != other.appearance) return false
- if (sensation != other.sensation) return false
- if (time != other.time) return false
- if (zoneOffset != other.zoneOffset) return false
- if (metadata != other.metadata) return false
+ const val SENSATION_UNKNOWN = 0
+ const val SENSATION_LIGHT = 1
+ const val SENSATION_MEDIUM = 2
+ const val SENSATION_HEAVY = 3
- return true
- }
+ /** Internal mappings useful for interoperability between integers and strings. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val APPEARANCE_STRING_TO_INT_MAP: Map<String, Int> =
+ mapOf(
+ Appearance.CLEAR to APPEARANCE_EGG_WHITE,
+ Appearance.CREAMY to APPEARANCE_CREAMY,
+ Appearance.DRY to APPEARANCE_DRY,
+ Appearance.STICKY to APPEARANCE_STICKY,
+ Appearance.WATERY to APPEARANCE_WATERY,
+ Appearance.UNUSUAL to APPEARANCE_UNUSUAL
+ )
- override fun hashCode(): Int {
- var result = 0
- result = 31 * result + appearance.hashCode()
- result = 31 * result + sensation.hashCode()
- result = 31 * result + time.hashCode()
- result = 31 * result + (zoneOffset?.hashCode() ?: 0)
- result = 31 * result + metadata.hashCode()
- return result
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val APPEARANCE_INT_TO_STRING_MAP =
+ APPEARANCE_STRING_TO_INT_MAP.entries.associate { it.value to it.key }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val SENSATION_STRING_TO_INT_MAP: Map<String, Int> =
+ mapOf(
+ Sensation.LIGHT to SENSATION_LIGHT,
+ Sensation.MEDIUM to SENSATION_MEDIUM,
+ Sensation.HEAVY to SENSATION_HEAVY
+ )
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val SENSATION_INT_TO_STRING_MAP =
+ SENSATION_STRING_TO_INT_MAP.entries.associate { it.value to it.key }
}
/** List of supported Cervical Mucus Sensation types on Health Platform. */
- public object Sensation {
+ internal object Sensation {
const val LIGHT = "light"
const val MEDIUM = "medium"
const val HEAVY = "heavy"
@@ -81,24 +101,18 @@
* @suppress
*/
@Retention(AnnotationRetention.SOURCE)
- @StringDef(
- value =
- [
- Sensation.LIGHT,
- Sensation.MEDIUM,
- Sensation.HEAVY,
- ]
- )
+ @IntDef(value = [SENSATION_UNKNOWN, SENSATION_LIGHT, SENSATION_MEDIUM, SENSATION_HEAVY])
@RestrictTo(RestrictTo.Scope.LIBRARY)
annotation class Sensations
/** The consistency or appearance of the user's cervical mucus. */
- public object Appearance {
+ internal object Appearance {
const val DRY = "dry"
const val STICKY = "sticky"
const val CREAMY = "creamy"
const val WATERY = "watery"
const val CLEAR = "clear"
+ const val UNUSUAL = "unusual"
}
/**
@@ -106,16 +120,42 @@
* @suppress
*/
@Retention(AnnotationRetention.SOURCE)
- @StringDef(
+ @IntDef(
value =
[
- Appearance.DRY,
- Appearance.STICKY,
- Appearance.CREAMY,
- Appearance.WATERY,
- Appearance.CLEAR,
+ APPEARANCE_UNKNOWN,
+ APPEARANCE_DRY,
+ APPEARANCE_STICKY,
+ APPEARANCE_CREAMY,
+ APPEARANCE_WATERY,
+ APPEARANCE_EGG_WHITE,
+ APPEARANCE_UNUSUAL
]
)
@RestrictTo(RestrictTo.Scope.LIBRARY)
annotation class Appearances
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as CervicalMucusRecord
+
+ if (time != other.time) return false
+ if (zoneOffset != other.zoneOffset) return false
+ if (appearance != other.appearance) return false
+ if (sensation != other.sensation) return false
+ if (metadata != other.metadata) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = time.hashCode()
+ result = 31 * result + (zoneOffset?.hashCode() ?: 0)
+ result = 31 * result + appearance
+ result = 31 * result + sensation
+ result = 31 * result + metadata.hashCode()
+ return result
+ }
}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
index be9cdbe..cdde08f 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
@@ -27,8 +27,6 @@
import androidx.health.connect.client.records.BodyWaterMassRecord
import androidx.health.connect.client.records.BoneMassRecord
import androidx.health.connect.client.records.CervicalMucusRecord
-import androidx.health.connect.client.records.CervicalMucusRecord.Appearance
-import androidx.health.connect.client.records.CervicalMucusRecord.Sensation
import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
import androidx.health.connect.client.records.DistanceRecord
import androidx.health.connect.client.records.ElevationGainedRecord
@@ -254,8 +252,8 @@
fun testCervicalMucus() {
val data =
CervicalMucusRecord(
- appearance = Appearance.CLEAR,
- sensation = Sensation.HEAVY,
+ appearance = CervicalMucusRecord.APPEARANCE_EGG_WHITE,
+ sensation = CervicalMucusRecord.SENSATION_HEAVY,
time = START_TIME,
zoneOffset = END_ZONE_OFFSET,
metadata = TEST_METADATA
diff --git a/hilt/hilt-navigation-compose/build.gradle b/hilt/hilt-navigation-compose/build.gradle
index 0fad8e6..83fb1c1 100644
--- a/hilt/hilt-navigation-compose/build.gradle
+++ b/hilt/hilt-navigation-compose/build.gradle
@@ -36,7 +36,7 @@
dependencies {
implementation(libs.kotlinStdlib)
- api("androidx.hilt:hilt-navigation:1.0.0")
+ api projectOrArtifact(":hilt:hilt-navigation")
api("androidx.compose.runtime:runtime:1.0.1")
api("androidx.compose.ui:ui:1.0.1")
api("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
diff --git a/hilt/hilt-navigation-compose/src/androidTest/java/androidx/hilt/navigation/compose/HiltViewModelComposeTest.kt b/hilt/hilt-navigation-compose/src/androidTest/java/androidx/hilt/navigation/compose/HiltViewModelComposeTest.kt
index 403a9d8..b2fb118 100644
--- a/hilt/hilt-navigation-compose/src/androidTest/java/androidx/hilt/navigation/compose/HiltViewModelComposeTest.kt
+++ b/hilt/hilt-navigation-compose/src/androidTest/java/androidx/hilt/navigation/compose/HiltViewModelComposeTest.kt
@@ -16,6 +16,7 @@
package androidx.hilt.navigation.compose
+import android.content.Context
import androidx.activity.ComponentActivity
import androidx.compose.material.Button
import androidx.compose.material.Text
@@ -34,6 +35,7 @@
import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Rule
@@ -171,7 +173,10 @@
@HiltViewModel
class SimpleViewModel @Inject constructor(
val handle: SavedStateHandle,
- val logger: MyLogger
+ val logger: MyLogger,
+ // TODO(kuanyingchou) Remove this after https://github.com/google/dagger/issues/3601 is
+ // resolved.
+ @ApplicationContext val context: Context
) : ViewModel()
class MyLogger @Inject constructor()
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/SingleFragmentTestActivity.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SingleFragmentTestActivity.java
index 9b052a5..2f3d994 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/SingleFragmentTestActivity.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SingleFragmentTestActivity.java
@@ -70,7 +70,8 @@
if (savedInstanceState == null && findViewById(R.id.main_frame) != null) {
try {
Fragment fragment = (Fragment) Class.forName(
- intent.getStringExtra(EXTRA_FRAGMENT_NAME)).newInstance();
+ intent.getStringExtra(EXTRA_FRAGMENT_NAME))
+ .getDeclaredConstructor().newInstance();
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.main_frame, fragment);
ft.commit();
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/SingleSupportFragmentTestActivity.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SingleSupportFragmentTestActivity.java
index 54440c6..e63e568 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/SingleSupportFragmentTestActivity.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SingleSupportFragmentTestActivity.java
@@ -67,7 +67,8 @@
if (savedInstanceState == null && findViewById(R.id.main_frame) != null) {
try {
Fragment fragment = (Fragment) Class.forName(
- intent.getStringExtra(EXTRA_FRAGMENT_NAME)).newInstance();
+ intent.getStringExtra(EXTRA_FRAGMENT_NAME))
+ .getDeclaredConstructor().newInstance();
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.main_frame, fragment);
ft.commit();
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/TestActivity.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/TestActivity.java
index cc83090..512db7e 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/TestActivity.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/TestActivity.java
@@ -128,7 +128,8 @@
super.onCreate(savedInstanceState);
mProviderName = getIntent().getStringExtra(EXTRA_PROVIDER);
try {
- mProvider = (Provider) Class.forName(mProviderName).newInstance();
+ mProvider = (Provider) Class.forName(mProviderName)
+ .getDeclaredConstructor().newInstance();
} catch (Exception ex) {
ex.printStackTrace();
}
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridActivity.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridActivity.java
index 03200f2..cfc1991 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridActivity.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridActivity.java
@@ -32,6 +32,7 @@
import androidx.recyclerview.widget.ConcatAdapter;
import androidx.recyclerview.widget.RecyclerView;
+import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
public class GridActivity extends Activity {
@@ -167,15 +168,15 @@
try {
if (alignmentClass != null) {
mAlignmentProvider = (GridWidgetTest.ItemAlignmentFacetProvider)
- Class.forName(alignmentClass).newInstance();
+ Class.forName(alignmentClass).getDeclaredConstructor().newInstance();
}
if (alignmentViewTypeClass != null) {
mAlignmentViewTypeProvider = (GridWidgetTest.ItemAlignmentFacetProvider)
- Class.forName(alignmentViewTypeClass).newInstance();
+ Class.forName(alignmentViewTypeClass).getDeclaredConstructor().newInstance();
}
if (viewTypeClass != null) {
mViewTypeProvider = (GridWidgetTest.ViewTypeProvider)
- Class.forName(viewTypeClass).newInstance();
+ Class.forName(viewTypeClass).getDeclaredConstructor().newInstance();
}
} catch (ClassNotFoundException ex) {
throw new RuntimeException(ex);
@@ -183,6 +184,10 @@
throw new RuntimeException(ex);
} catch (IllegalAccessException ex) {
throw new RuntimeException(ex);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e);
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
}
super.onCreate(savedInstanceState);
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateFactoryTest.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateFactoryTest.kt
index 3eefd71..59b07b8 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateFactoryTest.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateFactoryTest.kt
@@ -119,11 +119,8 @@
try {
vm[MyAndroidViewModel::class.java]
fail("Creating an AndroidViewModel should fail when no Application is provided")
- } catch (e: RuntimeException) {
- assertThat(e).hasMessageThat().isEqualTo(
- "Cannot create an instance of " +
- MyAndroidViewModel::class.java
- )
+ } catch (e: NoSuchMethodException) {
+ // Exception message varies across platform versions, just make sure it's thrown.
}
assertThat(vm[MyViewModel::class.java].handle).isNotNull()
}
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelProvider.kt b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelProvider.kt
index fb5616b..ae7b4ab 100644
--- a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelProvider.kt
+++ b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelProvider.kt
@@ -199,7 +199,7 @@
@Suppress("DocumentExceptions")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return try {
- modelClass.newInstance()
+ modelClass.getDeclaredConstructor().newInstance()
} catch (e: InstantiationException) {
throw RuntimeException("Cannot create an instance of $modelClass", e)
} catch (e: IllegalAccessException) {
diff --git a/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderReifiedTest.kt b/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderReifiedTest.kt
index 0a702cf..2862a73 100644
--- a/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderReifiedTest.kt
+++ b/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderReifiedTest.kt
@@ -28,7 +28,8 @@
@Test
fun providerReifiedGet() {
val factory = object : ViewModelProvider.Factory {
- override fun <T : ViewModel> create(modelClass: Class<T>) = modelClass.newInstance()
+ override fun <T : ViewModel> create(modelClass: Class<T>) = modelClass
+ .getDeclaredConstructor().newInstance()
}
val provider = ViewModelProvider(ViewModelStore(), factory)
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index bfd8234..a39575d 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -25,7 +25,7 @@
kotlin.code.style=official
# Disable docs
androidx.enableDocumentation=false
-androidx.playground.snapshotBuildId=9163047
+androidx.playground.snapshotBuildId=9189666
androidx.playground.metalavaBuildId=8993580
androidx.playground.dokkaBuildId=7472101
androidx.studio.type=playground
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
index 38f9a0c..0b3528e 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
@@ -18,6 +18,7 @@
import androidx.privacysandbox.tools.core.generator.build
import androidx.privacysandbox.tools.core.generator.poetSpec
+import androidx.privacysandbox.tools.core.generator.stubDelegateNameSpec
import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import androidx.privacysandbox.tools.core.model.ParsedApi
import androidx.privacysandbox.tools.core.model.getOnlyService
@@ -69,8 +70,9 @@
"val sdk = ${getCreateServiceFunctionName(api.getOnlyService())}(context!!)"
)
addStatement(
- "return ${SANDBOXED_SDK_CLASS.simpleName}" +
- "(${api.getOnlyService().stubDelegateName()}(sdk))"
+ "return %T(%T(sdk))",
+ SANDBOXED_SDK_CLASS,
+ api.getOnlyService().stubDelegateNameSpec()
)
}
}
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
index 4195f09..55185ab 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
@@ -19,8 +19,9 @@
import androidx.privacysandbox.tools.core.model.ParsedApi
import androidx.privacysandbox.tools.core.generator.AidlCompiler
import androidx.privacysandbox.tools.core.generator.AidlGenerator
+import androidx.privacysandbox.tools.core.generator.ClientProxyTypeGenerator
+import androidx.privacysandbox.tools.core.generator.StubDelegatesGenerator
import androidx.privacysandbox.tools.core.generator.ValueConverterFileGenerator
-import androidx.privacysandbox.tools.core.generator.converterNameSpec
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import com.squareup.kotlinpoet.FileSpec
@@ -35,17 +36,14 @@
fun generate() {
generateAidlSources()
generateValueConverters()
- AbstractSdkProviderGenerator(api).generate()?.also(::write)
- StubDelegatesGenerator(api).generate().forEach(::write)
+ generateCallbackProxies()
+ generateAbstractSdkProvider()
+ generateStubDelegates()
}
private fun generateValueConverters() {
api.values.forEach { value ->
- val file = ValueConverterFileGenerator(api, value).generate()
- codeGenerator.createNewFile(
- Dependencies(false), value.converterNameSpec().packageName,
- value.converterNameSpec().simpleName,
- ).write(file)
+ write(ValueConverterFileGenerator(api, value).generate())
}
}
@@ -69,6 +67,24 @@
}
}
+ private fun generateCallbackProxies() {
+ for (callback in api.callbacks) {
+ val classSpec = ClientProxyTypeGenerator(api, callback).generate()
+ write(
+ FileSpec.builder(callback.type.packageName, classSpec.name!!).addType(classSpec)
+ .build()
+ )
+ }
+ }
+
+ private fun generateAbstractSdkProvider() {
+ AbstractSdkProviderGenerator(api).generate()?.also(::write)
+ }
+
+ private fun generateStubDelegates() {
+ StubDelegatesGenerator(api).generate().forEach(::write)
+ }
+
private fun write(spec: FileSpec) {
codeGenerator.createNewFile(Dependencies(false), spec.packageName, spec.name)
.write(spec)
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
index 8b88411..77da6f1 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
@@ -58,6 +58,8 @@
"com/mysdk/ParcelableRequest.java",
"com/mysdk/ParcelableResponse.java",
"com/mysdk/IResponseTransactionCallback.java",
+ "com/mysdk/MyCallbackClientProxy.kt",
+ "com/mysdk/IMyCallback.java",
)
}.also {
it.generatesSourcesWithContents(expectedOutput)
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt
index 8a358cf..b0cc37c 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt
@@ -1,5 +1,6 @@
package com.mysdk
+import androidx.privacysandbox.tools.PrivacySandboxCallback
import androidx.privacysandbox.tools.PrivacySandboxService
import androidx.privacysandbox.tools.PrivacySandboxValue
@@ -11,6 +12,8 @@
suspend fun logRequest(request: Request)
+ fun setListener(listener: MyCallback)
+
fun doMoreStuff()
}
@@ -18,4 +21,11 @@
data class Request(val query: String)
@PrivacySandboxValue
-data class Response(val response: String)
\ No newline at end of file
+data class Response(val response: String)
+
+@PrivacySandboxCallback
+interface MyCallback {
+ fun onComplete(response: Response)
+
+ fun onClick(x: Int, y: Int)
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyCallbackClientProxy.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyCallbackClientProxy.kt
new file mode 100644
index 0000000..351984d
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyCallbackClientProxy.kt
@@ -0,0 +1,17 @@
+package com.mysdk
+
+import com.mysdk.ResponseConverter.toParcelable
+import kotlin.Int
+import kotlin.Unit
+
+internal class MyCallbackClientProxy(
+ private val remote: IMyCallback,
+) : MyCallback {
+ public override fun onComplete(response: Response): Unit {
+ remote.onComplete(toParcelable(response))
+ }
+
+ public override fun onClick(x: Int, y: Int): Unit {
+ remote.onClick(x, y)
+ }
+}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt
index 84a9d3b..2626f44 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt
@@ -60,6 +60,10 @@
transactionCallback.onCancellable(cancellationSignal)
}
+ public override fun setListener(listener: IMyCallback): Unit {
+ delegate.setListener(MyCallbackClientProxy(listener))
+ }
+
public override fun doMoreStuff(): Unit {
delegate.doMoreStuff()
}
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceInterfaceFileGenerator.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt
similarity index 81%
rename from privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceInterfaceFileGenerator.kt
rename to privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt
index c0a5e9e..9c431f0 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceInterfaceFileGenerator.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt
@@ -26,15 +26,16 @@
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.TypeSpec
-internal class ServiceInterfaceFileGenerator(private val service: AnnotatedInterface) {
+internal class InterfaceFileGenerator(private val annotatedInterface: AnnotatedInterface) {
fun generate(): FileSpec {
val annotatedInterface =
- TypeSpec.interfaceBuilder(service.type.poetSpec()).build {
- addFunctions(service.methods.map(::generateInterfaceMethod))
+ TypeSpec.interfaceBuilder(annotatedInterface.type.poetSpec()).build {
+ addFunctions(annotatedInterface.methods.map(::generateInterfaceMethod))
}
- return FileSpec.get(service.type.packageName, annotatedInterface).toBuilder().build {
+ return FileSpec.get(this.annotatedInterface.type.packageName, annotatedInterface)
+ .toBuilder().build {
addCommonSettings()
}
}
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/PrivacySandboxApiGenerator.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/PrivacySandboxApiGenerator.kt
index 0affdc17..734b235 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/PrivacySandboxApiGenerator.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/PrivacySandboxApiGenerator.kt
@@ -20,6 +20,7 @@
import androidx.privacysandbox.tools.core.model.ParsedApi
import androidx.privacysandbox.tools.core.generator.AidlCompiler
import androidx.privacysandbox.tools.core.generator.AidlGenerator
+import androidx.privacysandbox.tools.core.generator.StubDelegatesGenerator
import androidx.privacysandbox.tools.core.generator.ValueConverterFileGenerator
import androidx.privacysandbox.tools.core.generator.ValueFileGenerator
import java.io.File
@@ -59,9 +60,13 @@
ValueConverterFileGenerator(sdkApi, it).generate().writeTo(output)
}
sdkApi.services.forEach {
- ServiceInterfaceFileGenerator(it).generate().writeTo(output)
+ InterfaceFileGenerator(it).generate().writeTo(output)
ServiceFactoryFileGenerator(sdkApi, it).generate().writeTo(output)
}
+ sdkApi.callbacks.forEach {
+ InterfaceFileGenerator(it).generate().writeTo(output)
+ StubDelegatesGenerator(sdkApi).generateInterfaceStubDelegate(it).writeTo(output)
+ }
}
private fun generateBinders(sdkApi: ParsedApi, aidlCompiler: AidlCompiler, output: File) {
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceFactoryFileGenerator.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceFactoryFileGenerator.kt
index b927183..443875e 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceFactoryFileGenerator.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceFactoryFileGenerator.kt
@@ -47,7 +47,7 @@
addFunction(generateFactoryFunction())
- addType(proxyTypeGenerator.generate())
+ addType(proxyTypeGenerator.generate(KModifier.PRIVATE))
}
private fun generateFactoryFunction() =
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/CallbacksApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/CallbacksApiGeneratorTest.kt
new file mode 100644
index 0000000..bc17d9d
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/CallbacksApiGeneratorTest.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.privacysandbox.tools.apigenerator
+
+import java.io.File
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class CallbacksApiGeneratorTest : BaseApiGeneratorTest() {
+ override val inputDirectory = File("src/test/test-data/callbacks/input")
+ override val outputDirectory = File("src/test/test-data/callbacks/output")
+ override val relativePathsToExpectedAidlClasses = listOf(
+ "com/sdkwithcallbacks/ParcelableResponse.java",
+ "com/sdkwithcallbacks/ISdkCallback.java",
+ "com/sdkwithcallbacks/ICancellationSignal.java",
+ "com/sdkwithcallbacks/IUnitTransactionCallback.java",
+ "com/sdkwithcallbacks/ISdkService.java",
+ )
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/input/com/sdkwithcallbacks/SdkService.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/input/com/sdkwithcallbacks/SdkService.kt
new file mode 100644
index 0000000..2bc8ff1
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/input/com/sdkwithcallbacks/SdkService.kt
@@ -0,0 +1,22 @@
+package com.sdkwithcallbacks
+
+import androidx.privacysandbox.tools.PrivacySandboxService
+import androidx.privacysandbox.tools.PrivacySandboxCallback
+import androidx.privacysandbox.tools.PrivacySandboxValue
+
+@PrivacySandboxService
+interface SdkService {
+ suspend fun registerCallback(callback: SdkCallback)
+}
+
+@PrivacySandboxCallback
+interface SdkCallback {
+ fun onValueReceived(response: Response)
+
+ fun onPrimitivesReceived(x: Int, y: Int)
+
+ fun onEmptyEvent()
+}
+
+@PrivacySandboxValue
+data class Response(val response: String)
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/Response.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/Response.kt
new file mode 100644
index 0000000..569c215
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/Response.kt
@@ -0,0 +1,5 @@
+package com.sdkwithcallbacks
+
+public data class Response(
+ public val response: String,
+)
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/ResponseConverter.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/ResponseConverter.kt
new file mode 100644
index 0000000..e1d05a5
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/ResponseConverter.kt
@@ -0,0 +1,15 @@
+package com.sdkwithcallbacks
+
+public object ResponseConverter {
+ public fun fromParcelable(parcelable: ParcelableResponse): Response {
+ val annotatedValue = Response(
+ response = parcelable.response)
+ return annotatedValue
+ }
+
+ public fun toParcelable(annotatedValue: Response): ParcelableResponse {
+ val parcelable = ParcelableResponse()
+ parcelable.response = annotatedValue.response
+ return parcelable
+ }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallback.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallback.kt
new file mode 100644
index 0000000..da06ec7
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallback.kt
@@ -0,0 +1,9 @@
+package com.sdkwithcallbacks
+
+public interface SdkCallback {
+ public fun onEmptyEvent(): Unit
+
+ public fun onPrimitivesReceived(x: Int, y: Int): Unit
+
+ public fun onValueReceived(response: Response): Unit
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallbackStubDelegate.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallbackStubDelegate.kt
new file mode 100644
index 0000000..e4a39953
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallbackStubDelegate.kt
@@ -0,0 +1,21 @@
+package com.sdkwithcallbacks
+
+import com.sdkwithcallbacks.ResponseConverter.fromParcelable
+import kotlin.Int
+import kotlin.Unit
+
+public class SdkCallbackStubDelegate internal constructor(
+ private val `delegate`: SdkCallback,
+) : ISdkCallback.Stub() {
+ public override fun onEmptyEvent(): Unit {
+ delegate.onEmptyEvent()
+ }
+
+ public override fun onPrimitivesReceived(x: Int, y: Int): Unit {
+ delegate.onPrimitivesReceived(x, y)
+ }
+
+ public override fun onValueReceived(response: ParcelableResponse): Unit {
+ delegate.onValueReceived(fromParcelable(response))
+ }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkService.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkService.kt
new file mode 100644
index 0000000..8363e3d
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkService.kt
@@ -0,0 +1,5 @@
+package com.sdkwithcallbacks
+
+public interface SdkService {
+ public suspend fun registerCallback(callback: SdkCallback): Unit
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceFactory.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceFactory.kt
new file mode 100644
index 0000000..66c836d
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceFactory.kt
@@ -0,0 +1,51 @@
+package com.sdkwithcallbacks
+
+import android.app.sdksandbox.LoadSdkException
+import android.app.sdksandbox.SandboxedSdk
+import android.app.sdksandbox.SdkSandboxManager
+import android.content.Context
+import android.os.Bundle
+import android.os.OutcomeReceiver
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+public suspend fun createSdkService(context: Context): SdkService = suspendCancellableCoroutine {
+ val sdkSandboxManager = context.getSystemService(SdkSandboxManager::class.java)
+ val outcomeReceiver = object: OutcomeReceiver<SandboxedSdk, LoadSdkException> {
+ override fun onResult(result: SandboxedSdk) {
+ it.resume(SdkServiceClientProxy(ISdkService.Stub.asInterface(result.getInterface())))
+ }
+ override fun onError(error: LoadSdkException) {
+ it.resumeWithException(error)
+ }
+ }
+ sdkSandboxManager.loadSdk("com.sdkwithcallbacks", Bundle.EMPTY, Runnable::run, outcomeReceiver)
+}
+
+private class SdkServiceClientProxy(
+ private val remote: ISdkService,
+) : SdkService {
+ public override suspend fun registerCallback(callback: SdkCallback): Unit =
+ suspendCancellableCoroutine {
+ var mCancellationSignal: ICancellationSignal? = null
+ val transactionCallback = object: IUnitTransactionCallback.Stub() {
+ override fun onCancellable(cancellationSignal: ICancellationSignal) {
+ if (it.isCancelled) {
+ cancellationSignal.cancel()
+ }
+ mCancellationSignal = cancellationSignal
+ }
+ override fun onSuccess() {
+ it.resumeWith(Result.success(Unit))
+ }
+ override fun onFailure(errorCode: Int, errorMessage: String) {
+ it.resumeWithException(RuntimeException(errorMessage))
+ }
+ }
+ remote.registerCallback(SdkCallbackStubDelegate(callback), transactionCallback)
+ it.invokeOnCancellation {
+ mCancellationSignal?.cancel()
+ }
+ }
+}
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt
new file mode 100644
index 0000000..ebf87d1
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.privacysandbox.tools.core.generator
+
+import androidx.privacysandbox.tools.core.model.Parameter
+import androidx.privacysandbox.tools.core.model.ParsedApi
+import androidx.privacysandbox.tools.core.model.Type
+import androidx.privacysandbox.tools.core.model.Types
+import com.squareup.kotlinpoet.ClassName
+import com.squareup.kotlinpoet.CodeBlock
+
+/** Utility to generate [CodeBlock]s that convert values to/from their binder equivalent. */
+class BinderCodeConverter(private val api: ParsedApi) {
+
+ /**
+ * Generate a block that converts the given expression from the binder representation to its
+ * model equivalent.
+ *
+ * @param type the type of the resulting expression. Can reference a primitive on annotated
+ * interface/value.
+ * @param expression the expression to be converted.
+ * @return a [CodeBlock] containing the generated code to perform the conversion.
+ */
+ fun convertToModelCode(type: Type, expression: String): CodeBlock {
+ require(type != Types.unit) { "Cannot convert Unit." }
+ val value = api.valueMap[type]
+ if (value != null) {
+ return CodeBlock.of("%M(%L)", value.fromParcelableNameSpec(), expression)
+ }
+ val callback = api.callbackMap[type]
+ if (callback != null) {
+ return CodeBlock.of("%T(%L)", callback.clientProxyNameSpec(), expression)
+ }
+ return CodeBlock.of(expression)
+ }
+
+ /**
+ * Convert the given [Parameter] from the binder representation to its model equivalent.
+ *
+ * See [convertToModelCode].
+ */
+ fun convertToModelCode(parameter: Parameter): CodeBlock {
+ return convertToModelCode(parameter.type, parameter.name)
+ }
+
+ /**
+ * Generate a block that converts the given expression from the model representation to its
+ * binder equivalent.
+ *
+ * @param type the type of the given expression. Can reference a primitive on annotated
+ * interface/value.
+ * @param expression the expression to be converted.
+ * @return a [CodeBlock] containing the generated code to perform the conversion.
+ */
+ fun convertToBinderCode(type: Type, expression: String): CodeBlock {
+ require(type != Types.unit) { "Cannot convert to Unit." }
+ val value = api.valueMap[type]
+ if (value != null) {
+ return CodeBlock.of("%M(%L)", value.toParcelableNameSpec(), expression)
+ }
+ val callback = api.callbackMap[type]
+ if (callback != null) {
+ return CodeBlock.of("%T(%L)", callback.stubDelegateNameSpec(), expression)
+ }
+ return CodeBlock.of(expression)
+ }
+
+ /**
+ * Convert the given [Parameter] from the model representation to its binder equivalent.
+ *
+ * See [convertToBinderCode].
+ */
+ fun convertToBinderCode(parameter: Parameter): CodeBlock {
+ return convertToBinderCode(parameter.type, parameter.name)
+ }
+
+ /** Convert the given model type declaration to its binder equivalent. */
+ fun convertToBinderType(type: Type): ClassName {
+ val value = api.valueMap[type]
+ if (value != null) {
+ return value.parcelableNameSpec()
+ }
+ val callback = api.callbackMap[type]
+ if (callback != null) {
+ return callback.aidlType().innerType.poetSpec()
+ }
+ return type.poetSpec()
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
index dbd3473..a234f9c 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
@@ -32,30 +32,31 @@
private val api: ParsedApi,
private val service: AnnotatedInterface
) {
- val className =
- ClassName(service.type.packageName, "${service.type.simpleName}ClientProxy")
+ val className = service.clientProxyNameSpec()
val remoteBinderClassName =
ClassName(service.type.packageName, "I${service.type.simpleName}")
private val cancellationSignalClassName =
ClassName(service.type.packageName, "ICancellationSignal")
+ private val binderCodeConverter = BinderCodeConverter(api)
- fun generate(): TypeSpec = TypeSpec.classBuilder(className).build {
- addModifiers(KModifier.PRIVATE)
- addSuperinterface(ClassName(service.type.packageName, service.type.simpleName))
- primaryConstructor(
- listOf(
- PropertySpec.builder("remote", remoteBinderClassName)
- .addModifiers(KModifier.PRIVATE).build()
+ fun generate(visibilityModifier: KModifier = KModifier.INTERNAL): TypeSpec =
+ TypeSpec.classBuilder(className).build {
+ addModifiers(visibilityModifier)
+ addSuperinterface(ClassName(service.type.packageName, service.type.simpleName))
+ primaryConstructor(
+ listOf(
+ PropertySpec.builder("remote", remoteBinderClassName)
+ .addModifiers(KModifier.PRIVATE).build()
+ )
)
- )
- addFunctions(service.methods.map { method ->
- if (method.isSuspend) {
- generateSuspendProxyMethodImplementation(method)
- } else {
- generateProxyMethodImplementation(method)
- }
- })
- }
+ addFunctions(service.methods.map { method ->
+ if (method.isSuspend) {
+ generateSuspendProxyMethodImplementation(method)
+ } else {
+ generateProxyMethodImplementation(method)
+ }
+ })
+ }
private fun generateProxyMethodImplementation(method: Method) =
FunSpec.builder(method.name).build {
@@ -124,25 +125,15 @@
}
}
- val value = api.valueMap[method.returnType]
- if (value != null) {
- return CodeBlock.builder().build {
- addControlFlow(
- "override fun onSuccess(result: %T)",
- value.parcelableNameSpec()
- ) {
- addStatement("it.resumeWith(Result.success(%M(result)))",
- value.fromParcelableNameSpec())
- }
- }
- }
-
return CodeBlock.builder().build {
addControlFlow(
"override fun onSuccess(result: %T)",
- method.returnType.poetSpec()
+ binderCodeConverter.convertToBinderType(method.returnType)
) {
- addStatement("it.resumeWith(Result.success(result))")
+ addStatement(
+ "it.resumeWith(Result.success(%L))",
+ binderCodeConverter.convertToModelCode(method.returnType, "result")
+ )
}
}
}
@@ -151,11 +142,8 @@
method: Method,
extraParameters: List<CodeBlock> = emptyList(),
) = CodeBlock.builder().build {
- val parameters = method.parameters.map { parameter ->
- api.valueMap[parameter.type]?.toParcelableNameSpec()?.let { converterFunctionName ->
- CodeBlock.of("%M(${parameter.name})", converterFunctionName)
- } ?: CodeBlock.of(parameter.name)
- } + extraParameters
+ val parameters =
+ method.parameters.map { binderCodeConverter.convertToBinderCode(it) } + extraParameters
addStatement {
add("remote.${method.name}(")
add(parameters.joinToCode())
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt
index 85cbd2f..17a226a 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt
@@ -16,6 +16,7 @@
package androidx.privacysandbox.tools.core.generator
+import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import androidx.privacysandbox.tools.core.model.AnnotatedValue
import androidx.privacysandbox.tools.core.model.Parameter
import androidx.privacysandbox.tools.core.model.Type
@@ -46,6 +47,10 @@
MemberName(converterNameSpec(), "fromParcelable")
fun AnnotatedValue.parcelableNameSpec() =
ClassName(type.packageName, "Parcelable${type.simpleName}")
+fun AnnotatedInterface.clientProxyNameSpec() =
+ ClassName(type.packageName, "${type.simpleName}ClientProxy")
+fun AnnotatedInterface.stubDelegateNameSpec() =
+ ClassName(type.packageName, "${type.simpleName}StubDelegate")
/**
* Defines the primary constructor of this type with the given list of properties.
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/StubDelegatesGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt
similarity index 71%
rename from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/StubDelegatesGenerator.kt
rename to privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt
index c4611bc..333e0732 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/StubDelegatesGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt
@@ -14,21 +14,8 @@
* limitations under the License.
*/
-package androidx.privacysandbox.tools.apicompiler.generator
+package androidx.privacysandbox.tools.core.generator
-import androidx.privacysandbox.tools.core.generator.AidlGenerator
-import androidx.privacysandbox.tools.core.generator.SpecNames
-import androidx.privacysandbox.tools.core.generator.addCode
-import androidx.privacysandbox.tools.core.generator.addControlFlow
-import androidx.privacysandbox.tools.core.generator.addStatement
-import androidx.privacysandbox.tools.core.generator.aidlName
-import androidx.privacysandbox.tools.core.generator.build
-import androidx.privacysandbox.tools.core.generator.toParcelableNameSpec
-import androidx.privacysandbox.tools.core.generator.fromParcelableNameSpec
-import androidx.privacysandbox.tools.core.generator.parcelableNameSpec
-import androidx.privacysandbox.tools.core.generator.poetSpec
-import androidx.privacysandbox.tools.core.generator.primaryConstructor
-import androidx.privacysandbox.tools.core.generator.transactionCallbackName
import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import androidx.privacysandbox.tools.core.model.Method
import androidx.privacysandbox.tools.core.model.ParsedApi
@@ -47,6 +34,8 @@
import com.squareup.kotlinpoet.joinToCode
class StubDelegatesGenerator(private val api: ParsedApi) {
+ private val binderCodeConverter = BinderCodeConverter(api)
+
companion object {
private val ATOMIC_BOOLEAN_CLASS = ClassName("java.util.concurrent.atomic", "AtomicBoolean")
}
@@ -55,12 +44,12 @@
if (api.services.isEmpty()) {
return emptyList()
}
- return api.services.map(::generateServiceStubDelegate) +
+ return api.services.map(::generateInterfaceStubDelegate) +
generateTransportCancellationCallback()
}
- private fun generateServiceStubDelegate(service: AnnotatedInterface): FileSpec {
- val className = service.stubDelegateName()
+ fun generateInterfaceStubDelegate(service: AnnotatedInterface): FileSpec {
+ val className = service.stubDelegateNameSpec().simpleName
val aidlBaseClassName = ClassName(service.type.packageName, service.aidlName(), "Stub")
val classSpec = TypeSpec.classBuilder(className).build {
@@ -104,18 +93,13 @@
add("val result = ")
add(getDelegateCallBlock(method))
}
- val value = api.valueMap[method.returnType]
- when {
- value != null -> {
- addStatement(
- "transactionCallback.onSuccess(%M(result))",
- value.toParcelableNameSpec()
- )
- }
- method.returnType == Types.unit -> {
- addStatement("transactionCallback.onSuccess()")
- }
- else -> addStatement("transactionCallback.onSuccess(result)")
+ if (method.returnType == Types.unit) {
+ addStatement("transactionCallback.onSuccess()")
+ } else {
+ addStatement(
+ "transactionCallback.onSuccess(%L)",
+ binderCodeConverter.convertToBinderCode(method.returnType, "result")
+ )
}
}
addControlFlow("catch (t: Throwable)") {
@@ -138,9 +122,7 @@
private fun getParameters(method: Method) = buildList {
addAll(method.parameters.map { parameter ->
- api.valueMap[parameter.type]?.let { value ->
- ParameterSpec(parameter.name, value.parcelableNameSpec())
- } ?: ParameterSpec(parameter.name, parameter.type.poetSpec())
+ ParameterSpec(parameter.name, binderCodeConverter.convertToBinderType(parameter.type))
})
if (method.isSuspend) add(
ParameterSpec(
@@ -153,16 +135,8 @@
}
private fun getDelegateCallBlock(method: Method) = CodeBlock.builder().build {
- val parameters = method.parameters.map {
- val value = api.valueMap[it.type]
- if (value != null) {
- CodeBlock.of("%M(${it.name})", value.fromParcelableNameSpec())
- } else {
- CodeBlock.of(it.name)
- }
- }
add("delegate.${method.name}(")
- add(parameters.joinToCode())
+ add(method.parameters.map { binderCodeConverter.convertToModelCode(it) }.joinToCode())
add(")")
}
@@ -201,5 +175,3 @@
return FileSpec.builder(packageName, className).addType(classSpec).build()
}
}
-
-internal fun AnnotatedInterface.stubDelegateName() = "${type.simpleName}StubDelegate"
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverterTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverterTest.kt
new file mode 100644
index 0000000..5c0a149
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverterTest.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.privacysandbox.tools.core.generator
+
+import androidx.privacysandbox.tools.core.model.AnnotatedInterface
+import androidx.privacysandbox.tools.core.model.AnnotatedValue
+import androidx.privacysandbox.tools.core.model.ParsedApi
+import androidx.privacysandbox.tools.core.model.Type
+import androidx.privacysandbox.tools.core.model.Types
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class BinderCodeConverterTest {
+ private val converter = BinderCodeConverter(
+ ParsedApi(
+ services = setOf(
+ AnnotatedInterface(
+ type = Type(packageName = "com.mysdk", simpleName = "MySdk"),
+ )
+ ),
+ values = setOf(
+ AnnotatedValue(
+ type = Type(packageName = "com.mysdk", simpleName = "Value"),
+ properties = listOf()
+ )
+ ),
+ callbacks = setOf(
+ AnnotatedInterface(
+ type = Type(packageName = "com.mysdk", simpleName = "Callback"),
+ )
+ ),
+ )
+ )
+
+ @Test
+ fun convertToModelCode_primitive() {
+ assertThat(
+ converter.convertToModelCode(
+ Types.int, expression = "5"
+ ).toString()
+ ).isEqualTo("5")
+ }
+
+ @Test
+ fun convertToModelCode_value() {
+ assertThat(
+ converter.convertToModelCode(
+ Type(packageName = "com.mysdk", simpleName = "Value"), expression = "value"
+ ).toString()
+ ).isEqualTo("com.mysdk.ValueConverter.fromParcelable(value)")
+ }
+
+ @Test
+ fun convertToModelCode_callback() {
+ assertThat(
+ converter.convertToModelCode(
+ Type(packageName = "com.mysdk", simpleName = "Callback"), expression = "callback"
+ ).toString()
+ ).isEqualTo("com.mysdk.CallbackClientProxy(callback)")
+ }
+
+ @Test
+ fun convertToBinderCode_primitive() {
+ assertThat(
+ converter.convertToBinderCode(
+ Types.int, expression = "5"
+ ).toString()
+ ).isEqualTo("5")
+ }
+
+ @Test
+ fun convertToBinderCode_value() {
+ assertThat(
+ converter.convertToBinderCode(
+ Type(packageName = "com.mysdk", simpleName = "Value"), expression = "value"
+ ).toString()
+ ).isEqualTo("com.mysdk.ValueConverter.toParcelable(value)")
+ }
+
+ @Test
+ fun convertToBinderCode_callback() {
+ assertThat(
+ converter.convertToBinderCode(
+ Type(packageName = "com.mysdk", simpleName = "Callback"), expression = "callback"
+ ).toString()
+ ).isEqualTo("com.mysdk.CallbackStubDelegate(callback)")
+ }
+}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
index c874d53..d5bc9e5 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
@@ -44,7 +44,7 @@
)
companion object {
- val TEST_DB = "migration-test"
+ const val TEST_DB = "migration-test"
}
abstract class EmptyDb : RoomDatabase()
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
index 9df26de..fb06e90 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
@@ -149,7 +149,7 @@
true,
MIGRATION_1_0
);
- DatabaseConfiguration config = helper.getDbConfigurationAfterMigrations();
+ DatabaseConfiguration config = helper.databaseConfiguration;
assertThat(config).isNotNull();
assertThat(config.migrationContainer.findMigrationPath(1, 2)).isNotNull();
assertThat(config.migrationContainer.findMigrationPath(1, 2)).isNotEmpty();
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java
index b83df2c..e298302 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java
@@ -70,11 +70,14 @@
public MigrationTestHelper helper;
public MigrationTest() {
- helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
- MigrationDb.class.getCanonicalName());
+ helper = new MigrationTestHelper(
+ InstrumentationRegistry.getInstrumentation(),
+ MigrationDb.class.getCanonicalName()
+ );
}
@Test
+ @SuppressWarnings("deprecation") // This test is for verifying the old deprecated constructor
public void giveBadResource() throws IOException {
MigrationTestHelper helper = new MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeKotlinPoetExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeKotlinPoetExt.kt
index a862b1b..affa165 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeKotlinPoetExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeKotlinPoetExt.kt
@@ -16,6 +16,7 @@
package androidx.room.compiler.processing.ksp
+import androidx.room.compiler.processing.util.ISSUE_TRACKER_LINK
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.KSDeclaration
@@ -27,6 +28,7 @@
import com.google.devtools.ksp.symbol.KSTypeReference
import com.google.devtools.ksp.symbol.Variance
import com.squareup.kotlinpoet.ANY
+import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.javapoet.KClassName
import com.squareup.kotlinpoet.javapoet.KTypeName
@@ -155,10 +157,59 @@
}
/**
+ * See [KTypeVariableNameFactory.newInstance]
+ */
+private val typeVarNameCompanionInstance by lazy {
+ try {
+ KTypeVariableName::class.java.getDeclaredField("Companion")
+ .apply { trySetAccessible() }
+ .get(null)
+ } catch (ex: NoSuchFieldException) {
+ throw IllegalStateException(
+ """
+ Room couldn't find the field it is looking for in KotlinPoet.
+ Please file a bug at $ISSUE_TRACKER_LINK.
+ """.trimIndent(),
+ ex
+ )
+ }
+}
+
+/**
+ * See [KTypeVariableNameFactory.newInstance] and
+ * https://github.com/square/kotlinpoet/blob/1.12.0/kotlinpoet/src/main/java/com/squareup/kotlinpoet/TypeVariableName.kt#L70-L74
+ */
+private val typeVarNameFactoryMethod by lazy {
+ try {
+ typeVarNameCompanionInstance::class.java.methods.first {
+ it.name.startsWith("of") &&
+ it.parameterCount == 3 &&
+ it.parameters[0].type == String::class.java &&
+ it.parameters[1].type == List::class.java &&
+ it.parameters[2].type == KModifier::class.java
+ }.apply { trySetAccessible() }
+ } catch (ex: NoSuchElementException) {
+ throw IllegalStateException(
+ """
+ Room couldn't find the method it is looking for in KotlinPoet.
+ Please file a bug at $ISSUE_TRACKER_LINK.
+ """.trimIndent(),
+ )
+ }
+}
+
+/**
* Creates a TypeVariableName where we can change the bounds after constructor.
* This is used to workaround a case for self referencing type declarations.
*/
private fun createModifiableTypeVariableName(
name: String,
bounds: List<KTypeName>
-): KTypeVariableName = KTypeVariableNameFactory.newInstance(name, bounds)
+): KTypeVariableName =
+ try {
+ KTypeVariableNameFactory.newInstance(name, bounds)
+ } catch (ex: NoSuchMethodError) {
+ typeVarNameFactoryMethod.invoke(
+ typeVarNameCompanionInstance, name, bounds, null
+ ) as KTypeVariableName
+ }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
index 7c6a68e..e78d084 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
@@ -312,6 +312,7 @@
)
val resultColumns = query.resultInfo?.columns
+
if (resultColumns != null) {
context.checker.check(
keyColumn.isEmpty() || resultColumns.contains(keyColumn, keyTable),
diff --git a/room/room-ktx/src/test/java/androidx/room/MigrationTest.kt b/room/room-ktx/src/test/java/androidx/room/MigrationTest.kt
index 753a700..162f50b 100644
--- a/room/room-ktx/src/test/java/androidx/room/MigrationTest.kt
+++ b/room/room-ktx/src/test/java/androidx/room/MigrationTest.kt
@@ -116,7 +116,7 @@
throw UnsupportedOperationException()
}
- override fun query(query: String, bindArgs: Array<Any?>): Cursor {
+ override fun query(query: String, bindArgs: Array<out Any?>): Cursor {
throw UnsupportedOperationException()
}
@@ -135,7 +135,7 @@
throw UnsupportedOperationException()
}
- override fun delete(table: String, whereClause: String?, whereArgs: Array<Any?>?): Int {
+ override fun delete(table: String, whereClause: String?, whereArgs: Array<out Any?>?): Int {
throw UnsupportedOperationException()
}
@@ -144,7 +144,7 @@
conflictAlgorithm: Int,
values: ContentValues,
whereClause: String?,
- whereArgs: Array<Any?>?
+ whereArgs: Array<out Any?>?
): Int {
throw UnsupportedOperationException()
}
@@ -153,7 +153,7 @@
throw UnsupportedOperationException()
}
- override fun execSQL(sql: String, bindArgs: Array<Any?>) {
+ override fun execSQL(sql: String, bindArgs: Array<out Any?>) {
throw UnsupportedOperationException()
}
diff --git a/room/room-runtime/src/main/java/androidx/room/AutoClosingRoomOpenHelper.kt b/room/room-runtime/src/main/java/androidx/room/AutoClosingRoomOpenHelper.kt
index 4cafb11..5c5b923 100644
--- a/room/room-runtime/src/main/java/androidx/room/AutoClosingRoomOpenHelper.kt
+++ b/room/room-runtime/src/main/java/androidx/room/AutoClosingRoomOpenHelper.kt
@@ -231,7 +231,7 @@
return KeepAliveCursor(result, autoCloser)
}
- override fun query(query: String, bindArgs: Array<Any?>): Cursor {
+ override fun query(query: String, bindArgs: Array<out Any?>): Cursor {
val result = try {
autoCloser.incrementCountAndEnsureDbIsOpen().query(query, bindArgs)
} catch (throwable: Throwable) {
@@ -275,7 +275,7 @@
}
}
- override fun delete(table: String, whereClause: String?, whereArgs: Array<Any?>?): Int {
+ override fun delete(table: String, whereClause: String?, whereArgs: Array<out Any?>?): Int {
return autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase ->
db.delete(
table,
@@ -290,7 +290,7 @@
conflictAlgorithm: Int,
values: ContentValues,
whereClause: String?,
- whereArgs: Array<Any?>?
+ whereArgs: Array<out Any?>?
): Int {
return autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase ->
db.update(
@@ -309,7 +309,7 @@
}
@Throws(SQLException::class)
- override fun execSQL(sql: String, bindArgs: Array<Any?>) {
+ override fun execSQL(sql: String, bindArgs: Array<out Any?>) {
autoCloser.executeRefCountingFunction<Any?> { db: SupportSQLiteDatabase ->
db.execSQL(sql, bindArgs)
null
diff --git a/room/room-runtime/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.kt b/room/room-runtime/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.kt
index 5131681..d140a3e 100644
--- a/room/room-runtime/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.kt
+++ b/room/room-runtime/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.kt
@@ -91,7 +91,7 @@
* @param entities Entities to delete or update
* @return The number of affected rows
*/
- fun handleMultiple(entities: Array<T>): Int {
+ fun handleMultiple(entities: Array<out T>): Int {
val stmt: SupportSQLiteStatement = acquire()
return try {
var total = 0
diff --git a/room/room-runtime/src/main/java/androidx/room/EntityInsertionAdapter.kt b/room/room-runtime/src/main/java/androidx/room/EntityInsertionAdapter.kt
index 2aec5e5..0e2473a 100644
--- a/room/room-runtime/src/main/java/androidx/room/EntityInsertionAdapter.kt
+++ b/room/room-runtime/src/main/java/androidx/room/EntityInsertionAdapter.kt
@@ -60,7 +60,7 @@
*
* @param entities Entities to insert
*/
- fun insert(entities: Array<T>) {
+ fun insert(entities: Array<out T>) {
val stmt: SupportSQLiteStatement = acquire()
try {
entities.forEach { entity ->
@@ -131,7 +131,7 @@
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
- fun insertAndReturnIdsArray(entities: Array<T>): LongArray {
+ fun insertAndReturnIdsArray(entities: Array<out T>): LongArray {
val stmt: SupportSQLiteStatement = acquire()
return try {
val result = LongArray(entities.size)
@@ -151,7 +151,7 @@
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
- fun insertAndReturnIdsArrayBox(entities: Collection<T>): Array<Long> {
+ fun insertAndReturnIdsArrayBox(entities: Collection<T>): Array<out Long> {
val stmt: SupportSQLiteStatement = acquire()
val iterator = entities.iterator()
return try {
@@ -172,7 +172,7 @@
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
- fun insertAndReturnIdsArrayBox(entities: Array<T>): Array<Long> {
+ fun insertAndReturnIdsArrayBox(entities: Array<out T>): Array<out Long> {
val stmt: SupportSQLiteStatement = acquire()
val iterator = entities.iterator()
return try {
@@ -193,7 +193,7 @@
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
- fun insertAndReturnIdsList(entities: Array<T>): List<Long> {
+ fun insertAndReturnIdsList(entities: Array<out T>): List<Long> {
val stmt: SupportSQLiteStatement = acquire()
return try {
buildList {
diff --git a/room/room-runtime/src/main/java/androidx/room/EntityUpsertionAdapter.kt b/room/room-runtime/src/main/java/androidx/room/EntityUpsertionAdapter.kt
index 1ce5838..aaefe4c 100644
--- a/room/room-runtime/src/main/java/androidx/room/EntityUpsertionAdapter.kt
+++ b/room/room-runtime/src/main/java/androidx/room/EntityUpsertionAdapter.kt
@@ -70,7 +70,7 @@
*
* @param entities array of entities to upsert
*/
- fun upsert(entities: Array<T>) {
+ fun upsert(entities: Array<out T>) {
entities.forEach { entity ->
try {
insertionAdapter.insert(entity)
@@ -116,7 +116,7 @@
* @param entities Entities to upsert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
- fun upsertAndReturnIdsArray(entities: Array<T>): LongArray {
+ fun upsertAndReturnIdsArray(entities: Array<out T>): LongArray {
return LongArray(entities.size) { index ->
try {
insertionAdapter.insertAndReturnId(entities[index])
@@ -142,7 +142,7 @@
}
}
- fun upsertAndReturnIdsList(entities: Array<T>): List<Long> {
+ fun upsertAndReturnIdsList(entities: Array<out T>): List<Long> {
return buildList {
entities.forEach { entity ->
try {
@@ -170,7 +170,7 @@
}
}
- fun upsertAndReturnIdsArrayBox(entities: Array<T>): Array<Long> {
+ fun upsertAndReturnIdsArrayBox(entities: Array<out T>): Array<out Long> {
return Array(entities.size) { index ->
try {
insertionAdapter.insertAndReturnId(entities[index])
@@ -182,7 +182,7 @@
}
}
- fun upsertAndReturnIdsArrayBox(entities: Collection<T>): Array<Long> {
+ fun upsertAndReturnIdsArrayBox(entities: Collection<T>): Array<out Long> {
val iterator = entities.iterator()
return Array(entities.size) {
val entity = iterator.next()
diff --git a/room/room-runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.kt b/room/room-runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.kt
index 2a51d0c..73e7ee6 100644
--- a/room/room-runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.kt
+++ b/room/room-runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.kt
@@ -32,7 +32,7 @@
internal val liveDataSet: MutableSet<LiveData<*>> = Collections.newSetFromMap(IdentityHashMap())
fun <T> create(
- tableNames: Array<String>,
+ tableNames: Array<out String>,
inTransaction: Boolean,
computeFunction: Callable<T>
): LiveData<T> {
diff --git a/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt b/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
index b766d6f..251b82a 100644
--- a/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
+++ b/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
@@ -60,7 +60,7 @@
vararg tableNames: String
) {
internal val tableIdLookup: Map<String, Int>
- internal val tablesNames: Array<String>
+ internal val tablesNames: Array<out String>
private var autoCloser: AutoCloser? = null
@@ -267,7 +267,7 @@
}
}
- private fun validateAndResolveTableNames(tableNames: Array<String>): Array<String> {
+ private fun validateAndResolveTableNames(tableNames: Array<out String>): Array<out String> {
val resolved = resolveViews(tableNames)
resolved.forEach { tableName ->
require(tableIdLookup.containsKey(tableName.lowercase(Locale.US))) {
@@ -283,7 +283,7 @@
* @param names The names of tables or views.
* @return The names of the underlying tables.
*/
- private fun resolveViews(names: Array<String>): Array<String> {
+ private fun resolveViews(names: Array<out String>): Array<out String> {
return buildSet {
names.forEach { name ->
if (viewTables.containsKey(name.lowercase(Locale.US))) {
@@ -548,7 +548,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@Deprecated("Use [createLiveData(String[], boolean, Callable)]")
open fun <T> createLiveData(
- tableNames: Array<String>,
+ tableNames: Array<out String>,
computeFunction: Callable<T>
): LiveData<T> {
return createLiveData(tableNames, false, computeFunction)
@@ -571,7 +571,7 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
open fun <T> createLiveData(
- tableNames: Array<String>,
+ tableNames: Array<out String>,
inTransaction: Boolean,
computeFunction: Callable<T>
): LiveData<T> {
@@ -589,7 +589,7 @@
internal class ObserverWrapper(
internal val observer: Observer,
internal val tableIds: IntArray,
- private val tableNames: Array<String>
+ private val tableNames: Array<out String>
) {
private val singleTableSet = if (tableNames.isNotEmpty()) {
setOf(tableNames[0])
@@ -663,7 +663,7 @@
/**
* An observer that can listen for changes in the database.
*/
- abstract class Observer(internal val tables: Array<String>) {
+ abstract class Observer(internal val tables: Array<out String>) {
/**
* Observes the given list of tables and views.
*
diff --git a/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationClient.kt b/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationClient.kt
index e4eb340..0659f92 100644
--- a/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationClient.kt
+++ b/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationClient.kt
@@ -56,7 +56,7 @@
val callback: IMultiInstanceInvalidationCallback =
object : IMultiInstanceInvalidationCallback.Stub() {
- override fun onInvalidation(tables: Array<String>) {
+ override fun onInvalidation(tables: Array<out String>) {
executor.execute { invalidationTracker.notifyObserversByTableNames(*tables) }
}
}
diff --git a/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationService.kt b/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationService.kt
index 9d72873..761541d 100644
--- a/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationService.kt
+++ b/room/room-runtime/src/main/java/androidx/room/MultiInstanceInvalidationService.kt
@@ -86,7 +86,7 @@
// Broadcasts table invalidation to other instances of the same database file.
// The broadcast is not sent to the caller itself.
- override fun broadcastInvalidation(clientId: Int, tables: Array<String>) {
+ override fun broadcastInvalidation(clientId: Int, tables: Array<out String>) {
synchronized(callbackList) {
val name = clientNames[clientId]
if (name == null) {
diff --git a/room/room-runtime/src/main/java/androidx/room/QueryInterceptorDatabase.kt b/room/room-runtime/src/main/java/androidx/room/QueryInterceptorDatabase.kt
index 248dfe5..4fb5661 100644
--- a/room/room-runtime/src/main/java/androidx/room/QueryInterceptorDatabase.kt
+++ b/room/room-runtime/src/main/java/androidx/room/QueryInterceptorDatabase.kt
@@ -94,7 +94,7 @@
return delegate.query(query)
}
- override fun query(query: String, bindArgs: Array<Any?>): Cursor {
+ override fun query(query: String, bindArgs: Array<out Any?>): Cursor {
queryCallbackExecutor.execute { queryCallback.onQuery(query, bindArgs.toList()) }
return delegate.query(query, bindArgs)
}
@@ -136,7 +136,7 @@
// Suppress warning about `SQL` in execSQL not being camel case. This is an override function
// and it can't be renamed.
@Suppress("AcronymName")
- override fun execSQL(sql: String, bindArgs: Array<Any?>) {
+ override fun execSQL(sql: String, bindArgs: Array<out Any?>) {
val inputArguments = mutableListOf<Any>()
inputArguments.addAll(listOf(bindArgs))
queryCallbackExecutor.execute {
diff --git a/room/room-runtime/src/main/java/androidx/room/Room.kt b/room/room-runtime/src/main/java/androidx/room/Room.kt
index 9a68205..fcd1527 100644
--- a/room/room-runtime/src/main/java/androidx/room/Room.kt
+++ b/room/room-runtime/src/main/java/androidx/room/Room.kt
@@ -53,7 +53,7 @@
val aClass = Class.forName(
fullClassName, true, klass.classLoader
) as Class<T>
- aClass.newInstance()
+ aClass.getDeclaredConstructor().newInstance()
} catch (e: ClassNotFoundException) {
throw RuntimeException(
"Cannot find implementation for ${klass.canonicalName}. $implName does not " +
diff --git a/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt b/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt
index 1c5732e..599d9d3 100644
--- a/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt
+++ b/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt
@@ -460,7 +460,7 @@
* @param args The bind arguments for the placeholders in the query
* @return A Cursor obtained by running the given query in the Room database.
*/
- open fun query(query: String, args: Array<Any?>?): Cursor {
+ open fun query(query: String, args: Array<out Any?>?): Cursor {
return openHelper.writableDatabase.query(SimpleSQLiteQuery(query, args))
}
diff --git a/room/room-runtime/src/main/java/androidx/room/RoomTrackingLiveData.kt b/room/room-runtime/src/main/java/androidx/room/RoomTrackingLiveData.kt
index a2785b8..8b0d7a6 100644
--- a/room/room-runtime/src/main/java/androidx/room/RoomTrackingLiveData.kt
+++ b/room/room-runtime/src/main/java/androidx/room/RoomTrackingLiveData.kt
@@ -44,7 +44,7 @@
private val container: InvalidationLiveDataContainer,
val inTransaction: Boolean,
val computeFunction: Callable<T>,
- tableNames: Array<String>
+ tableNames: Array<out String>
) : LiveData<T>() {
val observer: InvalidationTracker.Observer = object : InvalidationTracker.Observer(tableNames) {
override fun onInvalidated(tables: Set<String>) {
diff --git a/room/room-testing/api/current.ignore b/room/room-testing/api/current.ignore
new file mode 100644
index 0000000..abbadbf
--- /dev/null
+++ b/room/room-testing/api/current.ignore
@@ -0,0 +1,9 @@
+// Baseline format: 1.0
+ChangedThrows: androidx.room.testing.MigrationTestHelper#runMigrationsAndValidate(String, int, boolean, androidx.room.migration.Migration...):
+ Method androidx.room.testing.MigrationTestHelper.runMigrationsAndValidate no longer throws exception java.io.IOException
+
+
+RemovedMethod: androidx.room.testing.MigrationTestHelper#MigrationTestHelper(android.app.Instrumentation, Class<? extends androidx.room.RoomDatabase>, java.util.List<androidx.room.migration.AutoMigrationSpec>):
+ Removed constructor androidx.room.testing.MigrationTestHelper(android.app.Instrumentation,Class<? extends androidx.room.RoomDatabase>,java.util.List<androidx.room.migration.AutoMigrationSpec>)
+RemovedMethod: androidx.room.testing.MigrationTestHelper#MigrationTestHelper(android.app.Instrumentation, Class<? extends androidx.room.RoomDatabase>, java.util.List<androidx.room.migration.AutoMigrationSpec>, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory):
+ Removed constructor androidx.room.testing.MigrationTestHelper(android.app.Instrumentation,Class<? extends androidx.room.RoomDatabase>,java.util.List<androidx.room.migration.AutoMigrationSpec>,androidx.sqlite.db.SupportSQLiteOpenHelper.Factory)
diff --git a/room/room-testing/api/current.txt b/room/room-testing/api/current.txt
index ea0639e..7138ac0 100644
--- a/room/room-testing/api/current.txt
+++ b/room/room-testing/api/current.txt
@@ -2,15 +2,15 @@
package androidx.room.testing {
public class MigrationTestHelper extends org.junit.rules.TestWatcher {
- ctor @Deprecated public MigrationTestHelper(android.app.Instrumentation!, String!);
- ctor @Deprecated public MigrationTestHelper(android.app.Instrumentation!, String!, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory!);
- ctor public MigrationTestHelper(android.app.Instrumentation, Class<? extends androidx.room.RoomDatabase>);
- ctor public MigrationTestHelper(android.app.Instrumentation, Class<? extends androidx.room.RoomDatabase>, java.util.List<androidx.room.migration.AutoMigrationSpec!>);
- ctor public MigrationTestHelper(android.app.Instrumentation, Class<? extends androidx.room.RoomDatabase>, java.util.List<androidx.room.migration.AutoMigrationSpec!>, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory);
- method public void closeWhenFinished(androidx.sqlite.db.SupportSQLiteDatabase!);
- method public void closeWhenFinished(androidx.room.RoomDatabase!);
- method public androidx.sqlite.db.SupportSQLiteDatabase! createDatabase(String!, int) throws java.io.IOException;
- method public androidx.sqlite.db.SupportSQLiteDatabase! runMigrationsAndValidate(String!, int, boolean, androidx.room.migration.Migration!...) throws java.io.IOException;
+ ctor @Deprecated public MigrationTestHelper(android.app.Instrumentation instrumentation, String assetsFolder, optional androidx.sqlite.db.SupportSQLiteOpenHelper.Factory openFactory);
+ ctor @Deprecated public MigrationTestHelper(android.app.Instrumentation instrumentation, String assetsFolder);
+ ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass);
+ ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> specs, optional androidx.sqlite.db.SupportSQLiteOpenHelper.Factory openFactory);
+ ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> specs);
+ method public void closeWhenFinished(androidx.sqlite.db.SupportSQLiteDatabase db);
+ method public void closeWhenFinished(androidx.room.RoomDatabase db);
+ method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public androidx.sqlite.db.SupportSQLiteDatabase createDatabase(String name, int version) throws java.io.IOException;
+ method public androidx.sqlite.db.SupportSQLiteDatabase runMigrationsAndValidate(String name, int version, boolean validateDroppedTables, androidx.room.migration.Migration... migrations);
}
}
diff --git a/room/room-testing/api/public_plus_experimental_current.txt b/room/room-testing/api/public_plus_experimental_current.txt
index ea0639e..7138ac0 100644
--- a/room/room-testing/api/public_plus_experimental_current.txt
+++ b/room/room-testing/api/public_plus_experimental_current.txt
@@ -2,15 +2,15 @@
package androidx.room.testing {
public class MigrationTestHelper extends org.junit.rules.TestWatcher {
- ctor @Deprecated public MigrationTestHelper(android.app.Instrumentation!, String!);
- ctor @Deprecated public MigrationTestHelper(android.app.Instrumentation!, String!, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory!);
- ctor public MigrationTestHelper(android.app.Instrumentation, Class<? extends androidx.room.RoomDatabase>);
- ctor public MigrationTestHelper(android.app.Instrumentation, Class<? extends androidx.room.RoomDatabase>, java.util.List<androidx.room.migration.AutoMigrationSpec!>);
- ctor public MigrationTestHelper(android.app.Instrumentation, Class<? extends androidx.room.RoomDatabase>, java.util.List<androidx.room.migration.AutoMigrationSpec!>, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory);
- method public void closeWhenFinished(androidx.sqlite.db.SupportSQLiteDatabase!);
- method public void closeWhenFinished(androidx.room.RoomDatabase!);
- method public androidx.sqlite.db.SupportSQLiteDatabase! createDatabase(String!, int) throws java.io.IOException;
- method public androidx.sqlite.db.SupportSQLiteDatabase! runMigrationsAndValidate(String!, int, boolean, androidx.room.migration.Migration!...) throws java.io.IOException;
+ ctor @Deprecated public MigrationTestHelper(android.app.Instrumentation instrumentation, String assetsFolder, optional androidx.sqlite.db.SupportSQLiteOpenHelper.Factory openFactory);
+ ctor @Deprecated public MigrationTestHelper(android.app.Instrumentation instrumentation, String assetsFolder);
+ ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass);
+ ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> specs, optional androidx.sqlite.db.SupportSQLiteOpenHelper.Factory openFactory);
+ ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> specs);
+ method public void closeWhenFinished(androidx.sqlite.db.SupportSQLiteDatabase db);
+ method public void closeWhenFinished(androidx.room.RoomDatabase db);
+ method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public androidx.sqlite.db.SupportSQLiteDatabase createDatabase(String name, int version) throws java.io.IOException;
+ method public androidx.sqlite.db.SupportSQLiteDatabase runMigrationsAndValidate(String name, int version, boolean validateDroppedTables, androidx.room.migration.Migration... migrations);
}
}
diff --git a/room/room-testing/api/restricted_current.ignore b/room/room-testing/api/restricted_current.ignore
new file mode 100644
index 0000000..abbadbf
--- /dev/null
+++ b/room/room-testing/api/restricted_current.ignore
@@ -0,0 +1,9 @@
+// Baseline format: 1.0
+ChangedThrows: androidx.room.testing.MigrationTestHelper#runMigrationsAndValidate(String, int, boolean, androidx.room.migration.Migration...):
+ Method androidx.room.testing.MigrationTestHelper.runMigrationsAndValidate no longer throws exception java.io.IOException
+
+
+RemovedMethod: androidx.room.testing.MigrationTestHelper#MigrationTestHelper(android.app.Instrumentation, Class<? extends androidx.room.RoomDatabase>, java.util.List<androidx.room.migration.AutoMigrationSpec>):
+ Removed constructor androidx.room.testing.MigrationTestHelper(android.app.Instrumentation,Class<? extends androidx.room.RoomDatabase>,java.util.List<androidx.room.migration.AutoMigrationSpec>)
+RemovedMethod: androidx.room.testing.MigrationTestHelper#MigrationTestHelper(android.app.Instrumentation, Class<? extends androidx.room.RoomDatabase>, java.util.List<androidx.room.migration.AutoMigrationSpec>, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory):
+ Removed constructor androidx.room.testing.MigrationTestHelper(android.app.Instrumentation,Class<? extends androidx.room.RoomDatabase>,java.util.List<androidx.room.migration.AutoMigrationSpec>,androidx.sqlite.db.SupportSQLiteOpenHelper.Factory)
diff --git a/room/room-testing/api/restricted_current.txt b/room/room-testing/api/restricted_current.txt
index ea0639e..7138ac0 100644
--- a/room/room-testing/api/restricted_current.txt
+++ b/room/room-testing/api/restricted_current.txt
@@ -2,15 +2,15 @@
package androidx.room.testing {
public class MigrationTestHelper extends org.junit.rules.TestWatcher {
- ctor @Deprecated public MigrationTestHelper(android.app.Instrumentation!, String!);
- ctor @Deprecated public MigrationTestHelper(android.app.Instrumentation!, String!, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory!);
- ctor public MigrationTestHelper(android.app.Instrumentation, Class<? extends androidx.room.RoomDatabase>);
- ctor public MigrationTestHelper(android.app.Instrumentation, Class<? extends androidx.room.RoomDatabase>, java.util.List<androidx.room.migration.AutoMigrationSpec!>);
- ctor public MigrationTestHelper(android.app.Instrumentation, Class<? extends androidx.room.RoomDatabase>, java.util.List<androidx.room.migration.AutoMigrationSpec!>, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory);
- method public void closeWhenFinished(androidx.sqlite.db.SupportSQLiteDatabase!);
- method public void closeWhenFinished(androidx.room.RoomDatabase!);
- method public androidx.sqlite.db.SupportSQLiteDatabase! createDatabase(String!, int) throws java.io.IOException;
- method public androidx.sqlite.db.SupportSQLiteDatabase! runMigrationsAndValidate(String!, int, boolean, androidx.room.migration.Migration!...) throws java.io.IOException;
+ ctor @Deprecated public MigrationTestHelper(android.app.Instrumentation instrumentation, String assetsFolder, optional androidx.sqlite.db.SupportSQLiteOpenHelper.Factory openFactory);
+ ctor @Deprecated public MigrationTestHelper(android.app.Instrumentation instrumentation, String assetsFolder);
+ ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass);
+ ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> specs, optional androidx.sqlite.db.SupportSQLiteOpenHelper.Factory openFactory);
+ ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> specs);
+ method public void closeWhenFinished(androidx.sqlite.db.SupportSQLiteDatabase db);
+ method public void closeWhenFinished(androidx.room.RoomDatabase db);
+ method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public androidx.sqlite.db.SupportSQLiteDatabase createDatabase(String name, int version) throws java.io.IOException;
+ method public androidx.sqlite.db.SupportSQLiteDatabase runMigrationsAndValidate(String name, int version, boolean validateDroppedTables, androidx.room.migration.Migration... migrations);
}
}
diff --git a/room/room-testing/src/main/java/androidx/room/testing/MigrationTestHelper.java b/room/room-testing/src/main/java/androidx/room/testing/MigrationTestHelper.java
deleted file mode 100644
index 8e282c4..0000000
--- a/room/room-testing/src/main/java/androidx/room/testing/MigrationTestHelper.java
+++ /dev/null
@@ -1,719 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room.testing;
-
-import static java.util.Collections.emptyList;
-import static java.util.Collections.emptySet;
-
-import android.annotation.SuppressLint;
-import android.app.Instrumentation;
-import android.content.Context;
-import android.database.Cursor;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.arch.core.executor.ArchTaskExecutor;
-import androidx.room.AutoMigration;
-import androidx.room.DatabaseConfiguration;
-import androidx.room.Room;
-import androidx.room.RoomDatabase;
-import androidx.room.RoomOpenHelper;
-import androidx.room.RoomOpenHelper.ValidationResult;
-import androidx.room.migration.AutoMigrationSpec;
-import androidx.room.migration.Migration;
-import androidx.room.migration.bundle.DatabaseBundle;
-import androidx.room.migration.bundle.DatabaseViewBundle;
-import androidx.room.migration.bundle.EntityBundle;
-import androidx.room.migration.bundle.FieldBundle;
-import androidx.room.migration.bundle.ForeignKeyBundle;
-import androidx.room.migration.bundle.FtsEntityBundle;
-import androidx.room.migration.bundle.IndexBundle;
-import androidx.room.migration.bundle.SchemaBundle;
-import androidx.room.util.FtsTableInfo;
-import androidx.room.util.TableInfo;
-import androidx.room.util.ViewInfo;
-import androidx.sqlite.db.SupportSQLiteDatabase;
-import androidx.sqlite.db.SupportSQLiteOpenHelper;
-import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
-
-import org.junit.rules.TestWatcher;
-import org.junit.runner.Description;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * A class that can be used in your Instrumentation tests that can create the database in an
- * older schema.
- * <p>
- * You must copy the schema json files (created by passing {@code room.schemaLocation} argument
- * into the annotation processor) into your test assets and pass in the path for that folder into
- * the constructor. This class will read the folder and extract the schemas from there.
- * <pre>
- * android {
- * defaultConfig {
- * javaCompileOptions {
- * annotationProcessorOptions {
- * arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
- * }
- * }
- * }
- * sourceSets {
- * androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
- * }
- * }
- * </pre>
- */
-public class MigrationTestHelper extends TestWatcher {
- private static final String TAG = "MigrationTestHelper";
- private final String mAssetsFolder;
- private final SupportSQLiteOpenHelper.Factory mOpenFactory;
- private List<WeakReference<SupportSQLiteDatabase>> mManagedDatabases = new ArrayList<>();
- private List<WeakReference<RoomDatabase>> mManagedRoomDatabases = new ArrayList<>();
- private boolean mTestStarted;
- private Instrumentation mInstrumentation;
- @Nullable
- private List<AutoMigrationSpec> mSpecs;
- @Nullable
- private Class<? extends RoomDatabase> mDatabaseClass;
- @NonNull
- private DatabaseConfiguration mDatabaseConfiguration;
-
- /**
- * Creates a new migration helper. It uses the Instrumentation context to load the schema
- * (falls back to the app resources) and the target context to create the database.
- *
- * @deprecated Cannot be used to run migration tests involving {@link AutoMigration}.
- * <p>
- * To test {@link AutoMigration}, you must use
- * {@link #MigrationTestHelper(Instrumentation, Class, List, SupportSQLiteOpenHelper.Factory)}
- * for tests containing a {@link androidx.room.ProvidedAutoMigrationSpec}, or use
- * {@link #MigrationTestHelper(Instrumentation, Class, List)}
- * otherwise.
- *
- * @param instrumentation The instrumentation instance.
- * @param assetsFolder The asset folder in the assets directory.
- */
- @Deprecated
- public MigrationTestHelper(Instrumentation instrumentation, String assetsFolder) {
- this(instrumentation, assetsFolder, new FrameworkSQLiteOpenHelperFactory());
- }
-
- /**
- * Creates a new migration helper. It uses the Instrumentation context to load the schema
- * (falls back to the app resources) and the target context to create the database.
- *
- * @deprecated Cannot be used to run migration tests involving {@link AutoMigration}.
- * <p>
- * To test {@link AutoMigration}, you must use
- * {@link #MigrationTestHelper(Instrumentation, Class, List, SupportSQLiteOpenHelper.Factory)}
- * for tests containing a {@link androidx.room.ProvidedAutoMigrationSpec}, or use
- * {@link #MigrationTestHelper(Instrumentation, Class, List)}
- * otherwise.
- *
- * @param instrumentation The instrumentation instance.
- * @param assetsFolder The asset folder in the assets directory.
- * @param openFactory Factory class that allows creation of {@link SupportSQLiteOpenHelper}
- */
- @Deprecated
- public MigrationTestHelper(Instrumentation instrumentation, String assetsFolder,
- SupportSQLiteOpenHelper.Factory openFactory) {
- mInstrumentation = instrumentation;
- mAssetsFolder = assetsFolder;
- mOpenFactory = openFactory;
- mDatabaseClass = null;
- mSpecs = new ArrayList<>();
- }
-
- /**
- * Creates a new migration helper. It uses the Instrumentation context to load the schema
- * (falls back to the app resources) and the target context to create the database.
- *
- * @param instrumentation The instrumentation instance.
- * @param databaseClass The Database class to be tested.
- */
- public MigrationTestHelper(@NonNull Instrumentation instrumentation,
- @NonNull Class<? extends RoomDatabase> databaseClass) {
- this(instrumentation, databaseClass, new ArrayList<>(),
- new FrameworkSQLiteOpenHelperFactory());
- }
-
- /**
- * Creates a new migration helper. It uses the Instrumentation context to load the schema
- * (falls back to the app resources) and the target context to create the database.
- * <p>
- * An instance of a class annotated with {@link androidx.room.ProvidedAutoMigrationSpec} has
- * to be provided to Room using this constructor. MigrationTestHelper will map auto migration
- * spec classes to their provided instances before running and validatingt the Migrations.
- *
- * @param instrumentation The instrumentation instance.
- * @param databaseClass The Database class to be tested.
- * @param specs The list of available auto migration specs that will be provided to
- * Room at runtime.
- */
- public MigrationTestHelper(@NonNull Instrumentation instrumentation,
- @NonNull Class<? extends RoomDatabase> databaseClass,
- @NonNull List<AutoMigrationSpec> specs) {
- this(instrumentation, databaseClass, specs, new FrameworkSQLiteOpenHelperFactory());
- }
-
- /**
- * Creates a new migration helper. It uses the Instrumentation context to load the schema
- * (falls back to the app resources) and the target context to create the database.
- * <p>
- * An instance of a class annotated with {@link androidx.room.ProvidedAutoMigrationSpec} has
- * to be provided to Room using this constructor. MigrationTestHelper will map auto migration
- * spec classes to their provided instances before running and validatingt the Migrations.
- *
- * @param instrumentation The instrumentation instance.
- * @param databaseClass The Database class to be tested.
- * @param specs The list of available auto migration specs that will be provided to
- * Room at runtime.
- * @param openFactory Factory class that allows creation of {@link SupportSQLiteOpenHelper}
- */
- public MigrationTestHelper(@NonNull Instrumentation instrumentation,
- @NonNull Class<? extends RoomDatabase> databaseClass,
- @NonNull List<AutoMigrationSpec> specs,
- @NonNull SupportSQLiteOpenHelper.Factory openFactory
- ) {
- String assetsFolder = databaseClass.getCanonicalName();
- mInstrumentation = instrumentation;
- if (assetsFolder.endsWith("/")) {
- assetsFolder = assetsFolder.substring(0, assetsFolder.length() - 1);
- }
- mAssetsFolder = assetsFolder;
- mOpenFactory = openFactory;
- mDatabaseClass = databaseClass;
- mSpecs = specs;
- }
-
- @Override
- protected void starting(Description description) {
- super.starting(description);
- mTestStarted = true;
- }
-
- /**
- * Creates the database in the given version.
- * If the database file already exists, it tries to delete it first. If delete fails, throws
- * an exception.
- *
- * @param name The name of the database.
- * @param version The version in which the database should be created.
- * @return A database connection which has the schema in the requested version.
- * @throws IOException If it cannot find the schema description in the assets folder.
- */
- @SuppressLint("RestrictedApi")
- @SuppressWarnings("SameParameterValue")
- public SupportSQLiteDatabase createDatabase(String name, int version) throws IOException {
- File dbPath = mInstrumentation.getTargetContext().getDatabasePath(name);
- if (dbPath.exists()) {
- Log.d(TAG, "deleting database file " + name);
- if (!dbPath.delete()) {
- throw new IllegalStateException("There is a database file and I could not delete"
- + " it. Make sure you don't have any open connections to that database"
- + " before calling this method.");
- }
- }
- SchemaBundle schemaBundle = loadSchema(version);
- RoomDatabase.MigrationContainer container = new RoomDatabase.MigrationContainer();
- DatabaseConfiguration configuration = new DatabaseConfiguration(
- mInstrumentation.getTargetContext(),
- name,
- mOpenFactory,
- container,
- null,
- true,
- RoomDatabase.JournalMode.TRUNCATE,
- ArchTaskExecutor.getIOThreadExecutor(),
- ArchTaskExecutor.getIOThreadExecutor(),
- null,
- true,
- false,
- emptySet(),
- null,
- null,
- null,
- null,
- emptyList(),
- emptyList());
- RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
- new CreatingDelegate(schemaBundle.getDatabase()),
- schemaBundle.getDatabase().getIdentityHash(),
- // we pass the same hash twice since an old schema does not necessarily have
- // a legacy hash and we would not even persist it.
- schemaBundle.getDatabase().getIdentityHash());
- return openDatabase(name, roomOpenHelper);
- }
-
- /**
- * Runs the given set of migrations on the provided database.
- * <p>
- * It uses the same algorithm that Room uses to choose migrations so the migrations instances
- * that are provided to this method must be sufficient to bring the database from current
- * version to the desired version.
- * <p>
- * After the migration, the method validates the database schema to ensure that migration
- * result matches the expected schema. Handling of dropped tables depends on the
- * {@code validateDroppedTables} argument. If set to true, the verification will fail if it
- * finds a table that is not registered in the Database. If set to false, extra tables in the
- * database will be ignored (this is the runtime library behavior).
- *
- * @param name The database name. You must first create this database via
- * {@link #createDatabase(String, int)}.
- * @param version The final version after applying the migrations.
- * @param validateDroppedTables If set to true, validation will fail if the database has
- * unknown
- * tables.
- * @param migrations The list of available migrations.
- * @throws IOException If it cannot find the schema for {@code toVersion}.
- * @throws IllegalStateException If the schema validation fails.
- */
- @SuppressLint("RestrictedApi")
- public SupportSQLiteDatabase runMigrationsAndValidate(String name, int version,
- boolean validateDroppedTables, Migration... migrations) throws IOException {
- File dbPath = mInstrumentation.getTargetContext().getDatabasePath(name);
- if (!dbPath.exists()) {
- throw new IllegalStateException("Cannot find the database file for " + name + ". "
- + "Before calling runMigrations, you must first create the database via "
- + "createDatabase.");
- }
- SchemaBundle schemaBundle = loadSchema(version);
- RoomDatabase.MigrationContainer container = new RoomDatabase.MigrationContainer();
- container.addMigrations(migrations);
- List<Migration> autoMigrations = getAutoMigrations(mSpecs);
- for (Migration autoMigration : autoMigrations) {
- boolean migrationExists = container.contains(
- autoMigration.startVersion,
- autoMigration.endVersion
- );
- if (!migrationExists) {
- container.addMigrations(autoMigration);
- }
- }
-
- mDatabaseConfiguration = new DatabaseConfiguration(
- mInstrumentation.getTargetContext(),
- name,
- mOpenFactory,
- container,
- null,
- true,
- RoomDatabase.JournalMode.TRUNCATE,
- ArchTaskExecutor.getIOThreadExecutor(),
- ArchTaskExecutor.getIOThreadExecutor(),
- null,
- true,
- false,
- emptySet(),
- null,
- null,
- null,
- null,
- emptyList(),
- emptyList());
- RoomOpenHelper roomOpenHelper = new RoomOpenHelper(mDatabaseConfiguration,
- new MigratingDelegate(schemaBundle.getDatabase(), validateDroppedTables),
- // we pass the same hash twice since an old schema does not necessarily have
- // a legacy hash and we would not even persist it.
- schemaBundle.getDatabase().getIdentityHash(),
- schemaBundle.getDatabase().getIdentityHash());
- return openDatabase(name, roomOpenHelper);
- }
-
- /**
- * Returns the {@link DatabaseConfiguration} at the current state of the database after all
- * migrations have been run and validated in
- * {@link MigrationTestHelper#runMigrationsAndValidate(String, int, boolean, Migration...)}.
- * <p>
- * Returns null if runMigrationsAndValidate() has not been called yet.
- *
- * @hide
- */
- @Nullable
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- public DatabaseConfiguration getDbConfigurationAfterMigrations() {
- return mDatabaseConfiguration;
- }
-
- /**
- * Returns a list of {@link Migration} of a database that has been generated using
- * {@link AutoMigration}.
- */
- @NonNull
- private List<Migration> getAutoMigrations(List<AutoMigrationSpec> userProvidedSpecs) {
- if (mDatabaseClass == null) {
- if (userProvidedSpecs.isEmpty()) {
- // TODO: Detect that there are auto migrations to test when a deprecated
- // constructor is used.
- Log.e(TAG, "If you have any AutoMigrations in your implementation, you must use "
- + "a non-deprecated MigrationTestHelper constructor to provide the "
- + "Database class in order to test them. If you do not have any "
- + "AutoMigrations to test, you may ignore this warning.");
- return new ArrayList<>();
- } else {
- throw new IllegalStateException("You must provide the database class in the "
- + "MigrationTestHelper constructor in order to test auto migrations.");
- }
- }
-
- RoomDatabase db = Room.getGeneratedImplementation(mDatabaseClass, "_Impl");
- Set<Class<? extends AutoMigrationSpec>> requiredAutoMigrationSpecs =
- db.getRequiredAutoMigrationSpecs();
- return db.getAutoMigrations(
- createAutoMigrationSpecMap(requiredAutoMigrationSpecs, userProvidedSpecs)
- );
- }
-
- /**
- * Maps auto migration spec classes to their provided instance.
- */
- private Map<Class<? extends AutoMigrationSpec>, AutoMigrationSpec> createAutoMigrationSpecMap(
- Set<Class<? extends AutoMigrationSpec>> requiredAutoMigrationSpecs,
- List<AutoMigrationSpec> userProvidedSpecs) {
- Map<Class<? extends AutoMigrationSpec>, AutoMigrationSpec> specMap = new HashMap<>();
- if (requiredAutoMigrationSpecs.isEmpty()) {
- return specMap;
- }
-
- if (userProvidedSpecs == null) {
- throw new IllegalStateException(
- "You must provide all required auto migration specs in the "
- + "MigrationTestHelper constructor."
- );
- }
-
- for (Class<? extends AutoMigrationSpec> spec : requiredAutoMigrationSpecs) {
- boolean found = false;
- AutoMigrationSpec match = null;
- for (AutoMigrationSpec provided : userProvidedSpecs) {
- if (spec.isAssignableFrom(provided.getClass())) {
- found = true;
- match = provided;
- break;
- }
- }
- if (!found) {
- throw new IllegalArgumentException(
- "A required auto migration spec (" + spec.getCanonicalName() + ") has not"
- + " been provided."
- );
- }
- specMap.put(spec, match);
- }
- return specMap;
- }
-
-
- private SupportSQLiteDatabase openDatabase(String name, RoomOpenHelper roomOpenHelper) {
- SupportSQLiteOpenHelper.Configuration config =
- SupportSQLiteOpenHelper.Configuration
- .builder(mInstrumentation.getTargetContext())
- .callback(roomOpenHelper)
- .name(name)
- .build();
- SupportSQLiteDatabase db = mOpenFactory.create(config).getWritableDatabase();
- mManagedDatabases.add(new WeakReference<>(db));
- return db;
- }
-
- @Override
- protected void finished(Description description) {
- super.finished(description);
- for (WeakReference<SupportSQLiteDatabase> dbRef : mManagedDatabases) {
- SupportSQLiteDatabase db = dbRef.get();
- if (db != null && db.isOpen()) {
- try {
- db.close();
- } catch (Throwable ignored) {
- }
- }
- }
- for (WeakReference<RoomDatabase> dbRef : mManagedRoomDatabases) {
- final RoomDatabase roomDatabase = dbRef.get();
- if (roomDatabase != null) {
- roomDatabase.close();
- }
- }
- }
-
- /**
- * Registers a database connection to be automatically closed when the test finishes.
- * <p>
- * This only works if {@code MigrationTestHelper} is registered as a Junit test rule via
- * {@link org.junit.Rule Rule} annotation.
- *
- * @param db The database connection that should be closed after the test finishes.
- */
- public void closeWhenFinished(SupportSQLiteDatabase db) {
- if (!mTestStarted) {
- throw new IllegalStateException("You cannot register a database to be closed before"
- + " the test starts. Maybe you forgot to annotate MigrationTestHelper as a"
- + " test rule? (@Rule)");
- }
- mManagedDatabases.add(new WeakReference<>(db));
- }
-
- /**
- * Registers a database connection to be automatically closed when the test finishes.
- * <p>
- * This only works if {@code MigrationTestHelper} is registered as a Junit test rule via
- * {@link org.junit.Rule Rule} annotation.
- *
- * @param db The RoomDatabase instance which holds the database.
- */
- public void closeWhenFinished(RoomDatabase db) {
- if (!mTestStarted) {
- throw new IllegalStateException("You cannot register a database to be closed before"
- + " the test starts. Maybe you forgot to annotate MigrationTestHelper as a"
- + " test rule? (@Rule)");
- }
- mManagedRoomDatabases.add(new WeakReference<>(db));
- }
-
- private SchemaBundle loadSchema(int version) throws IOException {
- try {
- return loadSchema(mInstrumentation.getContext(), version);
- } catch (FileNotFoundException testAssetsIOExceptions) {
- Log.w(TAG, "Could not find the schema file in the test assets. Checking the"
- + " application assets");
- try {
- return loadSchema(mInstrumentation.getTargetContext(), version);
- } catch (FileNotFoundException appAssetsException) {
- // throw the test assets exception instead
- throw new FileNotFoundException("Cannot find the schema file in the assets folder. "
- + "Make sure to include the exported json schemas in your test assert "
- + "inputs. See "
- + "https://developer.android.com/training/data-storage/room/"
- + "migrating-db-versions#export-schema for details. Missing file: "
- + testAssetsIOExceptions.getMessage());
- }
- }
- }
-
- private SchemaBundle loadSchema(Context context, int version) throws IOException {
- InputStream input = context.getAssets().open(mAssetsFolder + "/" + version + ".json");
- return SchemaBundle.deserialize(input);
- }
-
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- static TableInfo toTableInfo(EntityBundle entityBundle) {
- return new TableInfo(entityBundle.getTableName(), toColumnMap(entityBundle),
- toForeignKeys(entityBundle.getForeignKeys()), toIndices(entityBundle.getIndices()));
- }
-
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- static FtsTableInfo toFtsTableInfo(FtsEntityBundle ftsEntityBundle) {
- return new FtsTableInfo(ftsEntityBundle.getTableName(), toColumnNamesSet(ftsEntityBundle),
- ftsEntityBundle.getCreateSql());
- }
-
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- static ViewInfo toViewInfo(DatabaseViewBundle viewBundle) {
- return new ViewInfo(viewBundle.getViewName(), viewBundle.createView());
- }
-
- private static Set<TableInfo.Index> toIndices(List<IndexBundle> indices) {
- if (indices == null) {
- return emptySet();
- }
- Set<TableInfo.Index> result = new HashSet<>();
- for (IndexBundle bundle : indices) {
- result.add(new TableInfo.Index(bundle.getName(), bundle.isUnique(),
- bundle.getColumnNames(), bundle.getOrders()));
- }
- return result;
- }
-
- private static Set<TableInfo.ForeignKey> toForeignKeys(
- List<ForeignKeyBundle> bundles) {
- if (bundles == null) {
- return emptySet();
- }
- Set<TableInfo.ForeignKey> result = new HashSet<>(bundles.size());
- for (ForeignKeyBundle bundle : bundles) {
- result.add(new TableInfo.ForeignKey(bundle.getTable(),
- bundle.getOnDelete(), bundle.getOnUpdate(),
- bundle.getColumns(), bundle.getReferencedColumns()));
- }
- return result;
- }
-
- private static Set<String> toColumnNamesSet(EntityBundle entity) {
- Set<String> result = new HashSet<>();
- for (FieldBundle field : entity.getFields()) {
- result.add(field.getColumnName());
- }
- return result;
- }
-
- private static Map<String, TableInfo.Column> toColumnMap(EntityBundle entity) {
- Map<String, TableInfo.Column> result = new HashMap<>();
- for (FieldBundle bundle : entity.getFields()) {
- TableInfo.Column column = toColumn(entity, bundle);
- result.put(column.name, column);
- }
- return result;
- }
-
- private static TableInfo.Column toColumn(EntityBundle entity, FieldBundle field) {
- return new TableInfo.Column(field.getColumnName(), field.getAffinity(),
- field.isNonNull(), findPrimaryKeyPosition(entity, field), field.getDefaultValue(),
- TableInfo.CREATED_FROM_ENTITY);
- }
-
- private static int findPrimaryKeyPosition(EntityBundle entity, FieldBundle field) {
- List<String> columnNames = entity.getPrimaryKey().getColumnNames();
- int i = 0;
- for (String columnName : columnNames) {
- i++;
- if (field.getColumnName().equalsIgnoreCase(columnName)) {
- return i;
- }
- }
- return 0;
- }
-
- static class MigratingDelegate extends RoomOpenHelperDelegate {
- private final boolean mVerifyDroppedTables;
-
- MigratingDelegate(DatabaseBundle databaseBundle, boolean verifyDroppedTables) {
- super(databaseBundle);
- mVerifyDroppedTables = verifyDroppedTables;
- }
-
- @Override
- public void createAllTables(SupportSQLiteDatabase database) {
- throw new UnsupportedOperationException("Was expecting to migrate but received create."
- + "Make sure you have created the database first.");
- }
-
- @NonNull
- @Override
- public RoomOpenHelper.ValidationResult onValidateSchema(
- @NonNull SupportSQLiteDatabase db) {
- final Map<String, EntityBundle> tables = mDatabaseBundle.getEntitiesByTableName();
- for (EntityBundle entity : tables.values()) {
- if (entity instanceof FtsEntityBundle) {
- final FtsTableInfo expected = toFtsTableInfo((FtsEntityBundle) entity);
- final FtsTableInfo found = FtsTableInfo.read(db, entity.getTableName());
- if (!expected.equals(found)) {
- return new ValidationResult(false, expected.name
- + "\nExpected: " + expected + "\nFound: " + found);
- }
- } else {
- final TableInfo expected = toTableInfo(entity);
- final TableInfo found = TableInfo.read(db, entity.getTableName());
- if (!expected.equals(found)) {
- return new ValidationResult(false, expected.name
- + "\nExpected: " + expected + " \nfound: " + found);
- }
- }
- }
- for (DatabaseViewBundle view : mDatabaseBundle.getViews()) {
- final ViewInfo expected = toViewInfo(view);
- final ViewInfo found = ViewInfo.read(db, view.getViewName());
- if (!expected.equals(found)) {
- return new ValidationResult(false, expected
- + "\nExpected: " + expected + " \nfound: " + found);
- }
- }
- if (mVerifyDroppedTables) {
- // now ensure tables that should be removed are removed.
- Set<String> expectedTables = new HashSet<>();
- for (EntityBundle entity : tables.values()) {
- expectedTables.add(entity.getTableName());
- if (entity instanceof FtsEntityBundle) {
- expectedTables.addAll(((FtsEntityBundle) entity).getShadowTableNames());
- }
- }
- Cursor cursor = db.query("SELECT name FROM sqlite_master WHERE type='table'"
- + " AND name NOT IN(?, ?, ?)",
- new String[]{Room.MASTER_TABLE_NAME, "android_metadata",
- "sqlite_sequence"});
- //noinspection TryFinallyCanBeTryWithResources
- try {
- while (cursor.moveToNext()) {
- final String tableName = cursor.getString(0);
- if (!expectedTables.contains(tableName)) {
- return new ValidationResult(false, "Unexpected table "
- + tableName);
- }
- }
- } finally {
- cursor.close();
- }
- }
- return new ValidationResult(true, null);
- }
- }
-
- static class CreatingDelegate extends RoomOpenHelperDelegate {
-
- CreatingDelegate(DatabaseBundle databaseBundle) {
- super(databaseBundle);
- }
-
- @Override
- public void createAllTables(SupportSQLiteDatabase database) {
- for (String query : mDatabaseBundle.buildCreateQueries()) {
- database.execSQL(query);
- }
- }
-
- @NonNull
- @Override
- public RoomOpenHelper.ValidationResult onValidateSchema(
- @NonNull SupportSQLiteDatabase db) {
- throw new UnsupportedOperationException("This open helper just creates the database but"
- + " it received a migration request.");
- }
- }
-
- abstract static class RoomOpenHelperDelegate extends RoomOpenHelper.Delegate {
- final DatabaseBundle mDatabaseBundle;
-
- RoomOpenHelperDelegate(DatabaseBundle databaseBundle) {
- super(databaseBundle.getVersion());
- mDatabaseBundle = databaseBundle;
- }
-
- @Override
- public void dropAllTables(SupportSQLiteDatabase database) {
- throw new UnsupportedOperationException("cannot drop all tables in the test");
- }
-
- @Override
- public void onCreate(SupportSQLiteDatabase database) {
- }
-
- @Override
- public void onOpen(SupportSQLiteDatabase database) {
- }
- }
-}
diff --git a/room/room-testing/src/main/java/androidx/room/testing/MigrationTestHelper.kt b/room/room-testing/src/main/java/androidx/room/testing/MigrationTestHelper.kt
new file mode 100644
index 0000000..0f33171
--- /dev/null
+++ b/room/room-testing/src/main/java/androidx/room/testing/MigrationTestHelper.kt
@@ -0,0 +1,679 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.room.testing
+
+import android.annotation.SuppressLint
+import android.app.Instrumentation
+import android.content.Context
+import android.util.Log
+import androidx.arch.core.executor.ArchTaskExecutor
+import androidx.room.DatabaseConfiguration
+import androidx.room.Room
+import androidx.room.Room.getGeneratedImplementation
+import androidx.room.RoomDatabase
+import androidx.room.RoomOpenHelper
+import androidx.room.migration.AutoMigrationSpec
+import androidx.room.migration.Migration
+import androidx.room.migration.bundle.DatabaseBundle
+import androidx.room.migration.bundle.DatabaseViewBundle
+import androidx.room.migration.bundle.EntityBundle
+import androidx.room.migration.bundle.FieldBundle
+import androidx.room.migration.bundle.ForeignKeyBundle
+import androidx.room.migration.bundle.FtsEntityBundle
+import androidx.room.migration.bundle.IndexBundle
+import androidx.room.migration.bundle.SchemaBundle
+import androidx.room.migration.bundle.SchemaBundle.Companion.deserialize
+import androidx.room.util.FtsTableInfo
+import androidx.room.util.TableInfo
+import androidx.room.util.ViewInfo
+import androidx.room.util.useCursor
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.SupportSQLiteOpenHelper
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
+import java.io.File
+import java.io.FileNotFoundException
+import java.io.IOException
+import java.lang.ref.WeakReference
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+/**
+ * A class that can be used in your Instrumentation tests that can create the database in an
+ * older schema.
+ *
+ * You must copy the schema json files (created by passing `room.schemaLocation` argument
+ * into the annotation processor) into your test assets and pass in the path for that folder into
+ * the constructor. This class will read the folder and extract the schemas from there.
+ *
+ * ```
+ * android {
+ * defaultConfig {
+ * javaCompileOptions {
+ * annotationProcessorOptions {
+ * arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
+ * }
+ * }
+ * }
+ * sourceSets {
+ * androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
+ * }
+ * }
+ * ```
+ */
+open class MigrationTestHelper : TestWatcher {
+ private val assetsFolder: String
+ private val openFactory: SupportSQLiteOpenHelper.Factory
+ private val managedDatabases = mutableListOf<WeakReference<SupportSQLiteDatabase>>()
+ private val managedRoomDatabases = mutableListOf<WeakReference<RoomDatabase>>()
+ private var testStarted = false
+ private val instrumentation: Instrumentation
+ private val specs: List<AutoMigrationSpec>
+ private val databaseClass: Class<out RoomDatabase>?
+ internal lateinit var databaseConfiguration: DatabaseConfiguration
+
+ /**
+ * Creates a new migration helper. It uses the Instrumentation context to load the schema
+ * (falls back to the app resources) and the target context to create the database.
+ *
+ * @param instrumentation The instrumentation instance.
+ * @param assetsFolder The asset folder in the assets directory.
+ * @param openFactory factory for creating an [SupportSQLiteOpenHelper]
+ */
+ @Deprecated(
+ """
+ Cannot be used to run migration tests involving [AutoMigration].
+ To test [AutoMigration], you must use [MigrationTestHelper(Instrumentation, Class, List,
+ SupportSQLiteOpenHelper.Factory)] for tests containing a
+ [androidx.room.ProvidedAutoMigrationSpec], or use
+ [MigrationTestHelper(Instrumentation, Class, List)] otherwise.
+ """
+ )
+ @JvmOverloads
+ constructor(
+ instrumentation: Instrumentation,
+ assetsFolder: String,
+ openFactory: SupportSQLiteOpenHelper.Factory = FrameworkSQLiteOpenHelperFactory()
+ ) {
+ this.instrumentation = instrumentation
+ this.assetsFolder = assetsFolder
+ this.openFactory = openFactory
+ databaseClass = null
+ specs = mutableListOf()
+ }
+
+ /**
+ * Creates a new migration helper. It uses the Instrumentation context to load the schema
+ * (falls back to the app resources) and the target context to create the database.
+ *
+ * An instance of a class annotated with [androidx.room.ProvidedAutoMigrationSpec] has
+ * to be provided to Room using this constructor. MigrationTestHelper will map auto migration
+ * spec classes to their provided instances before running and validating the Migrations.
+ *
+ * @param instrumentation The instrumentation instance.
+ * @param databaseClass The Database class to be tested.
+ */
+ constructor(
+ instrumentation: Instrumentation,
+ databaseClass: Class<out RoomDatabase>
+ ) : this(
+ instrumentation, databaseClass, emptyList(), FrameworkSQLiteOpenHelperFactory()
+ )
+
+ /**
+ * Creates a new migration helper. It uses the Instrumentation context to load the schema
+ * (falls back to the app resources) and the target context to create the database.
+ *
+ *
+ * An instance of a class annotated with [androidx.room.ProvidedAutoMigrationSpec] has
+ * to be provided to Room using this constructor. MigrationTestHelper will map auto migration
+ * spec classes to their provided instances before running and validating the Migrations.
+ *
+ * @param instrumentation The instrumentation instance.
+ * @param databaseClass The Database class to be tested.
+ * @param specs The list of available auto migration specs that will be provided to
+ * Room at runtime.
+ * @param openFactory factory for creating an [SupportSQLiteOpenHelper]
+ */
+ @JvmOverloads
+ constructor(
+ instrumentation: Instrumentation,
+ databaseClass: Class<out RoomDatabase>,
+ specs: List<AutoMigrationSpec>,
+ openFactory: SupportSQLiteOpenHelper.Factory = FrameworkSQLiteOpenHelperFactory()
+ ) {
+ this.assetsFolder = checkNotNull(databaseClass.canonicalName).let {
+ if (it.endsWith("/")) {
+ it.substring(0, databaseClass.canonicalName!!.length - 1)
+ } else {
+ it
+ }
+ }
+ this.instrumentation = instrumentation
+ this.openFactory = openFactory
+ this.databaseClass = databaseClass
+ this.specs = specs
+ }
+
+ override fun starting(description: Description?) {
+ super.starting(description)
+ testStarted = true
+ }
+
+ /**
+ * Creates the database in the given version.
+ * If the database file already exists, it tries to delete it first. If delete fails, throws
+ * an exception.
+ *
+ * @param name The name of the database.
+ * @param version The version in which the database should be created.
+ * @return A database connection which has the schema in the requested version.
+ */
+ @SuppressLint("RestrictedApi")
+ @Throws(IOException::class)
+ open fun createDatabase(name: String, version: Int): SupportSQLiteDatabase {
+ val dbPath: File = instrumentation.targetContext.getDatabasePath(name)
+ if (dbPath.exists()) {
+ Log.d(TAG, "deleting database file $name")
+ check(dbPath.delete()) {
+ "There is a database file and I could not delete" +
+ " it. Make sure you don't have any open connections to that database" +
+ " before calling this method."
+ }
+ }
+ val schemaBundle = loadSchema(version)
+ val container: RoomDatabase.MigrationContainer = RoomDatabase.MigrationContainer()
+ val configuration = DatabaseConfiguration(
+ context = instrumentation.targetContext,
+ name = name,
+ sqliteOpenHelperFactory = openFactory,
+ migrationContainer = container,
+ callbacks = null,
+ allowMainThreadQueries = true,
+ journalMode = RoomDatabase.JournalMode.TRUNCATE,
+ queryExecutor = ArchTaskExecutor.getIOThreadExecutor(),
+ transactionExecutor = ArchTaskExecutor.getIOThreadExecutor(),
+ multiInstanceInvalidationServiceIntent = null,
+ requireMigration = true,
+ allowDestructiveMigrationOnDowngrade = false,
+ migrationNotRequiredFrom = emptySet(),
+ copyFromAssetPath = null,
+ copyFromFile = null,
+ copyFromInputStream = null,
+ prepackagedDatabaseCallback = null,
+ typeConverters = emptyList(),
+ autoMigrationSpecs = emptyList()
+ )
+ val roomOpenHelper = RoomOpenHelper(
+ configuration = configuration,
+ delegate = CreatingDelegate(schemaBundle.database),
+ identityHash = schemaBundle.database.identityHash,
+ // we pass the same hash twice since an old schema does not necessarily have
+ // a legacy hash and we would not even persist it.
+ legacyHash = schemaBundle.database.identityHash
+ )
+ return openDatabase(name, roomOpenHelper)
+ }
+
+ /**
+ * Runs the given set of migrations on the provided database.
+ *
+ * It uses the same algorithm that Room uses to choose migrations so the migrations instances
+ * that are provided to this method must be sufficient to bring the database from current
+ * version to the desired version.
+ *
+ * After the migration, the method validates the database schema to ensure that migration
+ * result matches the expected schema. Handling of dropped tables depends on the
+ * `validateDroppedTables` argument. If set to true, the verification will fail if it
+ * finds a table that is not registered in the Database. If set to false, extra tables in the
+ * database will be ignored (this is the runtime library behavior).
+ *
+ * @param name The database name. You must first create this database via
+ * [createDatabase].
+ * @param version The final version after applying the migrations.
+ * @param validateDroppedTables If set to true, validation will fail if the database has
+ * unknown tables.
+ * @param migrations The list of available migrations.
+ * @throws IllegalArgumentException If the schema validation fails.
+ */
+ @SuppressLint("RestrictedApi")
+ open fun runMigrationsAndValidate(
+ name: String,
+ version: Int,
+ validateDroppedTables: Boolean,
+ vararg migrations: Migration
+ ): SupportSQLiteDatabase {
+ val dbPath = instrumentation.targetContext.getDatabasePath(name)
+ check(dbPath.exists()) {
+ "Cannot find the database file for $name. " +
+ "Before calling runMigrations, you must first create the database via " +
+ "createDatabase."
+ }
+ val schemaBundle = loadSchema(version)
+ val container = RoomDatabase.MigrationContainer()
+ container.addMigrations(*migrations)
+ val autoMigrations = getAutoMigrations(specs)
+ autoMigrations.forEach { autoMigration ->
+ val migrationExists = container.contains(
+ autoMigration.startVersion,
+ autoMigration.endVersion
+ )
+ if (!migrationExists) {
+ container.addMigrations(autoMigration)
+ }
+ }
+ databaseConfiguration = DatabaseConfiguration(
+ context = instrumentation.targetContext,
+ name = name,
+ sqliteOpenHelperFactory = openFactory,
+ migrationContainer = container,
+ callbacks = null,
+ allowMainThreadQueries = true,
+ journalMode = RoomDatabase.JournalMode.TRUNCATE,
+ queryExecutor = ArchTaskExecutor.getIOThreadExecutor(),
+ transactionExecutor = ArchTaskExecutor.getIOThreadExecutor(),
+ multiInstanceInvalidationServiceIntent = null,
+ requireMigration = true,
+ allowDestructiveMigrationOnDowngrade = false,
+ migrationNotRequiredFrom = emptySet(),
+ copyFromAssetPath = null,
+ copyFromFile = null,
+ copyFromInputStream = null,
+ prepackagedDatabaseCallback = null,
+ typeConverters = emptyList(),
+ autoMigrationSpecs = emptyList()
+ )
+ val roomOpenHelper = RoomOpenHelper(
+ configuration = databaseConfiguration,
+ delegate = MigratingDelegate(
+ databaseBundle = schemaBundle.database,
+ // we pass the same hash twice since an old schema does not necessarily have
+ // a legacy hash and we would not even persist it.
+ mVerifyDroppedTables = validateDroppedTables
+ ),
+ identityHash = schemaBundle.database.identityHash,
+ legacyHash = schemaBundle.database.identityHash
+ )
+ return openDatabase(name, roomOpenHelper)
+ }
+
+ /**
+ * Returns a list of [Migration] of a database that has been generated using
+ * [androidx.room.AutoMigration].
+ */
+ private fun getAutoMigrations(userProvidedSpecs: List<AutoMigrationSpec>): List<Migration> {
+ if (databaseClass == null) {
+ return if (userProvidedSpecs.isEmpty()) {
+ // TODO: Detect that there are auto migrations to test when a deprecated
+ // constructor is used.
+ Log.e(
+ TAG, "If you have any AutoMigrations in your implementation, you must use " +
+ "a non-deprecated MigrationTestHelper constructor to provide the " +
+ "Database class in order to test them. If you do not have any " +
+ "AutoMigrations to test, you may ignore this warning."
+ )
+ mutableListOf()
+ } else {
+ error(
+ "You must provide the database class in the " +
+ "MigrationTestHelper constructor in order to test auto migrations."
+ )
+ }
+ }
+ val db: RoomDatabase = getGeneratedImplementation(
+ databaseClass, "_Impl"
+ )
+ val requiredAutoMigrationSpecs = db.getRequiredAutoMigrationSpecs()
+ return db.getAutoMigrations(
+ createAutoMigrationSpecMap(requiredAutoMigrationSpecs, userProvidedSpecs)
+ )
+ }
+
+ /**
+ * Maps auto migration spec classes to their provided instance.
+ */
+ private fun createAutoMigrationSpecMap(
+ requiredAutoMigrationSpecs: Set<Class<out AutoMigrationSpec>>,
+ userProvidedSpecs: List<AutoMigrationSpec>
+ ): Map<Class<out AutoMigrationSpec>, AutoMigrationSpec> {
+ if (requiredAutoMigrationSpecs.isEmpty()) {
+ return emptyMap()
+ }
+ return buildMap {
+ requiredAutoMigrationSpecs.forEach { spec ->
+ val match = userProvidedSpecs.firstOrNull { provided ->
+ spec.isAssignableFrom(provided.javaClass)
+ }
+ require(match != null) {
+ "A required auto migration spec (${spec.canonicalName}) has not been provided."
+ }
+ put(spec, match)
+ }
+ }
+ }
+
+ private fun openDatabase(name: String, roomOpenHelper: RoomOpenHelper): SupportSQLiteDatabase {
+ val config = SupportSQLiteOpenHelper.Configuration.builder(instrumentation.targetContext)
+ .callback(roomOpenHelper)
+ .name(name)
+ .build()
+ val db = openFactory.create(config).writableDatabase
+ managedDatabases.add(WeakReference(db))
+ return db
+ }
+
+ override fun finished(description: Description?) {
+ super.finished(description)
+ managedDatabases.forEach { dbRef ->
+ val db = dbRef.get()
+ if (db != null && db.isOpen) {
+ try {
+ db.close()
+ } catch (ignored: Throwable) {
+ }
+ }
+ }
+ managedRoomDatabases.forEach { dbRef ->
+ val roomDatabase = dbRef.get()
+ roomDatabase?.close()
+ }
+ }
+
+ /**
+ * Registers a database connection to be automatically closed when the test finishes.
+ *
+ * This only works if `MigrationTestHelper` is registered as a Junit test rule via
+ * [Rule][org.junit.Rule] annotation.
+ *
+ * @param db The database connection that should be closed after the test finishes.
+ */
+ open fun closeWhenFinished(db: SupportSQLiteDatabase) {
+ check(testStarted) {
+ "You cannot register a database to be closed before" +
+ " the test starts. Maybe you forgot to annotate MigrationTestHelper as a" +
+ " test rule? (@Rule)"
+ }
+ managedDatabases.add(WeakReference(db))
+ }
+
+ /**
+ * Registers a database connection to be automatically closed when the test finishes.
+ *
+ * This only works if `MigrationTestHelper` is registered as a Junit test rule via
+ * [Rule][org.junit.Rule] annotation.
+ *
+ * @param db The RoomDatabase instance which holds the database.
+ */
+ open fun closeWhenFinished(db: RoomDatabase) {
+ check(testStarted) {
+ "You cannot register a database to be closed before" +
+ " the test starts. Maybe you forgot to annotate MigrationTestHelper as a" +
+ " test rule? (@Rule)"
+ }
+ managedRoomDatabases.add(WeakReference(db))
+ }
+
+ private fun loadSchema(version: Int): SchemaBundle {
+ return try {
+ loadSchema(instrumentation.context, version)
+ } catch (testAssetsIOExceptions: FileNotFoundException) {
+ Log.w(
+ TAG, "Could not find the schema file in the test assets. Checking the" +
+ " application assets"
+ )
+ try {
+ loadSchema(instrumentation.targetContext, version)
+ } catch (appAssetsException: FileNotFoundException) {
+ // throw the test assets exception instead
+ throw FileNotFoundException(
+ "Cannot find the schema file in the assets folder. " +
+ "Make sure to include the exported json schemas in your test assert " +
+ "inputs. See " +
+ "https://developer.android.com/training/data-storage/room/" +
+ "migrating-db-versions#export-schema for details. Missing file: " +
+ testAssetsIOExceptions.message
+ )
+ }
+ }
+ }
+
+ private fun loadSchema(context: Context, version: Int): SchemaBundle {
+ val input = context.assets.open("$assetsFolder/$version.json")
+ return deserialize(input)
+ }
+
+ internal class MigratingDelegate(
+ databaseBundle: DatabaseBundle,
+ private val mVerifyDroppedTables: Boolean
+ ) : RoomOpenHelperDelegate(databaseBundle) {
+ override fun createAllTables(database: SupportSQLiteDatabase) {
+ throw UnsupportedOperationException(
+ "Was expecting to migrate but received create." +
+ "Make sure you have created the database first."
+ )
+ }
+
+ override fun onValidateSchema(
+ db: SupportSQLiteDatabase
+ ): RoomOpenHelper.ValidationResult {
+ val tables = mDatabaseBundle.entitiesByTableName
+ tables.values.forEach { entity ->
+ if (entity is FtsEntityBundle) {
+ val expected = toFtsTableInfo(entity)
+ val found = FtsTableInfo.read(db, entity.tableName)
+ if (expected != found) {
+ return RoomOpenHelper.ValidationResult(
+ false,
+ """
+ ${expected.name}
+ Expected: $expected
+ Found: $found
+ """.trimIndent()
+ )
+ }
+ } else {
+ val expected = toTableInfo(entity)
+ val found = TableInfo.read(db, entity.tableName)
+ if (expected != found) {
+ return RoomOpenHelper.ValidationResult(
+ false,
+ """
+ ${expected.name}
+ Expected: $expected
+ found: $found
+ """.trimIndent()
+ )
+ }
+ }
+ }
+ mDatabaseBundle.views.forEach { view ->
+ val expected = toViewInfo(view)
+ val found = ViewInfo.read(db, view.viewName)
+ if (expected != found) {
+ return RoomOpenHelper.ValidationResult(
+ false,
+ """
+ ${expected.name}
+ Expected: $expected
+ Found: $found
+ """.trimIndent()
+ )
+ }
+ }
+ if (mVerifyDroppedTables) {
+ // now ensure tables that should be removed are removed.
+ val expectedTables = buildSet {
+ tables.values.forEach { entity ->
+ add(entity.tableName)
+ if (entity is FtsEntityBundle) {
+ addAll(entity.shadowTableNames)
+ }
+ }
+ }
+ db.query(
+ "SELECT name FROM sqlite_master WHERE type='table'" +
+ " AND name NOT IN(?, ?, ?)",
+ arrayOf(
+ Room.MASTER_TABLE_NAME, "android_metadata",
+ "sqlite_sequence"
+ )
+ ).useCursor { cursor ->
+ while (cursor.moveToNext()) {
+ val tableName = cursor.getString(0)
+ if (!expectedTables.contains(tableName)) {
+ return RoomOpenHelper.ValidationResult(
+ false, "Unexpected table $tableName"
+ )
+ }
+ }
+ }
+ }
+ return RoomOpenHelper.ValidationResult(true, null)
+ }
+ }
+
+ internal class CreatingDelegate(
+ databaseBundle: DatabaseBundle
+ ) : RoomOpenHelperDelegate(databaseBundle) {
+ override fun createAllTables(database: SupportSQLiteDatabase) {
+ mDatabaseBundle.buildCreateQueries().forEach { query ->
+ database.execSQL(query)
+ }
+ }
+
+ override fun onValidateSchema(
+ db: SupportSQLiteDatabase
+ ): RoomOpenHelper.ValidationResult {
+ throw UnsupportedOperationException(
+ "This open helper just creates the database but it received a migration request."
+ )
+ }
+ }
+
+ internal abstract class RoomOpenHelperDelegate(
+ val mDatabaseBundle: DatabaseBundle
+ ) : RoomOpenHelper.Delegate(
+ mDatabaseBundle.version
+ ) {
+ override fun dropAllTables(database: SupportSQLiteDatabase) {
+ throw UnsupportedOperationException("cannot drop all tables in the test")
+ }
+
+ override fun onCreate(database: SupportSQLiteDatabase) {}
+ override fun onOpen(database: SupportSQLiteDatabase) {}
+ }
+
+ internal companion object {
+ private const val TAG = "MigrationTestHelper"
+ @JvmStatic
+ internal fun toTableInfo(entityBundle: EntityBundle): TableInfo {
+ return TableInfo(
+ name = entityBundle.tableName,
+ columns = toColumnMap(entityBundle),
+ foreignKeys = toForeignKeys(entityBundle.foreignKeys),
+ indices = toIndices(entityBundle.indices)
+ )
+ }
+
+ @JvmStatic
+ internal fun toFtsTableInfo(ftsEntityBundle: FtsEntityBundle): FtsTableInfo {
+ return FtsTableInfo(
+ name = ftsEntityBundle.tableName,
+ columns = toColumnNamesSet(ftsEntityBundle),
+ createSql = ftsEntityBundle.createSql
+ )
+ }
+
+ @JvmStatic
+ internal fun toViewInfo(viewBundle: DatabaseViewBundle): ViewInfo {
+ return ViewInfo(
+ name = viewBundle.viewName,
+ sql = viewBundle.createView()
+ )
+ }
+
+ @JvmStatic
+ internal fun toIndices(indices: List<IndexBundle>?): Set<TableInfo.Index> {
+ if (indices == null) {
+ return emptySet()
+ }
+ val result = indices.map { bundle ->
+ TableInfo.Index(
+ name = bundle.name,
+ unique = bundle.isUnique,
+ columns = bundle.columnNames!!,
+ orders = bundle.orders!!
+ )
+ }.toSet()
+ return result
+ }
+
+ @JvmStatic
+ internal fun toForeignKeys(
+ bundles: List<ForeignKeyBundle>?
+ ): Set<TableInfo.ForeignKey> {
+ if (bundles == null) {
+ return emptySet()
+ }
+ val result = bundles.map { bundle ->
+ TableInfo.ForeignKey(
+ referenceTable = bundle.table,
+ onDelete = bundle.onDelete,
+ onUpdate = bundle.onUpdate,
+ columnNames = bundle.columns,
+ referenceColumnNames = bundle.referencedColumns
+ )
+ }.toSet()
+ return result
+ }
+
+ @JvmStatic
+ internal fun toColumnNamesSet(entity: EntityBundle): Set<String> {
+ val result = entity.fields.map { field ->
+ field.columnName
+ }.toSet()
+ return result
+ }
+
+ @JvmStatic
+ internal fun toColumnMap(entity: EntityBundle): Map<String, TableInfo.Column> {
+ val result: MutableMap<String, TableInfo.Column> = HashMap()
+ entity.fields.associateBy { bundle ->
+ val column = toColumn(entity, bundle)
+ result[column.name] = column
+ }
+ return result
+ }
+
+ @JvmStatic
+ internal fun toColumn(entity: EntityBundle, field: FieldBundle): TableInfo.Column {
+ return TableInfo.Column(
+ name = field.columnName,
+ type = field.affinity,
+ notNull = field.isNonNull,
+ primaryKeyPosition = findPrimaryKeyPosition(entity, field),
+ defaultValue = field.defaultValue,
+ createdFrom = TableInfo.CREATED_FROM_ENTITY
+ )
+ }
+
+ @JvmStatic
+ internal fun findPrimaryKeyPosition(entity: EntityBundle, field: FieldBundle): Int {
+ return entity.primaryKey.columnNames.indexOfFirst { columnName ->
+ field.columnName.equals(columnName, ignoreCase = true)
+ } + 1 // Shift by 1 to get primary key position
+ }
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index fbc4a7c..331f5c6 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -384,6 +384,7 @@
includeProject(":benchmark:benchmark-darwin", [BuildType.KMP])
includeProject(":benchmark:benchmark-darwin-core", [BuildType.KMP])
includeProject(":benchmark:benchmark-darwin-samples", [BuildType.KMP])
+includeProject(":benchmark:benchmark-darwin-gradle-plugin", [BuildType.KMP])
includeProject(":benchmark:benchmark-gradle-plugin", "benchmark/gradle-plugin", [BuildType.MAIN])
includeProject(":benchmark:benchmark-junit4")
includeProject(":benchmark:benchmark-macro", [BuildType.MAIN, BuildType.COMPOSE])
diff --git a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt
index a52636c..00257a1 100644
--- a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt
+++ b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt
@@ -114,7 +114,7 @@
override val isExecPerConnectionSQLSupported: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
- override fun execPerConnectionSQL(sql: String, bindArgs: Array<Any?>?) {
+ override fun execPerConnectionSQL(sql: String, bindArgs: Array<out Any?>?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Api30Impl.execPerConnectionSQL(delegate, sql, bindArgs)
} else {
@@ -135,7 +135,7 @@
return query(SimpleSQLiteQuery(query))
}
- override fun query(query: String, bindArgs: Array<Any?>): Cursor {
+ override fun query(query: String, bindArgs: Array<out Any?>): Cursor {
return query(SimpleSQLiteQuery(query, bindArgs))
}
@@ -182,7 +182,7 @@
return delegate.insertWithOnConflict(table, null, values, conflictAlgorithm)
}
- override fun delete(table: String, whereClause: String?, whereArgs: Array<Any?>?): Int {
+ override fun delete(table: String, whereClause: String?, whereArgs: Array<out Any?>?): Int {
val query = buildString {
append("DELETE FROM ")
append(table)
@@ -201,7 +201,7 @@
conflictAlgorithm: Int,
values: ContentValues,
whereClause: String?,
- whereArgs: Array<Any?>?
+ whereArgs: Array<out Any?>?
): Int {
// taken from SQLiteDatabase class.
require(values.size() != 0) { "Empty values" }
@@ -247,7 +247,7 @@
}
@Throws(SQLException::class)
- override fun execSQL(sql: String, bindArgs: Array<Any?>) {
+ override fun execSQL(sql: String, bindArgs: Array<out Any?>) {
delegate.execSQL(sql, bindArgs)
}
@@ -313,7 +313,7 @@
fun execPerConnectionSQL(
sQLiteDatabase: SQLiteDatabase,
sql: String,
- bindArgs: Array<Any?>?
+ bindArgs: Array<out Any?>?
) {
sQLiteDatabase.execPerConnectionSQL(sql, bindArgs)
}
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.kt b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.kt
index 2e6a9d9..e3245e2 100644
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.kt
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.kt
@@ -29,7 +29,7 @@
class SimpleSQLiteQuery(
private val query: String,
@Suppress("ArrayReturn") // Due to legacy API
- private val bindArgs: Array<Any?>?
+ private val bindArgs: Array<out Any?>?
) : SupportSQLiteQuery {
/**
@@ -66,7 +66,7 @@
fun bind(
statement: SupportSQLiteProgram,
@Suppress("ArrayReturn") // Due to legacy API
- bindArgs: Array<Any?>?
+ bindArgs: Array<out Any?>?
) {
if (bindArgs == null) {
return
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.kt b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.kt
index c0d7fd8..553c19b 100644
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.kt
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.kt
@@ -106,7 +106,7 @@
fun rawQueryWithFactory(
sQLiteDatabase: SQLiteDatabase,
sql: String,
- selectionArgs: Array<String?>,
+ selectionArgs: Array<out String?>,
editTable: String?,
cancellationSignal: CancellationSignal,
cursorFactory: CursorFactory
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.kt b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.kt
index dc474eb..bd253f2cc 100644
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.kt
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.kt
@@ -234,7 +234,7 @@
@Suppress("AcronymName") // To keep consistency with framework method name.
fun execPerConnectionSQL(
sql: String,
- @SuppressLint("ArrayReturn") bindArgs: Array<Any?>?
+ @SuppressLint("ArrayReturn") bindArgs: Array<out Any?>?
) {
throw UnsupportedOperationException()
}
@@ -288,7 +288,7 @@
* @return A [Cursor] object, which is positioned before the first entry. Note that
* [Cursor]s are not synchronized, see the documentation for more details.
*/
- fun query(query: String, bindArgs: Array<Any?>): Cursor
+ fun query(query: String, bindArgs: Array<out Any?>): Cursor
/**
* Runs the given query on the database.
@@ -354,7 +354,7 @@
* otherwise. To remove all rows and get a count pass "1" as the
* whereClause.
*/
- fun delete(table: String, whereClause: String?, whereArgs: Array<Any?>?): Int
+ fun delete(table: String, whereClause: String?, whereArgs: Array<out Any?>?): Int
/**
* Convenience method for updating rows in the database.
@@ -381,7 +381,7 @@
conflictAlgorithm: Int,
values: ContentValues,
whereClause: String?,
- whereArgs: Array<Any?>?
+ whereArgs: Array<out Any?>?
): Int
/**
@@ -413,7 +413,7 @@
* @throws SQLException if the SQL string is invalid
*/
@Throws(SQLException::class)
- fun execSQL(sql: String, bindArgs: Array<Any?>)
+ fun execSQL(sql: String, bindArgs: Array<out Any?>)
/**
* Is true if the database is opened as read only.
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQueryBuilder.kt b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQueryBuilder.kt
index 044a86f..ed54ec4 100644
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQueryBuilder.kt
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQueryBuilder.kt
@@ -22,9 +22,9 @@
*/
class SupportSQLiteQueryBuilder private constructor(private val table: String) {
private var distinct = false
- private var columns: Array<String>? = null
+ private var columns: Array<out String>? = null
private var selection: String? = null
- private var bindArgs: Array<Any?>? = null
+ private var bindArgs: Array<out Any?>? = null
private var groupBy: String? = null
private var having: String? = null
private var orderBy: String? = null
@@ -46,7 +46,7 @@
*
* @return this
*/
- fun columns(columns: Array<String>?): SupportSQLiteQueryBuilder = apply {
+ fun columns(columns: Array<out String>?): SupportSQLiteQueryBuilder = apply {
this.columns = columns
}
@@ -58,7 +58,10 @@
*
* @return this
*/
- fun selection(selection: String?, bindArgs: Array<Any?>?): SupportSQLiteQueryBuilder = apply {
+ fun selection(
+ selection: String?,
+ bindArgs: Array<out Any?>?
+ ): SupportSQLiteQueryBuilder = apply {
this.selection = selection
this.bindArgs = bindArgs
}
@@ -153,7 +156,7 @@
* Add the names that are non-null in columns to string, separating
* them with commas.
*/
- private fun StringBuilder.appendColumns(columns: Array<String>) {
+ private fun StringBuilder.appendColumns(columns: Array<out String>) {
val n = columns.size
for (i in 0 until n) {
val column = columns[i]
diff --git a/sqlite/sqlite/src/test/java/androidx/sqlite/db/SupportSQLiteQueryBuilderTest.kt b/sqlite/sqlite/src/test/java/androidx/sqlite/db/SupportSQLiteQueryBuilderTest.kt
index cd0e65e..9d9a173 100644
--- a/sqlite/sqlite/src/test/java/androidx/sqlite/db/SupportSQLiteQueryBuilderTest.kt
+++ b/sqlite/sqlite/src/test/java/androidx/sqlite/db/SupportSQLiteQueryBuilderTest.kt
@@ -51,4 +51,17 @@
.create()
assertThat(query.sql).isEqualTo("SELECT * FROM Books GROUP BY pages HAVING >100")
}
+
+ @Test
+ fun subtypes_in_array_selection_does_not_throw_error() {
+ val bindArgs: Array<String?> = arrayOf("USA")
+
+ val query = SupportSQLiteQueryBuilder.builder("Books")
+ .columns(arrayOf("country_published"))
+ .selection("country_published=USA", bindArgs)
+ .create()
+ assertThat(query.sql).isEqualTo(
+ "SELECT country_published FROM Books WHERE country_published=USA"
+ )
+ }
}
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
index 4f035e8..8f00efe 100644
--- a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
@@ -60,7 +60,6 @@
public enum ComplicationType {
method public static androidx.wear.watchface.complications.data.ComplicationType valueOf(String name) throws java.lang.IllegalArgumentException;
method public static androidx.wear.watchface.complications.data.ComplicationType[] values();
- enum_constant @androidx.wear.watchface.complications.data.ComplicationExperimental public static final androidx.wear.watchface.complications.data.ComplicationType DISCRETE_RANGED_VALUE;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType EMPTY;
enum_constant @androidx.wear.watchface.complications.data.ComplicationExperimental public static final androidx.wear.watchface.complications.data.ComplicationType GOAL_PROGRESS;
enum_constant @androidx.wear.watchface.complications.data.ComplicationExperimental public static final androidx.wear.watchface.complications.data.ComplicationType LIST;
@@ -92,41 +91,6 @@
public final class DataKt {
}
- @androidx.wear.watchface.complications.data.ComplicationExperimental public final class DiscreteRangedValueComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
- method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
- method public int getMax();
- method public int getMin();
- method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
- method public androidx.wear.watchface.complications.data.SmallImage? getSmallImage();
- method public androidx.wear.watchface.complications.data.ComplicationText? getText();
- method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
- method public int getValue();
- property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
- property public final int max;
- property public final int min;
- property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
- property public final androidx.wear.watchface.complications.data.SmallImage? smallImage;
- property public final androidx.wear.watchface.complications.data.ComplicationText? text;
- property public final androidx.wear.watchface.complications.data.ComplicationText? title;
- property public final int value;
- field public static final int PLACEHOLDER;
- field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
- }
-
- public static final class DiscreteRangedValueComplicationData.Builder {
- ctor public DiscreteRangedValueComplicationData.Builder(int value, int min, int max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
- method public androidx.wear.watchface.complications.data.DiscreteRangedValueComplicationData build();
- method public final T setDataSource(android.content.ComponentName? dataSource);
- method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
- method public androidx.wear.watchface.complications.data.DiscreteRangedValueComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
- method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
- method public androidx.wear.watchface.complications.data.DiscreteRangedValueComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
- method public androidx.wear.watchface.complications.data.DiscreteRangedValueComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
- method public androidx.wear.watchface.complications.data.DiscreteRangedValueComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
- method public androidx.wear.watchface.complications.data.DiscreteRangedValueComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
- method public androidx.wear.watchface.complications.data.DiscreteRangedValueComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
- }
-
public final class EmptyComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
ctor public EmptyComplicationData();
field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
@@ -361,6 +325,7 @@
method public androidx.wear.watchface.complications.data.ComplicationText? getText();
method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
method public float getValue();
+ method @androidx.wear.watchface.complications.data.ComplicationExperimental public int getValueType();
property @androidx.wear.watchface.complications.data.ComplicationExperimental public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
property public final float max;
@@ -370,6 +335,7 @@
property public final androidx.wear.watchface.complications.data.ComplicationText? text;
property public final androidx.wear.watchface.complications.data.ComplicationText? title;
property public final float value;
+ property @androidx.wear.watchface.complications.data.ComplicationExperimental public final int valueType;
field public static final float PLACEHOLDER;
field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
}
@@ -387,6 +353,13 @@
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
+ method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setValueType(int valueType);
+ }
+
+ @androidx.wear.watchface.complications.data.ComplicationExperimental public final class RangedValueTypes {
+ field public static final androidx.wear.watchface.complications.data.RangedValueTypes INSTANCE;
+ field public static final int SCORE = 1; // 0x1
+ field public static final int UNDEFINED = 0; // 0x0
}
public final class ShortTextComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
@@ -535,17 +508,21 @@
@androidx.wear.watchface.complications.data.ComplicationExperimental public final class WeightedElementsComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
+ method public int getElementBackgroundColor();
method public java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> getElements();
method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
method public androidx.wear.watchface.complications.data.SmallImage? getSmallImage();
method public androidx.wear.watchface.complications.data.ComplicationText? getText();
method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
+ property public final int elementBackgroundColor;
property public final java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> elements;
property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
property public final androidx.wear.watchface.complications.data.SmallImage? smallImage;
property public final androidx.wear.watchface.complications.data.ComplicationText? text;
property public final androidx.wear.watchface.complications.data.ComplicationText? title;
+ field public static final androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Companion Companion;
+ field public static final int MAX_NUM_ELEMENTS = 20; // 0x14
field public static final java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> PLACEHOLDER;
field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
}
@@ -555,6 +532,7 @@
method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+ method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setElementBackgroundColor(@ColorInt int elementBackgroundColor);
method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
@@ -564,6 +542,9 @@
method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
}
+ public static final class WeightedElementsComplicationData.Companion {
+ }
+
public static final class WeightedElementsComplicationData.Element {
ctor public WeightedElementsComplicationData.Element(@FloatRange(from=0.0, fromInclusive=false) float weight, @ColorInt int color);
method public int getColor();
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
index 5f2fa3d..65924c0 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
@@ -246,20 +246,6 @@
public static final int TYPE_GOAL_PROGRESS = 13;
/**
- * Type used for complications including a discrete integer value within a range, such as a
- * 3/6 daily cups of water drunk. The value may be accompanied by an icon and/or short text
- * and title.
- *
- * <p>The <i>value</i>, <i>min value</i>, and <i>max value</i> fields are required for this
- * type, and the value within the range is expected to always be displayed.
- *
- * <p>The <i>icon</i> (and <i>burnInProtectionIcon</i>), <i>short title</i>, and <i>short
- * text</i> fields are optional for this type, but at least one must be defined. The watch face
- * may choose which of these fields to display, if any.
- */
- public static final int TYPE_DISCRETE_RANGED_VALUE = 14;
-
- /**
* Type used for complications to display a series of weighted values e.g. in a pie chart. The
* weighted values may be accompanied by an icon and/or short text and title.
*
@@ -270,7 +256,7 @@
* text</i> fields are optional for this type, but at least one must be defined. The watch face
* may choose which of these fields to display, if any.
*/
- public static final int TYPE_WEIGHTED_ELEMENTS = 15;
+ public static final int TYPE_WEIGHTED_ELEMENTS = 14;
/** @hide */
@IntDef({IMAGE_STYLE_PHOTO, IMAGE_STYLE_ICON})
@@ -300,15 +286,13 @@
private static final String FIELD_COLOR_RAMP_INTERPOLATED = "COLOR_RAMP_INTERPOLATED";
private static final String FIELD_DATA_SOURCE = "FIELD_DATA_SOURCE";
private static final String FIELD_DISPLAY_POLICY = "DISPLAY_POLICY";
+ private static final String FIELD_ELEMENT_BACKGROUND_COLOR = "ELEMENT_BACKGROUND_COLOR";
private static final String FIELD_ELEMENT_COLORS = "ELEMENT_COLORS";
private static final String FIELD_ELEMENT_WEIGHTS = "ELEMENT_WEIGHTS";
private static final String FIELD_END_TIME = "END_TIME";
private static final String FIELD_ICON = "ICON";
private static final String FIELD_ICON_BURN_IN_PROTECTION = "ICON_BURN_IN_PROTECTION";
private static final String FIELD_IMAGE_STYLE = "IMAGE_STYLE";
- private static final String FIELD_INT_MAX_VALUE = "INT_MAX_VALUE";
- private static final String FIELD_INT_MIN_VALUE = "INT_MIN_VALUE";
- private static final String FIELD_INT_VALUE = "INT_VALUE";
private static final String FIELD_LARGE_IMAGE = "LARGE_IMAGE";
private static final String FIELD_LIST_ENTRIES = "LIST_ENTRIES";
private static final String FIELD_LIST_ENTRY_TYPE = "LIST_ENTRY_TYPE";
@@ -337,6 +321,7 @@
private static final String FIELD_TIMELINE_ENTRIES = "TIMELINE";
private static final String FIELD_TIMELINE_ENTRY_TYPE = "TIMELINE_ENTRY_TYPE";
private static final String FIELD_VALUE = "VALUE";
+ private static final String FIELD_VALUE_TYPE = "VALUE_TYPE";
// Originally it was planned to support both content and image content descriptions.
private static final String FIELD_CONTENT_DESCRIPTION = "IMAGE_CONTENT_DESCRIPTION";
@@ -362,8 +347,11 @@
},
{FIELD_LIST_ENTRIES}, // TYPE_LIST
{FIELD_VALUE, FIELD_TARGET_VALUE}, // GOAL_PROGRESS
- {FIELD_INT_VALUE, FIELD_INT_MIN_VALUE, FIELD_INT_MAX_VALUE}, // DISCRETE_RANGED_VALUE
- {FIELD_ELEMENT_WEIGHTS, FIELD_ELEMENT_COLORS} // TYPE_WEIGHTED_ELEMENTS
+ { // TYPE_WEIGHTED_ELEMENTS
+ FIELD_ELEMENT_WEIGHTS,
+ FIELD_ELEMENT_COLORS,
+ FIELD_ELEMENT_BACKGROUND_COLOR
+ }
};
// Used for validation. OPTIONAL_FIELDS[i] is an array containing all the fields which are
@@ -412,7 +400,8 @@
FIELD_COLOR_RAMP,
FIELD_COLOR_RAMP_INTERPOLATED,
FIELD_PERSISTENCE_POLICY,
- FIELD_DISPLAY_POLICY
+ FIELD_DISPLAY_POLICY,
+ FIELD_VALUE_TYPE
}, // RANGED_VALUE
{
FIELD_TAP_ACTION,
@@ -468,6 +457,7 @@
FIELD_SMALL_IMAGE_BURN_IN_PROTECTION,
FIELD_TAP_ACTION,
FIELD_VALUE,
+ FIELD_VALUE_TYPE,
FIELD_DATA_SOURCE,
FIELD_PERSISTENCE_POLICY,
FIELD_DISPLAY_POLICY
@@ -516,20 +506,6 @@
FIELD_DATA_SOURCE,
FIELD_PERSISTENCE_POLICY,
FIELD_DISPLAY_POLICY
- }, // DISCRETE_RANGED_VALUE
- {
- FIELD_SHORT_TEXT,
- FIELD_SHORT_TITLE,
- FIELD_ICON,
- FIELD_ICON_BURN_IN_PROTECTION,
- FIELD_SMALL_IMAGE,
- FIELD_SMALL_IMAGE_BURN_IN_PROTECTION,
- FIELD_IMAGE_STYLE,
- FIELD_TAP_ACTION,
- FIELD_CONTENT_DESCRIPTION,
- FIELD_DATA_SOURCE,
- FIELD_PERSISTENCE_POLICY,
- FIELD_DISPLAY_POLICY
} // TYPE_WEIGHTED_ELEMENTS
};
@@ -572,7 +548,7 @@
@RequiresApi(api = Build.VERSION_CODES.P)
private static class SerializedForm implements Serializable {
- private static final int VERSION_NUMBER = 17;
+ private static final int VERSION_NUMBER = 19;
@NonNull
ComplicationData mComplicationData;
@@ -630,6 +606,9 @@
if (isFieldValidForType(FIELD_VALUE, type)) {
oos.writeFloat(mComplicationData.getRangedValue());
}
+ if (isFieldValidForType(FIELD_VALUE_TYPE, type)) {
+ oos.writeInt(mComplicationData.getRangedValueType());
+ }
if (isFieldValidForType(FIELD_MIN_VALUE, type)) {
oos.writeFloat(mComplicationData.getRangedMinValue());
}
@@ -639,15 +618,6 @@
if (isFieldValidForType(FIELD_TARGET_VALUE, type)) {
oos.writeFloat(mComplicationData.getTargetValue());
}
- if (isFieldValidForType(FIELD_INT_VALUE, type)) {
- oos.writeInt(mComplicationData.getDiscreteRangedValue());
- }
- if (isFieldValidForType(FIELD_INT_MIN_VALUE, type)) {
- oos.writeInt(mComplicationData.getDiscreteRangedMinValue());
- }
- if (isFieldValidForType(FIELD_INT_MAX_VALUE, type)) {
- oos.writeInt(mComplicationData.getDiscreteRangedMaxValue());
- }
if (isFieldValidForType(FIELD_COLOR_RAMP, type)) {
int[] colors = mComplicationData.getColorRamp();
if (colors != null) {
@@ -693,6 +663,9 @@
oos.writeBoolean(false);
}
}
+ if (isFieldValidForType(FIELD_ELEMENT_BACKGROUND_COLOR, type)) {
+ oos.writeInt(mComplicationData.getElementBackgroundColor());
+ }
if (isFieldValidForType(FIELD_START_TIME, type)) {
oos.writeLong(mComplicationData.getStartDateTimeMillis());
}
@@ -835,6 +808,9 @@
if (isFieldValidForType(FIELD_VALUE, type)) {
fields.putFloat(FIELD_VALUE, ois.readFloat());
}
+ if (isFieldValidForType(FIELD_VALUE_TYPE, type)) {
+ fields.putInt(FIELD_VALUE_TYPE, ois.readInt());
+ }
if (isFieldValidForType(FIELD_MIN_VALUE, type)) {
fields.putFloat(FIELD_MIN_VALUE, ois.readFloat());
}
@@ -844,15 +820,6 @@
if (isFieldValidForType(FIELD_TARGET_VALUE, type)) {
fields.putFloat(FIELD_TARGET_VALUE, ois.readFloat());
}
- if (isFieldValidForType(FIELD_INT_VALUE, type)) {
- fields.putInt(FIELD_INT_VALUE, ois.readInt());
- }
- if (isFieldValidForType(FIELD_INT_MIN_VALUE, type)) {
- fields.putInt(FIELD_INT_MIN_VALUE, ois.readInt());
- }
- if (isFieldValidForType(FIELD_INT_MAX_VALUE, type)) {
- fields.putInt(FIELD_INT_MAX_VALUE, ois.readInt());
- }
if (isFieldValidForType(FIELD_COLOR_RAMP, type) && ois.readBoolean()) {
int numColors = ois.readInt();
int[] colors = new int[numColors];
@@ -880,6 +847,9 @@
}
fields.putIntArray(FIELD_ELEMENT_COLORS, colors);
}
+ if (isFieldValidForType(FIELD_ELEMENT_BACKGROUND_COLOR, type)) {
+ fields.putInt(FIELD_ELEMENT_BACKGROUND_COLOR, ois.readInt());
+ }
if (isFieldValidForType(FIELD_START_TIME, type)) {
fields.putLong(FIELD_START_TIME, ois.readLong());
}
@@ -1184,6 +1154,29 @@
}
/**
+ * Returns true if the ComplicationData contains a ranged max type. I.e. if
+ * {@link #getRangedValueType} can succeed.
+ */
+ public boolean hasRangedValueType() {
+ try {
+ return isFieldValidForType(FIELD_VALUE_TYPE, mType);
+ } catch (BadParcelableException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns the <i>value</i> field for this complication.
+ *
+ * <p>Valid only if the type of this complication data is {@link #TYPE_RANGED_VALUE}.
+ * Otherwise returns zero.
+ */
+ public int getRangedValueType() {
+ checkFieldValidForTypeWithoutThrowingException(FIELD_VALUE_TYPE, mType);
+ return mFields.getInt(FIELD_VALUE_TYPE);
+ }
+
+ /**
* Returns true if the ComplicationData contains a ranged max value. I.e. if
* {@link #getRangedMinValue} can succeed.
*/
@@ -1253,75 +1246,6 @@
}
/**
- * Returns true if the ComplicationData contains a discrete integer ranged max value. I.e. if
- * {@link #getDiscreteRangedValue} can succeed.
- */
- public boolean hasDiscreteRangedValue() {
- try {
- return isFieldValidForType(FIELD_INT_VALUE, mType);
- } catch (BadParcelableException e) {
- return false;
- }
- }
-
- /**
- * Returns the <i>discrete integer value</i> field for this complication.
- *
- * <p>Valid only if the type of this complication data is {@link #TYPE_DISCRETE_RANGED_VALUE}.
- * Otherwise returns zero.
- */
- public int getDiscreteRangedValue() {
- checkFieldValidForTypeWithoutThrowingException(FIELD_INT_VALUE, mType);
- return mFields.getInt(FIELD_INT_VALUE);
- }
-
- /**
- * Returns true if the ComplicationData contains a discrete integer ranged max value. I.e. if
- * {@link #getDiscreteRangedMinValue} can succeed.
- */
- public boolean hasDiscreteRangedMinValue() {
- try {
- return isFieldValidForType(FIELD_INT_MIN_VALUE, mType);
- } catch (BadParcelableException e) {
- return false;
- }
- }
-
- /**
- * Returns the <i>discrete integer min value</i> field for this complication.
- *
- * <p>Valid only if the type of this complication data is {@link #TYPE_DISCRETE_RANGED_VALUE}.
- * Otherwise returns zero.
- */
- public int getDiscreteRangedMinValue() {
- checkFieldValidForTypeWithoutThrowingException(FIELD_INT_MIN_VALUE, mType);
- return mFields.getInt(FIELD_INT_MIN_VALUE);
- }
-
- /**
- * Returns true if the ComplicationData contains a discrete integer ranged max value. I.e. if
- * {@link #getDiscreteRangedMaxValue} can succeed.
- */
- public boolean hasDiscreteRangedMaxValue() {
- try {
- return isFieldValidForType(FIELD_INT_MAX_VALUE, mType);
- } catch (BadParcelableException e) {
- return false;
- }
- }
-
- /**
- * Returns the <i>discrete integer max value</i> field for this complication.
- *
- * <p>Valid only if the type of this complication data is {@link #TYPE_DISCRETE_RANGED_VALUE}.
- * Otherwise returns zero.
- */
- public int getDiscreteRangedMaxValue() {
- checkFieldValidForTypeWithoutThrowingException(FIELD_INT_MAX_VALUE, mType);
- return mFields.getInt(FIELD_INT_MAX_VALUE);
- }
-
- /**
* Returns the colors for the progress bar.
*
* <p>Valid only if the type of this complication data is {@link #TYPE_RANGED_VALUE} or
@@ -1756,6 +1680,18 @@
}
/**
+ * Returns the background color to use between elements for this complication.
+ *
+ * <p>Valid only if the type of this complication data is {@link #TYPE_WEIGHTED_ELEMENTS}.
+ * Otherwise returns 0.
+ */
+ @ColorInt
+ public int getElementBackgroundColor() {
+ checkFieldValidForTypeWithoutThrowingException(FIELD_ELEMENT_BACKGROUND_COLOR, mType);
+ return mFields.getInt(FIELD_ELEMENT_BACKGROUND_COLOR);
+ }
+
+ /**
* Returns the placeholder ComplicationData if there is one or `null`.
*/
@Nullable
@@ -2032,6 +1968,16 @@
}
/**
+ * Sets the <i>value type</i> field which provides meta data about the value. This is
+ * optional for the {@link #TYPE_RANGED_VALUE} type.
+ */
+ @NonNull
+ public Builder setRangedValueType(int valueType) {
+ putIntField(FIELD_VALUE_TYPE, valueType);
+ return this;
+ }
+
+ /**
* Sets the <i>min value</i> field. This is required for the {@link #TYPE_RANGED_VALUE}
* type, and is not valid for any other type. A {@link #TYPE_RANGED_VALUE} complication
* visually presents a single value, which is usually a percentage. E.g. you have
@@ -2064,57 +2010,6 @@
}
/**
- * Sets the <i>discrete integer value</i> field. This is required for the
- * {@link #TYPE_DISCRETE_RANGED_VALUE} type, and is not valid for any other type. A
- * {@link #TYPE_DISCRETE_RANGED_VALUE} complication
- * visually presents a single value. E.g. you have drunk 3 out of today's target of 6 cups
- * of water.
- *
- * <p>Returns this Builder to allow chaining.
- *
- * @throws IllegalStateException if this field is not valid for the complication type
- */
- @NonNull
- public Builder setDiscreteRangedValue(int value) {
- putIntField(FIELD_INT_VALUE, value);
- return this;
- }
-
- /**
- * Sets the <i>min discrete integer value</i> field. This is required for the
- * {@link #TYPE_DISCRETE_RANGED_VALUE} type, and is not valid for any other type. A
- * {@link #TYPE_DISCRETE_RANGED_VALUE} complication visually presents a single value,
- * which is usually a percentage. E.g. you have completed 70% of today's target of 10000
- * steps.
- *
- * <p>Returns this Builder to allow chaining.
- *
- * @throws IllegalStateException if this field is not valid for the complication type
- */
- @NonNull
- public Builder setDiscreteRangedMinValue(int minValue) {
- putIntField(FIELD_INT_MIN_VALUE, minValue);
- return this;
- }
-
- /**
- * Sets the <i>max discrete integer value</i> field. This is required for the
- * {@link #TYPE_DISCRETE_RANGED_VALUE} type, and is not valid for any other type. A
- * {@link #TYPE_DISCRETE_RANGED_VALUE} complication visually presents a single value,
- * which is usually a percentage. E.g. you have completed 70% of today's target of 10000
- * steps.
- *
- * <p>Returns this Builder to allow chaining.
- *
- * @throws IllegalStateException if this field is not valid for the complication type
- */
- @NonNull
- public Builder setDiscreteRangedMaxValue(int maxValue) {
- putIntField(FIELD_INT_MAX_VALUE, maxValue);
- return this;
- }
-
- /**
* Sets the <i>targetValue</i> field. This is required for the {@link #TYPE_GOAL_PROGRESS}
* type, and is not valid for any other type. A {@link #TYPE_GOAL_PROGRESS} complication
* visually presents a single value, which is usually a percentage. E.g. you
@@ -2516,6 +2411,17 @@
}
/**
+ * Sets the background color to use between elements for this complication.
+ *
+ * <p>Returns this Builder to allow chaining.
+ */
+ @NonNull
+ public Builder setElementBackgroundColor(@ColorInt int elementBackgroundColor) {
+ putOrRemoveField(FIELD_ELEMENT_BACKGROUND_COLOR, elementBackgroundColor);
+ return this;
+ }
+
+ /**
* Constructs and returns {@link ComplicationData} with the provided fields. All required
* fields must be populated before this method is called.
*
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
index fb451c7..bdf8831 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
@@ -18,6 +18,7 @@
import android.app.PendingIntent
import android.content.ComponentName
+import android.graphics.Color
import android.graphics.drawable.Icon
import android.os.Build
import android.util.Log
@@ -276,7 +277,6 @@
is SmallImageComplicationData -> placeholder.contentDescription
is PhotoImageComplicationData -> placeholder.contentDescription
is GoalProgressComplicationData -> placeholder.contentDescription
- is DiscreteRangedValueComplicationData -> placeholder.contentDescription
is WeightedElementsComplicationData -> placeholder.contentDescription
else -> null
}
@@ -904,6 +904,33 @@
}
/**
+ * Describes the semantic meaning of a [RangedValueComplicationData] if known. The complication
+ * renderer may wish to visually differentiate between the different types, for example rendering a
+ * dot on a line/arc to show the value for a [SCORE].
+ */
+@ComplicationExperimental
+object RangedValueTypes {
+ /** The ranged value's semantic hasn't been defined, but it's most commonly a percentage. */
+ const val UNDEFINED = 0
+
+ /**
+ * The randed value represents a score for something unrelated to the user, e.g. the air quality
+ * index.
+ */
+ const val SCORE = 1
+}
+
+/** @hide */
+@OptIn(ComplicationExperimental::class)
+@IntDef(
+ value = [
+ RangedValueTypes.UNDEFINED,
+ RangedValueTypes.SCORE
+ ]
+)
+public annotation class RangedValueType
+
+/**
* Type used for complications including a numerical value within a range, such as a percentage.
* The value may be accompanied by an icon and/or short text and title.
*
@@ -922,7 +949,7 @@
* @property value The [Float] value of this complication which is >= [min] and <= [max] or equal to
* [PLACEHOLDER]. If it's equal to [PLACEHOLDER] the renderer must treat it as a placeholder rather
* than rendering normally, its suggested to be drawn as a grey arc with a percentage value selected
- * by the renderer.
+ * by the renderer. The semantic meaning of value is described by [valueType].
* @property min The minimum [Float] value for this complication.
* @property max The maximum [Float] value for this complication.
* @property monochromaticImage A simple [MonochromaticImage] image that can be tinted by the watch
@@ -966,6 +993,7 @@
cachedWireComplicationData: WireComplicationData?,
dataSource: ComponentName?,
colorRamp: ColorRamp?,
+ @RangedValueType valueType: Int,
@ComplicationPersistencePolicy persistencePolicy: Int,
@ComplicationDisplayPolicy displayPolicy: Int
) : ComplicationData(
@@ -983,6 +1011,13 @@
@ComplicationExperimental
val colorRamp: ColorRamp? = colorRamp
+ /** The semantic meaning of [value], see [RangedValueType] for more details. */
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ComplicationExperimental
+ @ComplicationExperimental
+ @RangedValueType
+ val valueType: Int = valueType
+
/**
* Builder for [RangedValueComplicationData].
*
@@ -990,7 +1025,7 @@
* least one of [monochromaticImage], [smallImage], [text] or [title].
*
* @param value The value of the ranged complication which should be in the range
- * [[min]] .. [[max]]
+ * [[min]] .. [[max]]. The semantic meaning of value can be specified via [setValueType].
* @param min The minimum value
* @param max The maximum value. This must be less than [Float.MAX_VALUE].
* @param contentDescription Localized description for use by screen readers
@@ -1010,6 +1045,9 @@
private var text: ComplicationText? = null
@OptIn(ComplicationExperimental::class)
private var colorRamp: ColorRamp? = null
+ @OptIn(ComplicationExperimental::class)
+ @RangedValueType
+ private var valueType: Int = RangedValueTypes.UNDEFINED
init {
require(max != Float.MAX_VALUE) {
@@ -1057,6 +1095,15 @@
this.colorRamp = colorRamp
}
+ /**
+ * Sets the semantic meaning of [value], see [@RangedValueTypes] for detail.s Defaults to
+ * [RangedValueTypes.UNDEFINED] if not set.
+ */
+ @ComplicationExperimental
+ public fun setValueType(@RangedValueType valueType: Int): Builder = apply {
+ this.valueType = valueType
+ }
+
/** Builds the [RangedValueComplicationData]. */
@OptIn(ComplicationExperimental::class)
public override fun build(): RangedValueComplicationData {
@@ -1079,6 +1126,7 @@
cachedWireComplicationData,
dataSource,
colorRamp,
+ valueType,
persistencePolicy,
displayPolicy
)
@@ -1118,6 +1166,7 @@
builder.setColorRamp(it.colors)
builder.setColorRampIsSmoothShaded(it.interpolated)
}
+ builder.setRangedValueType(valueType)
}
@OptIn(ComplicationExperimental::class)
@@ -1128,6 +1177,7 @@
other as RangedValueComplicationData
if (value != other.value) return false
+ if (valueType != other.valueType) return false
if (min != other.min) return false
if (max != other.max) return false
if (monochromaticImage != other.monochromaticImage) return false
@@ -1149,6 +1199,7 @@
@OptIn(ComplicationExperimental::class)
override fun hashCode(): Int {
var result = value.hashCode()
+ result = 31 * result + valueType
result = 31 * result + min.hashCode()
result = 31 * result + max.hashCode()
result = 31 * result + (monochromaticImage?.hashCode() ?: 0)
@@ -1173,9 +1224,9 @@
} else {
value.toString()
}
- return "RangedValueComplicationData(value=$valueString, min=$min, max=$max, " +
- "monochromaticImage=$monochromaticImage, smallImage=$smallImage, title=$title, " +
- "text=$text, contentDescription=$contentDescription), " +
+ return "RangedValueComplicationData(value=$valueString, valueType=$valueType, min=$min, " +
+ "max=$max, monochromaticImage=$monochromaticImage, smallImage=$smallImage, " +
+ "title=$title, text=$text, contentDescription=$contentDescription), " +
"tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
"tapAction=$tapAction, validTimeRange=$validTimeRange, dataSource=$dataSource, " +
"colorRamp=$colorRamp, persistencePolicy=$persistencePolicy, " +
@@ -1233,6 +1284,10 @@
* recommended to choose the [smallImage]. It's best practice for a ComplicationDataSource to
* specify both a [monochromaticImage] and a [smallImage].
*
+ * If you want to represent a score for something that's not based on the user (e.g. air quality
+ * index) then you should use a [RangedValueComplicationData] and pass [RangedValueTypes.SCORE] into
+ * [RangedValueComplicationData.Builder.setValueType].
+ *
* @property value The [Float] value of this complication which is >= 0f, this value may be larger
* than [targetValue]. If it's equal to [PLACEHOLDER] the renderer must treat it as a placeholder
* rather than rendering normally, its suggested to be drawn as a grey arc with a percentage value
@@ -1389,7 +1444,7 @@
dataSource,
colorRamp,
persistencePolicy,
- displayPolicy
+ displayPolicy,
)
}
}
@@ -1540,8 +1595,11 @@
*
* @property elements The breakdown of the subject into various [Element]s. E.g. the proportion of
* calories consumed which were carbohydrates, fats etc... If this is equal to [PLACEHOLDER] then
- * the renderer must display this in a visiually distinct way to suggest to the user that it's
- * placeholder data. E.g. each rendered is rendered in light grey.
+ * the renderer must display this in a visually distinct way to suggest to the user that it's
+ * placeholder data. E.g. each rendered is rendered in light grey. The maximum valid size of this
+ * list is [MAX_NUM_ELEMENTS].
+ * @property elementBackgroundColor If elements are draw as segments then this is the background
+ * color to use in between them.
* @property monochromaticImage A simple [MonochromaticImage] image that can be tinted by the watch
* face. If the monochromaticImage is equal to [MonochromaticImage.PLACEHOLDER] the renderer must
* treat it as a placeholder rather than rendering normally, its suggested it should be rendered as
@@ -1572,6 +1630,7 @@
public class WeightedElementsComplicationData
internal constructor(
public val elements: List<Element>,
+ @ColorInt public val elementBackgroundColor: Int,
public val monochromaticImage: MonochromaticImage?,
public val smallImage: SmallImage?,
public val title: ComplicationText?,
@@ -1642,7 +1701,8 @@
*
* @param elements The breakdown of the subject into various [Element]s. E.g. the proportion of
* calories consumed which were carbohydrates, fats etc... The [tapAction] must take the user to
- * an experience where the color key becomes obvious.
+ * an experience where the color key becomes obvious. The maximum valid size of this list is
+ * [MAX_NUM_ELEMENTS].
* @param contentDescription Localized description for use by screen readers
*/
@OptIn(ComplicationExperimental::class)
@@ -1650,6 +1710,7 @@
private val elements: List<Element>,
private var contentDescription: ComplicationText
) : BaseBuilder<Builder, WeightedElementsComplicationData>() {
+ @ColorInt private var elementBackgroundColor: Int = Color.TRANSPARENT
private var tapAction: PendingIntent? = null
private var validTimeRange: TimeRange? = null
private var monochromaticImage: MonochromaticImage? = null
@@ -1657,6 +1718,22 @@
private var title: ComplicationText? = null
private var text: ComplicationText? = null
+ init {
+ require(elements.size < MAX_NUM_ELEMENTS) {
+ "Found ${elements.size} elements but the maximum is $MAX_NUM_ELEMENTS"
+ }
+ }
+
+ /**
+ * Sets the background color to use between the [elements] if they are drawn segmented.
+ * Defaults to [Color.TRANSPARENT] if not set.
+ */
+ public fun setElementBackgroundColor(
+ @ColorInt elementBackgroundColor: Int
+ ): Builder = apply {
+ this.elementBackgroundColor = elementBackgroundColor
+ }
+
/** Sets optional pending intent to be invoked when the complication is tapped. */
public fun setTapAction(tapAction: PendingIntent?): Builder = apply {
this.tapAction = tapAction
@@ -1698,6 +1775,7 @@
}
return WeightedElementsComplicationData(
elements,
+ elementBackgroundColor,
monochromaticImage,
smallImage,
title,
@@ -1728,6 +1806,7 @@
override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
builder.setElementWeights(elements.map { it.weight }.toFloatArray())
builder.setElementColors(elements.map { it.color }.toIntArray())
+ builder.setElementBackgroundColor(elementBackgroundColor)
monochromaticImage?.addToWireComplicationData(builder)
smallImage?.addToWireComplicationData(builder)
builder.setShortText(text?.toWireComplicationText())
@@ -1761,6 +1840,7 @@
elements.joinToString()
}
return "WeightedElementsComplicationData(elements=$elementsString, " +
+ "elementBackgroundColor=$elementBackgroundColor, " +
"monochromaticImage=$monochromaticImage, smallImage=$smallImage, title=$title, " +
"text=$text, contentDescription=$contentDescription), " +
"tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
@@ -1779,6 +1859,7 @@
other as WeightedElementsComplicationData
if (elements != other.elements) return false
+ if (elementBackgroundColor != other.elementBackgroundColor) return false
if (monochromaticImage != other.monochromaticImage) return false
if (smallImage != other.smallImage) return false
if (title != other.title) return false
@@ -1792,6 +1873,7 @@
override fun hashCode(): Int {
var result = elements.hashCode()
+ result = 31 * result + elementBackgroundColor
result = 31 * result + (monochromaticImage?.hashCode() ?: 0)
result = 31 * result + (smallImage?.hashCode() ?: 0)
result = 31 * result + (title?.hashCode() ?: 0)
@@ -1804,7 +1886,6 @@
return result
}
- /** @hide */
public companion object {
/** The [ComplicationType] corresponding to objects of this type. */
@OptIn(ComplicationExperimental::class)
@@ -1821,294 +1902,12 @@
*/
@JvmField
public val PLACEHOLDER = emptyList<Element>()
- }
-}
-
-/**
- * Type used for complications including a discrete integer value within a range. E.g. 3 out of 6
- * daily cups of water drunk. The value may be accompanied by an icon and/or short text and title.
- *
- * The [value], [min], and [max] fields are required for this type and the value within the
- * range is expected to always be displayed.
- *
- * The icon, title, and text fields are optional and the watch face may choose which of these
- * fields to display, if any.
- *
- * Unlike [RangedValueComplicationData], DiscreteRangedValueComplicationData doesn't specify a color
- * ramp, this is because the ranged value is expected to be rendered using solid colored segments
- * with watch face selected colors.
- *
- * If a [monochromaticImage] and a [smallImage] are both specified then only one should be
- * displayed. If the complication is drawn with a single color it's recommended to choose
- * [monochromaticImage] and apply a tint. If the complication is rendered with multiple colors it's
- * recommended to choose the [smallImage]. It's best practice for a ComplicationDataSource to
- * specify both a [monochromaticImage] and a [smallImage].
- *
- * @property value The [Int] value of this complication which is >= [min] and <= [max] or equal to
- * [PLACEHOLDER]. If it's equal to [PLACEHOLDER] the renderer must treat it as a placeholder rather
- * than rendering normally, its suggested to be drawn as a grey arc with a percentage value selected
- * by the renderer.
- * @property min The minimum [Int] value for this complication.
- * @property max The maximum [Int] value for this complication.
- * @property monochromaticImage A simple [MonochromaticImage] image that can be tinted by the watch
- * face. If the monochromaticImage is equal to [MonochromaticImage.PLACEHOLDER] the renderer must
- * treat it as a placeholder rather than rendering normally, its suggested it should be rendered as
- * a light grey box.
- * @property title The optional title [ComplicationText]. The length of the title, including
- * any time-dependent values at any valid time, is expected to not exceed seven characters. When
- * using this text, the watch face should be able to display any string of up to seven characters
- * (reducing the text size appropriately if the string is very wide). Although not expected, it is
- * possible that strings of more than seven characters might be seen, in which case they may be
- * truncated. If the title is equal to [ComplicationText.PLACEHOLDER] the renderer must treat it as
- * a placeholder rather than rendering normally, its suggested it should be rendered as a light grey
- * box.
- * @property text The body [ComplicationText] of the complication. The length of the text, including
- * any time-dependent values at any valid time, is expected to not exceed seven characters. When
- * using this text, the watch face should be able to display any string of up to seven characters
- * (reducing the text size appropriately if the string is very wide). Although not expected, it is
- * possible that strings of more than seven characters might be seen, in which case they may be
- * truncated. If the text is equal to [ComplicationText.PLACEHOLDER] the renderer must treat it as a
- * placeholder rather than rendering normally, its suggested it should be rendered as a light grey
- * box.
- * @property contentDescription The content description field for accessibility.
- */
-@ComplicationExperimental
-public class DiscreteRangedValueComplicationData
-internal constructor(
- public val value: Int,
- public val min: Int,
- public val max: Int,
- public val monochromaticImage: MonochromaticImage?,
- public val smallImage: SmallImage?,
- public val title: ComplicationText?,
- public val text: ComplicationText?,
- public val contentDescription: ComplicationText?,
- tapAction: PendingIntent?,
- validTimeRange: TimeRange?,
- cachedWireComplicationData: WireComplicationData?,
- dataSource: ComponentName?,
- @ComplicationPersistencePolicy persistencePolicy: Int,
- @ComplicationDisplayPolicy displayPolicy: Int
-) : ComplicationData(
- TYPE,
- tapAction = tapAction,
- cachedWireComplicationData = cachedWireComplicationData,
- validTimeRange = validTimeRange ?: TimeRange.ALWAYS,
- dataSource = dataSource,
- persistencePolicy = persistencePolicy,
- displayPolicy = displayPolicy
-) {
- /**
- * Builder for [DiscreteRangedValueComplicationData].
- *
- * You must at a minimum set the [value], [min], [max] and [contentDescription] fields and at
- * least one of [monochromaticImage], [smallImage], [text] or [title].
- *
- * @param value The value of the ranged complication which should be in the range
- * [[min]] .. [[max]]
- * @param min The minimum value
- * @param max The maximum value. This must be less than [Float.MAX_VALUE].
- * @param contentDescription Localized description for use by screen readers
- */
- @OptIn(ComplicationExperimental::class)
- public class Builder(
- private val value: Int,
- private val min: Int,
- private val max: Int,
- private var contentDescription: ComplicationText
- ) : BaseBuilder<Builder, DiscreteRangedValueComplicationData>() {
- private var tapAction: PendingIntent? = null
- private var validTimeRange: TimeRange? = null
- private var monochromaticImage: MonochromaticImage? = null
- private var smallImage: SmallImage? = null
- private var title: ComplicationText? = null
- private var text: ComplicationText? = null
-
- init {
- require(max != Int.MAX_VALUE) {
- "Int.MAX_VALUE is reserved and can't be used for max"
- }
- }
-
- /** Sets optional pending intent to be invoked when the complication is tapped. */
- public fun setTapAction(tapAction: PendingIntent?): Builder = apply {
- this.tapAction = tapAction
- }
-
- /** Sets optional time range during which the complication has to be shown. */
- @Suppress("MissingGetterMatchingBuilder") // b/174052810
- public fun setValidTimeRange(validTimeRange: TimeRange?): Builder = apply {
- this.validTimeRange = validTimeRange
- }
-
- /** Sets optional icon associated with the complication data. */
- public fun setMonochromaticImage(monochromaticImage: MonochromaticImage?): Builder = apply {
- this.monochromaticImage = monochromaticImage
- }
-
- /** Sets optional image associated with the complication data. */
- public fun setSmallImage(smallImage: SmallImage?): Builder = apply {
- this.smallImage = smallImage
- }
-
- /** Sets optional title associated with the complication data. */
- public fun setTitle(title: ComplicationText?): Builder = apply {
- this.title = title
- }
-
- /** Sets optional text associated with the complication data. */
- public fun setText(text: ComplicationText?): Builder = apply {
- this.text = text
- }
-
- /** Builds the [DiscreteRangedValueComplicationData]. */
- @OptIn(ComplicationExperimental::class)
- public override fun build(): DiscreteRangedValueComplicationData {
- require(
- monochromaticImage != null || smallImage != null || text != null || title != null
- ) {
- "At least one of monochromaticImage, smallImage, text or title must be set"
- }
- return DiscreteRangedValueComplicationData(
- value,
- min,
- max,
- monochromaticImage,
- smallImage,
- title,
- text,
- contentDescription,
- tapAction,
- validTimeRange,
- cachedWireComplicationData,
- dataSource,
- persistencePolicy,
- displayPolicy
- )
- }
- }
-
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public override fun asWireComplicationData(): WireComplicationData {
- cachedWireComplicationData?.let {
- return it
- }
- return createWireComplicationDataBuilder().apply {
- fillWireComplicationDataBuilder(this)
- }.build().also { cachedWireComplicationData = it }
- }
-
- override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
- builder.setDiscreteRangedValue(value)
- builder.setDiscreteRangedMinValue(min)
- builder.setDiscreteRangedMaxValue(max)
- monochromaticImage?.addToWireComplicationData(builder)
- smallImage?.addToWireComplicationData(builder)
- builder.setShortText(text?.toWireComplicationText())
- builder.setShortTitle(title?.toWireComplicationText())
- builder.setTapAction(tapAction)
- builder.setContentDescription(
- when (contentDescription) {
- ComplicationText.EMPTY -> null
- else -> contentDescription?.toWireComplicationText()
- }
- )
- setValidTimeRange(validTimeRange, builder)
- builder.setTapActionLostDueToSerialization(tapActionLostDueToSerialization)
- }
-
- @OptIn(ComplicationExperimental::class)
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as DiscreteRangedValueComplicationData
-
- if (value != other.value) return false
- if (min != other.min) return false
- if (max != other.max) return false
- if (monochromaticImage != other.monochromaticImage) return false
- if (smallImage != other.smallImage) return false
- if (title != other.title) return false
- if (text != other.text) return false
- if (contentDescription != other.contentDescription) return false
- if (tapActionLostDueToSerialization != other.tapActionLostDueToSerialization) return false
- if (tapAction != other.tapAction) return false
- if (validTimeRange != other.validTimeRange) return false
- if (dataSource != other.dataSource) return false
- if (persistencePolicy != other.persistencePolicy) return false
- if (displayPolicy != other.displayPolicy) return false
-
- return true
- }
-
- @OptIn(ComplicationExperimental::class)
- override fun hashCode(): Int {
- var result = value.hashCode()
- result = 31 * result + min.hashCode()
- result = 31 * result + max.hashCode()
- result = 31 * result + (monochromaticImage?.hashCode() ?: 0)
- result = 31 * result + (smallImage?.hashCode() ?: 0)
- result = 31 * result + (title?.hashCode() ?: 0)
- result = 31 * result + (text?.hashCode() ?: 0)
- result = 31 * result + (contentDescription?.hashCode() ?: 0)
- result = 31 * result + tapActionLostDueToSerialization.hashCode()
- result = 31 * result + (tapAction?.hashCode() ?: 0)
- result = 31 * result + validTimeRange.hashCode()
- result = 31 * result + dataSource.hashCode()
- result = 31 * result + persistencePolicy.hashCode()
- result = 31 * result + persistencePolicy.hashCode()
- result = 31 * result + displayPolicy.hashCode()
-
- return result
- }
-
- override fun toString(): String {
- val valueString = if (WireComplicationData.shouldRedact()) {
- "REDACTED"
- } else {
- value.toString()
- }
- return "DiscreteRangedValueComplicationData(value=$valueString, min=$min, max=$max, " +
- "monochromaticImage=$monochromaticImage, smallImage=$smallImage, title=$title, " +
- "text=$text, contentDescription=$contentDescription), " +
- "tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
- "tapAction=$tapAction, validTimeRange=$validTimeRange, dataSource=$dataSource, " +
- "persistencePolicy=$persistencePolicy, displayPolicy=$displayPolicy)"
- }
-
- override fun hasPlaceholderFields() = value == PLACEHOLDER || text?.isPlaceholder() == true ||
- title?.isPlaceholder() == true || monochromaticImage?.isPlaceholder() == true ||
- smallImage?.isPlaceholder() == true
-
- override fun getNextChangeInstant(afterInstant: Instant): Instant {
- val titleChangeInstant = title?.getNextChangeTime(afterInstant) ?: Instant.MAX
- val textChangeInstant = text?.getNextChangeTime(afterInstant) ?: Instant.MAX
- return if (textChangeInstant.isBefore(titleChangeInstant)) {
- textChangeInstant
- } else {
- titleChangeInstant
- }
- }
-
- /** @hide */
- public companion object {
- /** The [ComplicationType] corresponding to objects of this type. */
- @OptIn(ComplicationExperimental::class)
- @JvmField
- public val TYPE: ComplicationType = ComplicationType.DISCRETE_RANGED_VALUE
/**
- * Used to signal the range should be rendered as a placeholder. It's suggested that a
- * placeholder ranged value be drawn as a grey arc with a percentage value selected by the
- * renderer.
- *
- * Note a placeholder may only be used in the context of
- * [NoDataComplicationData.placeholder].
- */
- @JvmField
- public val PLACEHOLDER = Int.MAX_VALUE
+ * The maximum size for [elements]. Complications are small and if we have a very large
+ * number of elements we likely won't be able to render them properly because the individual
+ * elements will be too small on screen. */
+ public const val MAX_NUM_ELEMENTS = 20
}
}
@@ -3211,6 +3010,7 @@
}
setPersistencePolicy(persistencePolicyCopy)
setDisplayPolicy(displayPolicyCopy)
+ setValueType(rangedValueType)
}.build()
MonochromaticImageComplicationData.TYPE.toWireComplicationType() ->
@@ -3301,24 +3101,6 @@
setDisplayPolicy(displayPolicyCopy)
}.build()
- DiscreteRangedValueComplicationData.TYPE.toWireComplicationType() ->
- DiscreteRangedValueComplicationData.Builder(
- value = discreteRangedValue,
- min = discreteRangedMinValue,
- max = discreteRangedMaxValue,
- contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
- ).apply {
- setTapAction(tapAction)
- setValidTimeRange(parseTimeRange())
- setMonochromaticImage(parseIconPlaceholderAware())
- setSmallImage(parseSmallImagePlaceholderAware())
- setTitle(shortTitle?.toApiComplicationTextPlaceholderAware())
- setText(shortText?.toApiComplicationTextPlaceholderAware())
- setDataSource(dataSourceCopy)
- setPersistencePolicy(persistencePolicyCopy)
- setDisplayPolicy(displayPolicyCopy)
- }.build()
-
WeightedElementsComplicationData.TYPE.toWireComplicationType() ->
WeightedElementsComplicationData.Builder(
elements = if (elementWeights!!.isEmpty()) {
@@ -3335,6 +3117,7 @@
},
contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
).apply {
+ setElementBackgroundColor(elementBackgroundColor)
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
setMonochromaticImage(parseIconPlaceholderAware())
@@ -3436,6 +3219,7 @@
setDataSource(dataSourceCopy)
setPersistencePolicy(persistencePolicyCopy)
setDisplayPolicy(displayPolicyCopy)
+ setValueType(rangedValueType)
}.build()
MonochromaticImageComplicationData.TYPE.toWireComplicationType() ->
@@ -3544,26 +3328,6 @@
setDisplayPolicy(displayPolicyCopy)
}.build()
- DiscreteRangedValueComplicationData.TYPE.toWireComplicationType() ->
- DiscreteRangedValueComplicationData.Builder(
- value = discreteRangedValue,
- min = discreteRangedMinValue,
- max = discreteRangedMaxValue,
- contentDescription = contentDescription?.toApiComplicationText()
- ?: ComplicationText.EMPTY
- ).apply {
- setTapAction(tapAction)
- setValidTimeRange(parseTimeRange())
- setMonochromaticImage(parseIcon())
- setSmallImage(parseSmallImage())
- setTitle(shortTitle?.toApiComplicationText())
- setText(shortText?.toApiComplicationText())
- setCachedWireComplicationData(wireComplicationData)
- setDataSource(dataSourceCopy)
- setPersistencePolicy(persistencePolicyCopy)
- setDisplayPolicy(displayPolicyCopy)
- }.build()
-
WeightedElementsComplicationData.TYPE.toWireComplicationType() -> {
val elementWeights = this.elementWeights!!
val elementColors = this.elementColors!!
@@ -3576,6 +3340,7 @@
}.toList(),
contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
).apply {
+ setElementBackgroundColor(elementBackgroundColor)
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
setMonochromaticImage(parseIcon())
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
index 905b056a..e64be10 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
@@ -42,8 +42,6 @@
@ComplicationExperimental
GOAL_PROGRESS(WireComplicationData.TYPE_GOAL_PROGRESS),
@ComplicationExperimental
- DISCRETE_RANGED_VALUE(WireComplicationData.TYPE_DISCRETE_RANGED_VALUE),
- @ComplicationExperimental
WEIGHTED_ELEMENTS(WireComplicationData.TYPE_WEIGHTED_ELEMENTS),
@ComplicationExperimental
@@ -86,7 +84,6 @@
NO_PERMISSION.wireType -> NO_PERMISSION
PROTO_LAYOUT.wireType -> PROTO_LAYOUT
GOAL_PROGRESS.wireType -> GOAL_PROGRESS
- DISCRETE_RANGED_VALUE.wireType -> DISCRETE_RANGED_VALUE
WEIGHTED_ELEMENTS.wireType -> WEIGHTED_ELEMENTS
LIST.wireType -> LIST
else -> EMPTY
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
index 25c7f64..29fc589 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
@@ -412,6 +412,7 @@
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(95f)
+ .setRangedValueType(RangedValueTypes.UNDEFINED)
.setRangedMinValue(0f)
.setRangedMaxValue(100f)
.setShortTitle(WireComplicationText.plainText("battery"))
@@ -452,7 +453,7 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "RangedValueComplicationData(value=95.0, min=0.0, max=100.0, " +
+ "RangedValueComplicationData(value=95.0, valueType=0, min=0.0, max=100.0, " +
"monochromaticImage=null, smallImage=null, title=ComplicationText{" +
"mSurroundingText=battery, mTimeDependentText=null}, text=null, " +
"contentDescription=ComplicationText{mSurroundingText=content description, " +
@@ -477,6 +478,7 @@
.setMonochromaticImage(monochromaticImage)
.setSmallImage(smallImage)
.setDataSource(dataSourceA)
+ .setValueType(RangedValueTypes.SCORE)
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
@@ -492,6 +494,7 @@
.setDataSource(dataSourceA)
.setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
.setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
+ .setRangedValueType(RangedValueTypes.SCORE)
.build()
)
testRoundTripConversions(data)
@@ -517,6 +520,7 @@
.setMonochromaticImage(monochromaticImage2)
.setSmallImage(smallImage2)
.setDataSource(dataSourceA)
+ .setValueType(RangedValueTypes.SCORE)
.build()
val data3 = RangedValueComplicationData.Builder(
@@ -525,6 +529,7 @@
)
.setTitle("battery2".complicationText)
.setDataSource(dataSourceB)
+ .setValueType(RangedValueTypes.SCORE)
.build()
assertThat(data).isEqualTo(data2)
@@ -532,7 +537,7 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "RangedValueComplicationData(value=95.0, min=0.0, max=100.0, " +
+ "RangedValueComplicationData(value=95.0, valueType=1, min=0.0, max=100.0, " +
"monochromaticImage=MonochromaticImage(image=Icon(typ=URI uri=someuri), " +
"ambientImage=null), smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), " +
"type=PHOTO, ambientImage=null), title=ComplicationText{mSurroundingText=battery," +
@@ -619,7 +624,7 @@
@RequiresApi(Build.VERSION_CODES.P)
@OptIn(ComplicationExperimental::class)
@Test
- public fun goalProgressComplicationData_with_ColorRamp_andiImages() {
+ public fun goalProgressComplicationData_with_ColorRamp_and_Images() {
val data = GoalProgressComplicationData.Builder(
value = 1200f, targetValue = 10000f,
contentDescription = "content description".complicationText
@@ -703,153 +708,6 @@
@OptIn(ComplicationExperimental::class)
@Test
- public fun discreteRangedValueComplicationData() {
- val data = DiscreteRangedValueComplicationData.Builder(
- value = 95, min = 0, max = 100,
- contentDescription = "content description".complicationText
- )
- .setTitle("battery".complicationText)
- .setDataSource(dataSourceA)
- .build()
- ParcelableSubject.assertThat(data.asWireComplicationData())
- .hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_DISCRETE_RANGED_VALUE)
- .setDiscreteRangedValue(95)
- .setDiscreteRangedMinValue(0)
- .setDiscreteRangedMaxValue(100)
- .setShortTitle(WireComplicationText.plainText("battery"))
- .setContentDescription(WireComplicationText.plainText("content description"))
- .setDataSource(dataSourceA)
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
- )
- testRoundTripConversions(data)
- val deserialized = serializeAndDeserialize(data) as DiscreteRangedValueComplicationData
- assertThat(deserialized.max).isEqualTo(100)
- assertThat(deserialized.min).isEqualTo(0)
- assertThat(deserialized.value).isEqualTo(95)
- assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
- .isEqualTo("content description")
- assertThat(deserialized.title!!.getTextAt(resources, Instant.EPOCH))
- .isEqualTo("battery")
-
- val data2 = DiscreteRangedValueComplicationData.Builder(
- value = 95, min = 0, max = 100,
- contentDescription = "content description".complicationText
- )
- .setTitle("battery".complicationText)
- .setDataSource(dataSourceA)
- .build()
-
- val data3 = DiscreteRangedValueComplicationData.Builder(
- value = 95, min = 0, max = 100,
- contentDescription = "content description2".complicationText
- )
- .setTitle("battery2".complicationText)
- .setDataSource(dataSourceB)
- .build()
-
- assertThat(data).isEqualTo(data2)
- assertThat(data).isNotEqualTo(data3)
- assertThat(data.hashCode()).isEqualTo(data2.hashCode())
- assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
- assertThat(data.toString()).isEqualTo(
- "DiscreteRangedValueComplicationData(value=95, min=0, max=100, " +
- "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
- "mSurroundingText=battery, mTimeDependentText=null}, text=null, " +
- "contentDescription=ComplicationText{mSurroundingText=content description, " +
- "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
- "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
- "+1000000000-12-31T23:59:59.999999999Z), " +
- "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
- )
- }
-
- @RequiresApi(Build.VERSION_CODES.P)
- @OptIn(ComplicationExperimental::class)
- @Test
- public fun discreteRangedValueComplicationData_withImages() {
- val data = DiscreteRangedValueComplicationData.Builder(
- value = 95, min = 0, max = 100,
- contentDescription = "content description".complicationText
- )
- .setTitle("battery".complicationText)
- .setMonochromaticImage(monochromaticImage)
- .setSmallImage(smallImage)
- .setDataSource(dataSourceA)
- .build()
- ParcelableSubject.assertThat(data.asWireComplicationData())
- .hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_DISCRETE_RANGED_VALUE)
- .setDiscreteRangedValue(95)
- .setDiscreteRangedMinValue(0)
- .setDiscreteRangedMaxValue(100)
- .setIcon(monochromaticImageIcon)
- .setSmallImage(smallImageIcon)
- .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
- .setShortTitle(WireComplicationText.plainText("battery"))
- .setContentDescription(WireComplicationText.plainText("content description"))
- .setDataSource(dataSourceA)
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
- )
- testRoundTripConversions(data)
- val deserialized = serializeAndDeserialize(data) as DiscreteRangedValueComplicationData
- assertThat(deserialized.max).isEqualTo(100)
- assertThat(deserialized.min).isEqualTo(0)
- assertThat(deserialized.value).isEqualTo(95)
- assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
- .isEqualTo("content description")
- assertThat(deserialized.title!!.getTextAt(resources, Instant.EPOCH))
- .isEqualTo("battery")
- assertThat(deserialized.monochromaticImage!!.image.uri.toString())
- .isEqualTo("someuri")
- assertThat(deserialized.smallImage!!.image.uri.toString())
- .isEqualTo("someuri2")
- assertThat(deserialized.smallImage!!.type).isEqualTo(SmallImageType.PHOTO)
-
- val data2 = DiscreteRangedValueComplicationData.Builder(
- value = 95, min = 0, max = 100,
- contentDescription = "content description".complicationText
- )
- .setMonochromaticImage(monochromaticImage2)
- .setSmallImage(smallImage2)
- .setTitle("battery".complicationText)
- .setDataSource(dataSourceA)
- .build()
-
- val data3 = DiscreteRangedValueComplicationData.Builder(
- value = 95, min = 0, max = 100,
- contentDescription = "content description2".complicationText
- )
- .setTitle("battery2".complicationText)
- .setDataSource(dataSourceB)
- .build()
-
- assertThat(data).isEqualTo(data2)
- assertThat(data).isNotEqualTo(data3)
- assertThat(data.hashCode()).isEqualTo(data2.hashCode())
- assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
- assertThat(data.toString()).isEqualTo(
- "DiscreteRangedValueComplicationData(value=95, min=0, max=100, " +
- "monochromaticImage=MonochromaticImage(image=Icon(typ=URI uri=someuri), " +
- "ambientImage=null), smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), " +
- "type=PHOTO, ambientImage=null), title=ComplicationText{mSurroundingText=battery," +
- " mTimeDependentText=null}, text=null," +
- " contentDescription=ComplicationText{mSurroundingText=content description, " +
- "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
- "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
- "+1000000000-12-31T23:59:59.999999999Z), " +
- "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
- )
- }
-
- @OptIn(ComplicationExperimental::class)
- @Test
public fun rangedValueComplicationData_with_ColorRamp() {
val data = RangedValueComplicationData.Builder(
value = 95f, min = 0f, max = 100f,
@@ -863,6 +721,7 @@
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(95f)
+ .setRangedValueType(RangedValueTypes.UNDEFINED)
.setRangedMinValue(0f)
.setRangedMaxValue(100f)
.setShortTitle(WireComplicationText.plainText("battery"))
@@ -907,7 +766,7 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "RangedValueComplicationData(value=95.0, min=0.0, max=100.0, " +
+ "RangedValueComplicationData(value=95.0, valueType=0, min=0.0, max=100.0, " +
"monochromaticImage=null, smallImage=null, title=ComplicationText{" +
"mSurroundingText=battery, mTimeDependentText=null}, text=null, " +
"contentDescription=ComplicationText{mSurroundingText=content description, " +
@@ -931,6 +790,7 @@
),
contentDescription = "content description".complicationText
)
+ .setElementBackgroundColor(Color.GRAY)
.setTitle("calories".complicationText)
.setDataSource(dataSourceA)
.build()
@@ -939,6 +799,7 @@
WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
+ .setElementBackgroundColor(Color.GRAY)
.setShortTitle(WireComplicationText.plainText("calories"))
.setContentDescription(WireComplicationText.plainText("content description"))
.setDataSource(dataSourceA)
@@ -968,6 +829,7 @@
),
contentDescription = "content description".complicationText
)
+ .setElementBackgroundColor(Color.GRAY)
.setTitle("calories".complicationText)
.setDataSource(dataSourceA)
.build()
@@ -991,8 +853,9 @@
assertThat(data.toString()).isEqualTo(
"WeightedElementsComplicationData(elements=Element(color=-65536, weight=0.5)," +
" Element(color=-16711936, weight=1.0), Element(color=-16776961, weight=2.0), " +
- "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
- "mSurroundingText=calories, mTimeDependentText=null}, text=null, " +
+ "elementBackgroundColor=-7829368, monochromaticImage=null, smallImage=null, " +
+ "title=ComplicationText{mSurroundingText=calories, mTimeDependentText=null}, " +
+ "text=null, " +
"contentDescription=ComplicationText{mSurroundingText=content description, " +
"mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
"tapAction=null, validTimeRange=TimeRange(" +
@@ -1024,6 +887,7 @@
WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
+ .setElementBackgroundColor(Color.TRANSPARENT)
.setShortTitle(WireComplicationText.plainText("calories"))
.setIcon(monochromaticImageIcon)
.setSmallImage(smallImageIcon)
@@ -1086,13 +950,14 @@
assertThat(data.toString()).isEqualTo(
"WeightedElementsComplicationData(elements=Element(color=-65536, weight=0.5)," +
" Element(color=-16711936, weight=1.0), Element(color=-16776961, weight=2.0), " +
- "monochromaticImage=MonochromaticImage(image=Icon(typ=URI uri=someuri), " +
- "ambientImage=null), smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), " +
- "type=PHOTO, ambientImage=null), title=ComplicationText{mSurroundingText=" +
- "calories, mTimeDependentText=null}, text=null, " +
- "contentDescription=ComplicationText{mSurroundingText=content description, " +
- "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
+ "elementBackgroundColor=0, monochromaticImage=MonochromaticImage(" +
+ "image=Icon(typ=URI uri=someuri), ambientImage=null), " +
+ "smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), type=PHOTO, " +
+ "ambientImage=null), title=ComplicationText{mSurroundingText=calories, " +
+ "mTimeDependentText=null}, text=null, contentDescription=ComplicationText{" +
+ "mSurroundingText=content description, mTimeDependentText=null}), " +
+ "tapActionLostDueToSerialization=false, tapAction=null, " +
+ "validTimeRange=TimeRange(startDateTimeMillis=" +
"-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
"+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
@@ -1408,6 +1273,7 @@
.build(),
WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(95f)
+ .setRangedValueType(RangedValueTypes.UNDEFINED)
.setRangedMinValue(0f)
.setRangedMaxValue(100f)
.setShortText(WireComplicationText.plainText("battery"))
@@ -1487,10 +1353,10 @@
"validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
"endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), dataSource=null, " +
"persistencePolicy=0, displayPolicy=0), RangedValueComplicationData(value=95.0, " +
- "min=0.0, max=100.0, monochromaticImage=null, smallImage=null, title=null, " +
- "text=ComplicationText{mSurroundingText=battery, mTimeDependentText=null}, " +
- "contentDescription=ComplicationText{mSurroundingText=, " +
- "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
+ "valueType=0, min=0.0, max=100.0, monochromaticImage=null, smallImage=null, " +
+ "title=null, text=ComplicationText{mSurroundingText=battery, " +
+ "mTimeDependentText=null}, contentDescription=ComplicationText{mSurroundingText=," +
+ " mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
"tapAction=null, validTimeRange=TimeRange(" +
"startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
"endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), dataSource=null, " +
@@ -1667,6 +1533,7 @@
.setPlaceholder(
WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(RangedValueComplicationData.PLACEHOLDER)
+ .setRangedValueType(RangedValueTypes.UNDEFINED)
.setRangedMinValue(0f)
.setRangedMaxValue(100f)
.setShortText(ComplicationText.PLACEHOLDER.toWireComplicationText())
@@ -1715,7 +1582,7 @@
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
"NoDataComplicationData(placeholder=RangedValueComplicationData(" +
- "value=3.4028235E38, min=0.0, max=100.0, monochromaticImage=null, " +
+ "value=3.4028235E38, valueType=0, min=0.0, max=100.0, monochromaticImage=null, " +
"smallImage=null, title=null, text=ComplicationText{" +
"mSurroundingText=__placeholder__, mTimeDependentText=null}, " +
"contentDescription=ComplicationText{mSurroundingText=" +
@@ -1819,90 +1686,6 @@
@OptIn(ComplicationExperimental::class)
@Test
- public fun noDataComplicationData_discreteRangedValue() {
- val data = NoDataComplicationData(
- DiscreteRangedValueComplicationData.Builder(
- value = DiscreteRangedValueComplicationData.PLACEHOLDER,
- min = 0,
- max = 100,
- "content description".complicationText
- )
- .setText(ComplicationText.PLACEHOLDER)
- .setDataSource(dataSourceA)
- .build()
- )
- ParcelableSubject.assertThat(data.asWireComplicationData())
- .hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_DISCRETE_RANGED_VALUE)
- .setDiscreteRangedValue(DiscreteRangedValueComplicationData.PLACEHOLDER)
- .setDiscreteRangedMinValue(0)
- .setDiscreteRangedMaxValue(100)
- .setShortText(ComplicationText.PLACEHOLDER.toWireComplicationText())
- .setContentDescription(
- WireComplicationText.plainText("content description")
- )
- .setDataSource(dataSourceA)
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
- )
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
- )
- testRoundTripConversions(data)
- val deserialized = serializeAndDeserialize(data) as NoDataComplicationData
- assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
- .isEqualTo("content description")
-
- val data2 = NoDataComplicationData(
- DiscreteRangedValueComplicationData.Builder(
- value = DiscreteRangedValueComplicationData.PLACEHOLDER,
- min = 0,
- max = 100,
- "content description".complicationText
- )
- .setText(ComplicationText.PLACEHOLDER)
- .setDataSource(dataSourceA)
- .build()
- )
- val data3 = NoDataComplicationData(
- DiscreteRangedValueComplicationData.Builder(
- value = DiscreteRangedValueComplicationData.PLACEHOLDER,
- min = 0,
- max = 100,
- "content description".complicationText
- )
- .setTitle(ComplicationText.PLACEHOLDER)
- .setText(ComplicationText.PLACEHOLDER)
- .build()
- )
-
- assertThat(data).isEqualTo(data2)
- assertThat(data).isNotEqualTo(data3)
- assertThat(data.hashCode()).isEqualTo(data2.hashCode())
- assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
- assertThat(data.toString()).isEqualTo(
- "NoDataComplicationData(placeholder=DiscreteRangedValueComplicationData(" +
- "value=2147483647, min=0, max=100, monochromaticImage=null, smallImage=null, " +
- "title=null, text=ComplicationText{mSurroundingText=__placeholder__, " +
- "mTimeDependentText=null}, contentDescription=ComplicationText{mSurroundingText=" +
- "content description, mTimeDependentText=null}), " +
- "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
- "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
- "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
- "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, " +
- "displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
- "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
- "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), persistencePolicy=0, " +
- "displayPolicy=0)"
- )
- }
-
- @OptIn(ComplicationExperimental::class)
- @Test
public fun noDataComplicationData_weightedElements() {
val data = NoDataComplicationData(
WeightedElementsComplicationData.Builder(
@@ -1914,6 +1697,7 @@
contentDescription = "content description".complicationText
)
.setTitle("calories".complicationText)
+ .setElementBackgroundColor(Color.GRAY)
.setDataSource(dataSourceA)
.build()
)
@@ -1924,6 +1708,7 @@
WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
+ .setElementBackgroundColor(Color.GRAY)
.setShortTitle(WireComplicationText.plainText("calories"))
.setContentDescription(
WireComplicationText.plainText("content description")
@@ -1952,6 +1737,7 @@
contentDescription = "content description".complicationText
)
.setTitle("calories".complicationText)
+ .setElementBackgroundColor(Color.GRAY)
.setDataSource(dataSourceA)
.build()
)
@@ -1975,7 +1761,8 @@
assertThat(data.toString()).isEqualTo(
"NoDataComplicationData(placeholder=WeightedElementsComplicationData(" +
"elements=Element(color=-65536, weight=0.5), Element(color=-16711936, " +
- "weight=1.0), Element(color=-16776961, weight=2.0), monochromaticImage=null, " +
+ "weight=1.0), Element(color=-16776961, weight=2.0), " +
+ "elementBackgroundColor=-7829368, monochromaticImage=null, " +
"smallImage=null, title=ComplicationText{mSurroundingText=calories, " +
"mTimeDependentText=null}, text=null, contentDescription=ComplicationText{" +
"mSurroundingText=content description, mTimeDependentText=null}), " +
@@ -2003,6 +1790,7 @@
.setText(ComplicationText.PLACEHOLDER)
.setDataSource(dataSourceA)
.setColorRamp(ColorRamp(intArrayOf(Color.RED, Color.GREEN, Color.BLUE), true))
+ .setValueType(RangedValueTypes.SCORE)
.build()
)
ParcelableSubject.assertThat(data.asWireComplicationData())
@@ -2011,6 +1799,7 @@
.setPlaceholder(
WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(RangedValueComplicationData.PLACEHOLDER)
+ .setRangedValueType(RangedValueTypes.SCORE)
.setRangedMinValue(0f)
.setRangedMaxValue(100f)
.setShortText(ComplicationText.PLACEHOLDER.toWireComplicationText())
@@ -2043,6 +1832,7 @@
.setText(ComplicationText.PLACEHOLDER)
.setDataSource(dataSourceA)
.setColorRamp(ColorRamp(intArrayOf(Color.RED, Color.GREEN, Color.BLUE), true))
+ .setValueType(RangedValueTypes.SCORE)
.build()
)
val data3 = NoDataComplicationData(
@@ -2062,7 +1852,7 @@
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
"NoDataComplicationData(placeholder=RangedValueComplicationData(" +
- "value=3.4028235E38, min=0.0, max=100.0, monochromaticImage=null, " +
+ "value=3.4028235E38, valueType=1, min=0.0, max=100.0, monochromaticImage=null, " +
"smallImage=null, title=null, text=ComplicationText{" +
"mSurroundingText=__placeholder__, mTimeDependentText=null}, " +
"contentDescription=ComplicationText{mSurroundingText=" +
@@ -2411,28 +2201,12 @@
@OptIn(ComplicationExperimental::class)
@Test
- public fun discreteRangedValueComplicationData() {
- assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_DISCRETE_RANGED_VALUE)
- .setDiscreteRangedValue(95)
- .setDiscreteRangedMinValue(0)
- .setDiscreteRangedMaxValue(100)
- .setShortTitle(WireComplicationText.plainText("battery"))
- .setContentDescription(WireComplicationText.plainText("content description"))
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build(),
- ComplicationType.DISCRETE_RANGED_VALUE
- )
- }
-
- @OptIn(ComplicationExperimental::class)
- @Test
public fun weightedElementsComplicationData() {
assertRoundtrip(
WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
+ .setElementBackgroundColor(Color.DKGRAY)
.setShortTitle(WireComplicationText.plainText("calories"))
.setContentDescription(WireComplicationText.plainText("content description"))
.setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
@@ -2603,33 +2377,7 @@
WireComplicationText.plainText("content description")
)
.setRangedValue(75f)
- .setRangedMinValue(0f)
- .setRangedMaxValue(100f)
- .setShortTitle(WireComplicationText.plainText("battery"))
- .setIcon(icon)
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
- )
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build(),
- ComplicationType.NO_DATA
- )
- }
-
- @OptIn(ComplicationExperimental::class)
- @Test
- public fun noDataComplicationData_rangedValue_drawSegmented() {
- val icon = Icon.createWithContentUri("someuri")
- assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
- .setContentDescription(
- WireComplicationText.plainText("content description")
- )
- .setRangedValue(75f)
+ .setRangedValueType(RangedValueTypes.UNDEFINED)
.setRangedMinValue(0f)
.setRangedMaxValue(100f)
.setShortTitle(WireComplicationText.plainText("battery"))
@@ -2675,33 +2423,6 @@
@OptIn(ComplicationExperimental::class)
@Test
- public fun noDataComplicationData_discreteRangedValue() {
- val icon = Icon.createWithContentUri("someuri")
- assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_DISCRETE_RANGED_VALUE)
- .setContentDescription(
- WireComplicationText.plainText("content description")
- )
- .setDiscreteRangedValue(75)
- .setDiscreteRangedMinValue(0)
- .setDiscreteRangedMaxValue(100)
- .setShortTitle(WireComplicationText.plainText("battery"))
- .setIcon(icon)
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
- )
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build(),
- ComplicationType.NO_DATA
- )
- }
-
- @OptIn(ComplicationExperimental::class)
- @Test
public fun noDataComplicationData_weightedElementsComplicationData() {
assertRoundtrip(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
@@ -2709,6 +2430,7 @@
WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
+ .setElementBackgroundColor(Color.DKGRAY)
.setShortTitle(WireComplicationText.plainText("calories"))
.setContentDescription(
WireComplicationText.plainText("content description")
@@ -2891,20 +2613,6 @@
@OptIn(ComplicationExperimental::class)
@Test
- public fun discreteRangedValueComplicationData() {
- assertThat(
- DiscreteRangedValueComplicationData.Builder(
- value = 95, min = 0, max = 100,
- contentDescription = ComplicationText.EMPTY
- )
- .setText("battery".complicationText)
- .setTapAction(mPendingIntent)
- .build().asWireComplicationData().tapAction
- ).isEqualTo(mPendingIntent)
- }
-
- @OptIn(ComplicationExperimental::class)
- @Test
public fun weightedElementsComplicationData() {
assertThat(
WeightedElementsComplicationData.Builder(
@@ -3065,20 +2773,6 @@
@OptIn(ComplicationExperimental::class)
@Test
- public fun discreteRangedValueComplicationData() {
- assertThat(
- DiscreteRangedValueComplicationData.Builder(
- value = 95, min = 0, max = 100,
- contentDescription = ComplicationText.EMPTY
- )
- .setText("battery".complicationText)
- .setTapAction(mPendingIntent)
- .build().asWireComplicationData().toApiComplicationData().tapAction
- ).isEqualTo(mPendingIntent)
- }
-
- @OptIn(ComplicationExperimental::class)
- @Test
public fun weightedElementsComplicationData() {
assertThat(
WeightedElementsComplicationData.Builder(
@@ -3243,6 +2937,7 @@
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(95f)
+ .setRangedValueType(RangedValueTypes.UNDEFINED)
.setRangedMinValue(0f)
.setRangedMaxValue(100f)
.setShortText(WireComplicationText.plainText("battery"))
@@ -3284,31 +2979,6 @@
@OptIn(ComplicationExperimental::class)
@Test
- public fun discreteRangedValueComplicationData() {
- val data = DiscreteRangedValueComplicationData.Builder(
- value = 95, min = 0, max = 100,
- contentDescription = ComplicationText.EMPTY
- )
- .setText("battery".complicationText)
- .setValidTimeRange(TimeRange.between(testStartInstant, testEndDateInstant))
- .build()
- ParcelableSubject.assertThat(data.asWireComplicationData())
- .hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_DISCRETE_RANGED_VALUE)
- .setDiscreteRangedValue(95)
- .setDiscreteRangedMinValue(0)
- .setDiscreteRangedMaxValue(100)
- .setShortText(WireComplicationText.plainText("battery"))
- .setStartDateTimeMillis(testStartInstant.toEpochMilli())
- .setEndDateTimeMillis(testEndDateInstant.toEpochMilli())
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
- )
- }
-
- @OptIn(ComplicationExperimental::class)
- @Test
public fun weightedElementsComplicationData() {
val data = WeightedElementsComplicationData.Builder(
listOf(
@@ -3326,6 +2996,7 @@
WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
+ .setElementBackgroundColor(Color.TRANSPARENT)
.setShortTitle(WireComplicationText.plainText("calories"))
.setContentDescription(WireComplicationText.plainText("content description"))
.setStartDateTimeMillis(testStartInstant.toEpochMilli())
@@ -3456,6 +3127,7 @@
val wireRangedValue = WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(95f)
+ .setRangedValueType(RangedValueTypes.UNDEFINED)
.setRangedMinValue(0f)
.setRangedMaxValue(100f)
.setShortText(WireComplicationText.plainText("battery"))
@@ -3541,6 +3213,7 @@
.setPlaceholder(
WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(95f)
+ .setRangedValueType(RangedValueTypes.UNDEFINED)
.setRangedMinValue(0f)
.setRangedMaxValue(100f)
.setShortText(WireComplicationText.plainText("battery"))
@@ -3591,38 +3264,6 @@
@OptIn(ComplicationExperimental::class)
@Test
- public fun noDataComplicationData_discreteRangedValue() {
- val data = NoDataComplicationData(
- DiscreteRangedValueComplicationData.Builder(
- value = 95,
- min = 0,
- max = 100,
- ComplicationText.EMPTY
- )
- .setText("battery".complicationText)
- .build()
- )
- ParcelableSubject.assertThat(data.asWireComplicationData())
- .hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_DISCRETE_RANGED_VALUE)
- .setDiscreteRangedValue(95)
- .setDiscreteRangedMinValue(0)
- .setDiscreteRangedMaxValue(100)
- .setShortText(WireComplicationText.plainText("battery"))
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
- )
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
- )
- }
-
- @OptIn(ComplicationExperimental::class)
- @Test
public fun noDataComplicationData_weightedElements() {
val data = NoDataComplicationData(
WeightedElementsComplicationData.Builder(
@@ -3643,6 +3284,7 @@
WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
+ .setElementBackgroundColor(Color.TRANSPARENT)
.setShortTitle(WireComplicationText.plainText("calories"))
.setContentDescription(
WireComplicationText.plainText("content description")
@@ -3792,6 +3434,7 @@
val wireRangedValue = WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(95f)
+ .setRangedValueType(RangedValueTypes.UNDEFINED)
.setRangedMinValue(0f)
.setRangedMaxValue(100f)
.setShortText(WireComplicationText.plainText("battery"))
@@ -3893,7 +3536,7 @@
.build()
assertThat(data.toString()).isEqualTo("RangedValueComplicationData(value=REDACTED, " +
- "min=0.0, max=100.0, monochromaticImage=null, smallImage=null, " +
+ "valueType=0, min=0.0, max=100.0, monochromaticImage=null, smallImage=null, " +
"title=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
"text=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
"contentDescription=ComplicationText{mSurroundingText=REDACTED, " +
@@ -3932,34 +3575,6 @@
)
}
- @OptIn(ComplicationExperimental::class)
- @Test
- fun discreteRangedValue() {
- val data = DiscreteRangedValueComplicationData.Builder(
- 50,
- 0,
- 100,
- "content description".complicationText
- )
- .setText("text".complicationText)
- .setTitle("title".complicationText)
- .build()
-
- assertThat(data.toString()).isEqualTo(
- "DiscreteRangedValueComplicationData(value=REDACTED, " +
- "min=0, max=100, monochromaticImage=null, smallImage=null, title=ComplicationText{" +
- "mSurroundingText=REDACTED, mTimeDependentText=null}, text=ComplicationText{" +
- "mSurroundingText=REDACTED, mTimeDependentText=null}, contentDescription=" +
- "ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}), " +
- "tapActionLostDueToSerialization=false, tapAction=null, " +
- "validTimeRange=TimeRange(REDACTED), dataSource=null, persistencePolicy=0, " +
- "displayPolicy=0)"
- )
- assertThat(data.asWireComplicationData().toString()).isEqualTo(
- "ComplicationData{mType=14, mFields=REDACTED}"
- )
- }
-
@Test
fun placeholder() {
val data = NoDataComplicationData(
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt
index 1f16233..dbedd54 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt
@@ -273,66 +273,6 @@
@OptIn(ComplicationExperimental::class)
@Test
- fun placeholder_discreteRangedValue() {
- val placeholderRangedValue = NoDataComplicationData(
- DiscreteRangedValueComplicationData.Builder(
- value = DiscreteRangedValueComplicationData.PLACEHOLDER,
- min = 1,
- max = 10,
- contentDescription
- )
- .setText(ComplicationText.PLACEHOLDER)
- .setTitle(ComplicationText.PLACEHOLDER)
- .setMonochromaticImage(MonochromaticImage.PLACEHOLDER)
- .setSmallImage(SmallImage.PLACEHOLDER)
- .build()
- ).toWireFormatRoundTrip().placeholder as DiscreteRangedValueComplicationData
-
- assertThat(placeholderRangedValue.value)
- .isEqualTo(DiscreteRangedValueComplicationData.PLACEHOLDER)
- assertThat(placeholderRangedValue.text).isEqualTo(ComplicationText.PLACEHOLDER)
- assertThat(placeholderRangedValue.title).isEqualTo(ComplicationText.PLACEHOLDER)
- assertThat(placeholderRangedValue.monochromaticImage)
- .isEqualTo(MonochromaticImage.PLACEHOLDER)
- assertThat(placeholderRangedValue.smallImage).isEqualTo(SmallImage.PLACEHOLDER)
- assertThat(placeholderRangedValue.contentDescription!!.getTextAt(resources, Instant.EPOCH))
- .isEqualTo("description")
- assertThat(placeholderRangedValue.hasPlaceholderFields()).isTrue()
- }
-
- @OptIn(ComplicationExperimental::class)
- @Test
- fun normal_discreteRangedValue() {
- val placeholderRangedValue = NoDataComplicationData(
- DiscreteRangedValueComplicationData.Builder(
- value = 7,
- min = 1,
- max = 10,
- contentDescription
- )
- .setText(text)
- .setTitle(title)
- .setMonochromaticImage(monochromaticImage)
- .setSmallImage(smallImage)
- .build()
- ).toWireFormatRoundTrip().placeholder as DiscreteRangedValueComplicationData
-
- assertThat(placeholderRangedValue.text!!.getTextAt(resources, Instant.EPOCH)).isEqualTo(
- text.getTextAt(resources, Instant.EPOCH)
- )
- assertThat(placeholderRangedValue.title!!.getTextAt(resources, Instant.EPOCH)).isEqualTo(
- title.getTextAt(resources, Instant.EPOCH)
- )
- assertThat(placeholderRangedValue.monochromaticImage).isEqualTo(monochromaticImage)
- assertThat(placeholderRangedValue.smallImage).isEqualTo(smallImage)
- assertThat(placeholderRangedValue.value).isEqualTo(7)
- assertThat(placeholderRangedValue.min).isEqualTo(1)
- assertThat(placeholderRangedValue.max).isEqualTo(10)
- assertThat(placeholderRangedValue.hasPlaceholderFields()).isFalse()
- }
-
- @OptIn(ComplicationExperimental::class)
- @Test
fun placeholder_weightedElements() {
val placeholderWeightedElements = NoDataComplicationData(
WeightedElementsComplicationData.Builder(
@@ -393,26 +333,6 @@
assertThat(weightedElements.hasPlaceholderFields()).isFalse()
}
- @OptIn(ComplicationExperimental::class)
- @Test
- fun titleAbsent_discreteRangedValue() {
- val placeholderRangedValue = NoDataComplicationData(
- DiscreteRangedValueComplicationData.Builder(
- value = DiscreteRangedValueComplicationData.PLACEHOLDER,
- min = 1,
- max = 10,
- contentDescription
- )
- .setText(ComplicationText.PLACEHOLDER)
- .build()
- ).toWireFormatRoundTrip().placeholder as DiscreteRangedValueComplicationData
-
- assertThat(placeholderRangedValue.text).isEqualTo(ComplicationText.PLACEHOLDER)
- assertThat(placeholderRangedValue.title).isNull()
- assertThat(placeholderRangedValue.monochromaticImage).isNull()
- assertThat(placeholderRangedValue.smallImage).isNull()
- }
-
@Test
fun placeholder_monochromaticImage() {
val placeholderMonochromaticImage = NoDataComplicationData(
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/ExampleWindowInitializer.kt b/window/window-samples/src/main/java/androidx/window/sample/embedding/ExampleWindowInitializer.kt
index 85b23b9..dde6316 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/ExampleWindowInitializer.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/embedding/ExampleWindowInitializer.kt
@@ -18,14 +18,12 @@
import android.content.Context
import androidx.startup.Initializer
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.embedding.SplitController
import androidx.window.sample.R
/**
* Initializes SplitController with a set of statically defined rules.
*/
-@OptIn(ExperimentalWindowApi::class)
class ExampleWindowInitializer : Initializer<SplitController> {
override fun create(context: Context): SplitController {
SplitController.initialize(context, R.xml.main_split_config)
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityBase.java b/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityBase.java
index 5810e48..05ee9a1 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityBase.java
+++ b/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityBase.java
@@ -33,7 +33,6 @@
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.util.Consumer;
-import androidx.window.core.ExperimentalWindowApi;
import androidx.window.embedding.ActivityFilter;
import androidx.window.embedding.ActivityRule;
import androidx.window.embedding.EmbeddingRule;
@@ -49,13 +48,10 @@
import java.util.List;
import java.util.Set;
-import kotlin.OptIn;
-
/**
* Sample showcase of split activity rules. Allows the user to select some split configuration
* options with checkboxes and launch activities with those options applied.
*/
-@OptIn(markerClass = ExperimentalWindowApi.class)
public class SplitActivityBase extends AppCompatActivity
implements CompoundButton.OnCheckedChangeListener {
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityList.kt b/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityList.kt
index 0c367c4..5ca237a 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityList.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityList.kt
@@ -24,13 +24,11 @@
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.util.Consumer
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.embedding.SplitController
import androidx.window.embedding.SplitInfo
import androidx.window.sample.R
import androidx.window.sample.embedding.SplitActivityDetail.Companion.EXTRA_SELECTED_ITEM
-@OptIn(ExperimentalWindowApi::class)
open class SplitActivityList : AppCompatActivity() {
lateinit var splitController: SplitController
val splitChangeListener = SplitStateChangeListener()
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityTrampoline.kt b/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityTrampoline.kt
index 67680f1..acb6565 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityTrampoline.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityTrampoline.kt
@@ -18,7 +18,6 @@
import android.content.Intent
import android.os.Bundle
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.embedding.ActivityFilter
import androidx.window.embedding.SplitController
import androidx.window.embedding.SplitPlaceholderRule
@@ -27,7 +26,6 @@
/**
* Example trampoline activity that launches a split and finishes itself.
*/
-@ExperimentalWindowApi
class SplitActivityTrampoline : SplitActivityBase() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityBase.kt b/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityBase.kt
index 2b278a0..e499df3 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityBase.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityBase.kt
@@ -27,7 +27,6 @@
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.util.Consumer
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.embedding.ActivityFilter
import androidx.window.embedding.EmbeddingRule
import androidx.window.embedding.SplitController
@@ -46,7 +45,6 @@
* split and PiP configuration options with checkboxes and launch activities with those options
* applied.
*/
-@OptIn(ExperimentalWindowApi::class)
abstract class SplitPipActivityBase : AppCompatActivity(), CompoundButton.OnCheckedChangeListener,
View.OnClickListener, RadioGroup.OnCheckedChangeListener {
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index aff36d9..e3dc726 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -8,6 +8,142 @@
}
+package androidx.window.embedding {
+
+ public final class ActivityFilter {
+ ctor public ActivityFilter(android.content.ComponentName componentName, String? intentAction);
+ method public boolean matchesActivity(android.app.Activity activity);
+ method public <T extends android.app.Activity> boolean matchesClassName(Class<T> clazz);
+ method public <T extends android.app.Activity> boolean matchesClassNameOrWildCard(Class<T>? clazz);
+ method public boolean matchesIntent(android.content.Intent intent);
+ }
+
+ public final class ActivityRule extends androidx.window.embedding.EmbeddingRule {
+ method public boolean getAlwaysExpand();
+ method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
+ property public final boolean alwaysExpand;
+ property public final java.util.Set<androidx.window.embedding.ActivityFilter> filters;
+ }
+
+ public static final class ActivityRule.Builder {
+ ctor public ActivityRule.Builder(java.util.Set<androidx.window.embedding.ActivityFilter> filters);
+ method public androidx.window.embedding.ActivityRule build();
+ method public androidx.window.embedding.ActivityRule.Builder setAlwaysExpand(boolean alwaysExpand);
+ }
+
+ public final class ActivityStack {
+ ctor public ActivityStack(java.util.List<? extends android.app.Activity> activities, optional boolean isEmpty);
+ method public operator boolean contains(android.app.Activity activity);
+ method public boolean isEmpty();
+ }
+
+ public abstract class EmbeddingRule {
+ }
+
+ public final class SplitController {
+ method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
+ method public void clearRegisteredRules();
+ method public static androidx.window.embedding.SplitController getInstance();
+ method public java.util.Set<androidx.window.embedding.EmbeddingRule> getSplitRules();
+ method public static void initialize(android.content.Context context, int staticRuleResourceId);
+ method public boolean isActivityEmbedded(android.app.Activity activity);
+ method public boolean isSplitSupported();
+ method public void registerRule(androidx.window.embedding.EmbeddingRule rule);
+ method public void removeSplitListener(androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
+ method public void unregisterRule(androidx.window.embedding.EmbeddingRule rule);
+ field public static final androidx.window.embedding.SplitController.Companion Companion;
+ }
+
+ public static final class SplitController.Companion {
+ method public androidx.window.embedding.SplitController getInstance();
+ method public void initialize(android.content.Context context, int staticRuleResourceId);
+ }
+
+ public final class SplitInfo {
+ method public operator boolean contains(android.app.Activity activity);
+ method public androidx.window.embedding.ActivityStack getPrimaryActivityStack();
+ method public androidx.window.embedding.ActivityStack getSecondaryActivityStack();
+ method public float getSplitRatio();
+ property public final androidx.window.embedding.ActivityStack primaryActivityStack;
+ property public final androidx.window.embedding.ActivityStack secondaryActivityStack;
+ property public final float splitRatio;
+ }
+
+ public final class SplitPairFilter {
+ ctor public SplitPairFilter(android.content.ComponentName primaryActivityName, android.content.ComponentName secondaryActivityName, String? secondaryActivityIntentAction);
+ method public android.content.ComponentName getPrimaryActivityName();
+ method public String? getSecondaryActivityIntentAction();
+ method public android.content.ComponentName getSecondaryActivityName();
+ method public boolean matchesActivityIntentPair(android.app.Activity primaryActivity, android.content.Intent secondaryActivityIntent);
+ method public boolean matchesActivityPair(android.app.Activity primaryActivity, android.app.Activity secondaryActivity);
+ property public final android.content.ComponentName primaryActivityName;
+ property public final String? secondaryActivityIntentAction;
+ property public final android.content.ComponentName secondaryActivityName;
+ }
+
+ public final class SplitPairRule extends androidx.window.embedding.SplitRule {
+ ctor @Deprecated public SplitPairRule(java.util.Set<androidx.window.embedding.SplitPairFilter> filters, optional int finishPrimaryWithSecondary, optional int finishSecondaryWithPrimary, optional boolean clearTop, @IntRange(from=0L) int minWidth, @IntRange(from=0L) int minSmallestWidth, optional @FloatRange(from=0.0, to=1.0) float splitRatio, optional int layoutDir);
+ method public boolean getClearTop();
+ method public java.util.Set<androidx.window.embedding.SplitPairFilter> getFilters();
+ method public int getFinishPrimaryWithSecondary();
+ method public int getFinishSecondaryWithPrimary();
+ property public final boolean clearTop;
+ property public final java.util.Set<androidx.window.embedding.SplitPairFilter> filters;
+ property public final int finishPrimaryWithSecondary;
+ property public final int finishSecondaryWithPrimary;
+ }
+
+ public static final class SplitPairRule.Builder {
+ ctor public SplitPairRule.Builder(java.util.Set<androidx.window.embedding.SplitPairFilter> filters, @IntRange(from=0L) int minWidth, @IntRange(from=0L) int minSmallestWidth);
+ method public androidx.window.embedding.SplitPairRule build();
+ method public androidx.window.embedding.SplitPairRule.Builder setClearTop(boolean clearTop);
+ method public androidx.window.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int finishPrimaryWithSecondary);
+ method public androidx.window.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int finishSecondaryWithPrimary);
+ method public androidx.window.embedding.SplitPairRule.Builder setLayoutDir(int layoutDir);
+ method public androidx.window.embedding.SplitPairRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
+ }
+
+ public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
+ ctor @Deprecated public SplitPlaceholderRule(java.util.Set<androidx.window.embedding.ActivityFilter> filters, android.content.Intent placeholderIntent, boolean isSticky, optional int finishPrimaryWithPlaceholder, optional @IntRange(from=0L) int minWidth, optional @IntRange(from=0L) int minSmallestWidth, optional @FloatRange(from=0.0, to=1.0) float splitRatio, optional int layoutDirection);
+ method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
+ method public int getFinishPrimaryWithPlaceholder();
+ method public android.content.Intent getPlaceholderIntent();
+ method public boolean isSticky();
+ property public final java.util.Set<androidx.window.embedding.ActivityFilter> filters;
+ property public final int finishPrimaryWithPlaceholder;
+ property public final boolean isSticky;
+ property public final android.content.Intent placeholderIntent;
+ }
+
+ public static final class SplitPlaceholderRule.Builder {
+ ctor public SplitPlaceholderRule.Builder(java.util.Set<androidx.window.embedding.ActivityFilter> filters, android.content.Intent placeholderIntent, @IntRange(from=0L) int minWidth, @IntRange(from=0L) int minSmallestWidth);
+ method public androidx.window.embedding.SplitPlaceholderRule build();
+ method public androidx.window.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(int finishPrimaryWithPlaceholder);
+ method public androidx.window.embedding.SplitPlaceholderRule.Builder setLayoutDir(int layoutDir);
+ method public androidx.window.embedding.SplitPlaceholderRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
+ method public androidx.window.embedding.SplitPlaceholderRule.Builder setSticky(boolean isSticky);
+ }
+
+ public class SplitRule extends androidx.window.embedding.EmbeddingRule {
+ method public final int getLayoutDirection();
+ method public final int getMinSmallestWidth();
+ method public final int getMinWidth();
+ method public final float getSplitRatio();
+ property public final int layoutDirection;
+ property public final int minSmallestWidth;
+ property public final int minWidth;
+ property public final float splitRatio;
+ field public static final androidx.window.embedding.SplitRule.Companion Companion;
+ field public static final int FINISH_ADJACENT = 2; // 0x2
+ field public static final int FINISH_ALWAYS = 1; // 0x1
+ field public static final int FINISH_NEVER = 0; // 0x0
+ }
+
+ public static final class SplitRule.Companion {
+ }
+
+}
+
package androidx.window.layout {
public interface DisplayFeature {
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index 6b7eeb8..7c76c1d 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -17,7 +17,7 @@
package androidx.window.embedding {
- @androidx.window.core.ExperimentalWindowApi public final class ActivityFilter {
+ public final class ActivityFilter {
ctor public ActivityFilter(android.content.ComponentName componentName, String? intentAction);
method public boolean matchesActivity(android.app.Activity activity);
method public <T extends android.app.Activity> boolean matchesClassName(Class<T> clazz);
@@ -25,7 +25,7 @@
method public boolean matchesIntent(android.content.Intent intent);
}
- @androidx.window.core.ExperimentalWindowApi public final class ActivityRule extends androidx.window.embedding.EmbeddingRule {
+ public final class ActivityRule extends androidx.window.embedding.EmbeddingRule {
method public boolean getAlwaysExpand();
method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
property public final boolean alwaysExpand;
@@ -38,16 +38,16 @@
method public androidx.window.embedding.ActivityRule.Builder setAlwaysExpand(boolean alwaysExpand);
}
- @androidx.window.core.ExperimentalWindowApi public final class ActivityStack {
+ public final class ActivityStack {
ctor public ActivityStack(java.util.List<? extends android.app.Activity> activities, optional boolean isEmpty);
method public operator boolean contains(android.app.Activity activity);
method public boolean isEmpty();
}
- @androidx.window.core.ExperimentalWindowApi public abstract class EmbeddingRule {
+ public abstract class EmbeddingRule {
}
- @androidx.window.core.ExperimentalWindowApi public final class SplitController {
+ public final class SplitController {
method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
method public void clearRegisteredRules();
method public static androidx.window.embedding.SplitController getInstance();
@@ -66,7 +66,7 @@
method public void initialize(android.content.Context context, int staticRuleResourceId);
}
- @androidx.window.core.ExperimentalWindowApi public final class SplitInfo {
+ public final class SplitInfo {
method public operator boolean contains(android.app.Activity activity);
method public androidx.window.embedding.ActivityStack getPrimaryActivityStack();
method public androidx.window.embedding.ActivityStack getSecondaryActivityStack();
@@ -76,7 +76,7 @@
property public final float splitRatio;
}
- @androidx.window.core.ExperimentalWindowApi public final class SplitPairFilter {
+ public final class SplitPairFilter {
ctor public SplitPairFilter(android.content.ComponentName primaryActivityName, android.content.ComponentName secondaryActivityName, String? secondaryActivityIntentAction);
method public android.content.ComponentName getPrimaryActivityName();
method public String? getSecondaryActivityIntentAction();
@@ -88,7 +88,7 @@
property public final android.content.ComponentName secondaryActivityName;
}
- @androidx.window.core.ExperimentalWindowApi public final class SplitPairRule extends androidx.window.embedding.SplitRule {
+ public final class SplitPairRule extends androidx.window.embedding.SplitRule {
ctor @Deprecated public SplitPairRule(java.util.Set<androidx.window.embedding.SplitPairFilter> filters, optional int finishPrimaryWithSecondary, optional int finishSecondaryWithPrimary, optional boolean clearTop, @IntRange(from=0L) int minWidth, @IntRange(from=0L) int minSmallestWidth, optional @FloatRange(from=0.0, to=1.0) float splitRatio, optional int layoutDir);
method public boolean getClearTop();
method public java.util.Set<androidx.window.embedding.SplitPairFilter> getFilters();
@@ -110,7 +110,7 @@
method public androidx.window.embedding.SplitPairRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
}
- @androidx.window.core.ExperimentalWindowApi public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
+ public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
ctor @Deprecated public SplitPlaceholderRule(java.util.Set<androidx.window.embedding.ActivityFilter> filters, android.content.Intent placeholderIntent, boolean isSticky, optional int finishPrimaryWithPlaceholder, optional @IntRange(from=0L) int minWidth, optional @IntRange(from=0L) int minSmallestWidth, optional @FloatRange(from=0.0, to=1.0) float splitRatio, optional int layoutDirection);
method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
method public int getFinishPrimaryWithPlaceholder();
@@ -131,7 +131,7 @@
method public androidx.window.embedding.SplitPlaceholderRule.Builder setSticky(boolean isSticky);
}
- @androidx.window.core.ExperimentalWindowApi public class SplitRule extends androidx.window.embedding.EmbeddingRule {
+ public class SplitRule extends androidx.window.embedding.EmbeddingRule {
method public final int getLayoutDirection();
method public final int getMinSmallestWidth();
method public final int getMinWidth();
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index aff36d9..e3dc726 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -8,6 +8,142 @@
}
+package androidx.window.embedding {
+
+ public final class ActivityFilter {
+ ctor public ActivityFilter(android.content.ComponentName componentName, String? intentAction);
+ method public boolean matchesActivity(android.app.Activity activity);
+ method public <T extends android.app.Activity> boolean matchesClassName(Class<T> clazz);
+ method public <T extends android.app.Activity> boolean matchesClassNameOrWildCard(Class<T>? clazz);
+ method public boolean matchesIntent(android.content.Intent intent);
+ }
+
+ public final class ActivityRule extends androidx.window.embedding.EmbeddingRule {
+ method public boolean getAlwaysExpand();
+ method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
+ property public final boolean alwaysExpand;
+ property public final java.util.Set<androidx.window.embedding.ActivityFilter> filters;
+ }
+
+ public static final class ActivityRule.Builder {
+ ctor public ActivityRule.Builder(java.util.Set<androidx.window.embedding.ActivityFilter> filters);
+ method public androidx.window.embedding.ActivityRule build();
+ method public androidx.window.embedding.ActivityRule.Builder setAlwaysExpand(boolean alwaysExpand);
+ }
+
+ public final class ActivityStack {
+ ctor public ActivityStack(java.util.List<? extends android.app.Activity> activities, optional boolean isEmpty);
+ method public operator boolean contains(android.app.Activity activity);
+ method public boolean isEmpty();
+ }
+
+ public abstract class EmbeddingRule {
+ }
+
+ public final class SplitController {
+ method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
+ method public void clearRegisteredRules();
+ method public static androidx.window.embedding.SplitController getInstance();
+ method public java.util.Set<androidx.window.embedding.EmbeddingRule> getSplitRules();
+ method public static void initialize(android.content.Context context, int staticRuleResourceId);
+ method public boolean isActivityEmbedded(android.app.Activity activity);
+ method public boolean isSplitSupported();
+ method public void registerRule(androidx.window.embedding.EmbeddingRule rule);
+ method public void removeSplitListener(androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
+ method public void unregisterRule(androidx.window.embedding.EmbeddingRule rule);
+ field public static final androidx.window.embedding.SplitController.Companion Companion;
+ }
+
+ public static final class SplitController.Companion {
+ method public androidx.window.embedding.SplitController getInstance();
+ method public void initialize(android.content.Context context, int staticRuleResourceId);
+ }
+
+ public final class SplitInfo {
+ method public operator boolean contains(android.app.Activity activity);
+ method public androidx.window.embedding.ActivityStack getPrimaryActivityStack();
+ method public androidx.window.embedding.ActivityStack getSecondaryActivityStack();
+ method public float getSplitRatio();
+ property public final androidx.window.embedding.ActivityStack primaryActivityStack;
+ property public final androidx.window.embedding.ActivityStack secondaryActivityStack;
+ property public final float splitRatio;
+ }
+
+ public final class SplitPairFilter {
+ ctor public SplitPairFilter(android.content.ComponentName primaryActivityName, android.content.ComponentName secondaryActivityName, String? secondaryActivityIntentAction);
+ method public android.content.ComponentName getPrimaryActivityName();
+ method public String? getSecondaryActivityIntentAction();
+ method public android.content.ComponentName getSecondaryActivityName();
+ method public boolean matchesActivityIntentPair(android.app.Activity primaryActivity, android.content.Intent secondaryActivityIntent);
+ method public boolean matchesActivityPair(android.app.Activity primaryActivity, android.app.Activity secondaryActivity);
+ property public final android.content.ComponentName primaryActivityName;
+ property public final String? secondaryActivityIntentAction;
+ property public final android.content.ComponentName secondaryActivityName;
+ }
+
+ public final class SplitPairRule extends androidx.window.embedding.SplitRule {
+ ctor @Deprecated public SplitPairRule(java.util.Set<androidx.window.embedding.SplitPairFilter> filters, optional int finishPrimaryWithSecondary, optional int finishSecondaryWithPrimary, optional boolean clearTop, @IntRange(from=0L) int minWidth, @IntRange(from=0L) int minSmallestWidth, optional @FloatRange(from=0.0, to=1.0) float splitRatio, optional int layoutDir);
+ method public boolean getClearTop();
+ method public java.util.Set<androidx.window.embedding.SplitPairFilter> getFilters();
+ method public int getFinishPrimaryWithSecondary();
+ method public int getFinishSecondaryWithPrimary();
+ property public final boolean clearTop;
+ property public final java.util.Set<androidx.window.embedding.SplitPairFilter> filters;
+ property public final int finishPrimaryWithSecondary;
+ property public final int finishSecondaryWithPrimary;
+ }
+
+ public static final class SplitPairRule.Builder {
+ ctor public SplitPairRule.Builder(java.util.Set<androidx.window.embedding.SplitPairFilter> filters, @IntRange(from=0L) int minWidth, @IntRange(from=0L) int minSmallestWidth);
+ method public androidx.window.embedding.SplitPairRule build();
+ method public androidx.window.embedding.SplitPairRule.Builder setClearTop(boolean clearTop);
+ method public androidx.window.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int finishPrimaryWithSecondary);
+ method public androidx.window.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int finishSecondaryWithPrimary);
+ method public androidx.window.embedding.SplitPairRule.Builder setLayoutDir(int layoutDir);
+ method public androidx.window.embedding.SplitPairRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
+ }
+
+ public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
+ ctor @Deprecated public SplitPlaceholderRule(java.util.Set<androidx.window.embedding.ActivityFilter> filters, android.content.Intent placeholderIntent, boolean isSticky, optional int finishPrimaryWithPlaceholder, optional @IntRange(from=0L) int minWidth, optional @IntRange(from=0L) int minSmallestWidth, optional @FloatRange(from=0.0, to=1.0) float splitRatio, optional int layoutDirection);
+ method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
+ method public int getFinishPrimaryWithPlaceholder();
+ method public android.content.Intent getPlaceholderIntent();
+ method public boolean isSticky();
+ property public final java.util.Set<androidx.window.embedding.ActivityFilter> filters;
+ property public final int finishPrimaryWithPlaceholder;
+ property public final boolean isSticky;
+ property public final android.content.Intent placeholderIntent;
+ }
+
+ public static final class SplitPlaceholderRule.Builder {
+ ctor public SplitPlaceholderRule.Builder(java.util.Set<androidx.window.embedding.ActivityFilter> filters, android.content.Intent placeholderIntent, @IntRange(from=0L) int minWidth, @IntRange(from=0L) int minSmallestWidth);
+ method public androidx.window.embedding.SplitPlaceholderRule build();
+ method public androidx.window.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(int finishPrimaryWithPlaceholder);
+ method public androidx.window.embedding.SplitPlaceholderRule.Builder setLayoutDir(int layoutDir);
+ method public androidx.window.embedding.SplitPlaceholderRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
+ method public androidx.window.embedding.SplitPlaceholderRule.Builder setSticky(boolean isSticky);
+ }
+
+ public class SplitRule extends androidx.window.embedding.EmbeddingRule {
+ method public final int getLayoutDirection();
+ method public final int getMinSmallestWidth();
+ method public final int getMinWidth();
+ method public final float getSplitRatio();
+ property public final int layoutDirection;
+ property public final int minSmallestWidth;
+ property public final int minWidth;
+ property public final float splitRatio;
+ field public static final androidx.window.embedding.SplitRule.Companion Companion;
+ field public static final int FINISH_ADJACENT = 2; // 0x2
+ field public static final int FINISH_ALWAYS = 1; // 0x1
+ field public static final int FINISH_NEVER = 0; // 0x0
+ }
+
+ public static final class SplitRule.Companion {
+ }
+
+}
+
package androidx.window.layout {
public interface DisplayFeature {
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/ActivityFilterTest.kt b/window/window/src/androidTest/java/androidx/window/embedding/ActivityFilterTest.kt
index 3aa1026..1d9d74c 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/ActivityFilterTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/ActivityFilterTest.kt
@@ -19,7 +19,6 @@
import android.app.Activity
import android.content.ComponentName
import android.content.Intent
-import androidx.window.core.ExperimentalWindowApi
import com.google.common.truth.Truth.assertWithMessage
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
@@ -31,7 +30,6 @@
/**
* Integration test for [ActivityFilter] to test construction from [ComponentName].
*/
-@OptIn(ExperimentalWindowApi::class)
class ActivityFilterTest {
private val intent = Intent()
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt
index 7de3ec5..07b2d1e 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt
@@ -23,7 +23,6 @@
import android.graphics.Rect
import android.util.LayoutDirection
import androidx.test.core.app.ApplicationProvider
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.embedding.SplitRule.Companion.FINISH_ADJACENT
import androidx.window.embedding.SplitRule.Companion.FINISH_ALWAYS
import androidx.window.embedding.SplitRule.Companion.FINISH_NEVER
@@ -40,7 +39,6 @@
* @see SplitRule
* @see ActivityRule
*/
-@OptIn(ExperimentalWindowApi::class)
class EmbeddingRuleConstructionTests {
/**
* Verifies that default params are set correctly when reading {@link SplitPairRule} from XML.
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/SplitPairFilterTest.kt b/window/window/src/androidTest/java/androidx/window/embedding/SplitPairFilterTest.kt
index 0ac37b0..408102b 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/SplitPairFilterTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/SplitPairFilterTest.kt
@@ -19,13 +19,11 @@
import android.app.Activity
import android.content.ComponentName
import android.content.Intent
-import androidx.window.core.ExperimentalWindowApi
import com.google.common.truth.Truth.assertWithMessage
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import org.junit.Test
-@OptIn(ExperimentalWindowApi::class)
class SplitPairFilterTest {
private val component1 = ComponentName("a.b.c", "a.b.c.TestActivity")
private val component2 = ComponentName("d.e.f", "d.e.f.TestActivity")
diff --git a/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowLayoutInfoBackendTest.kt b/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowLayoutInfoBackendTest.kt
index e7ca275..afef875 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowLayoutInfoBackendTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowLayoutInfoBackendTest.kt
@@ -16,6 +16,9 @@
package androidx.window.layout.adapter.extensions
+import androidx.window.extensions.layout.FoldingFeature as OEMFoldingFeature
+import androidx.window.extensions.layout.WindowLayoutInfo as OEMWindowLayoutInfo
+import java.util.function.Consumer as JavaConsumer
import android.annotation.SuppressLint
import android.app.Activity
import android.graphics.Rect
@@ -28,25 +31,23 @@
import androidx.window.extensions.layout.FoldingFeature.STATE_FLAT
import androidx.window.extensions.layout.FoldingFeature.TYPE_HINGE
import androidx.window.extensions.layout.WindowLayoutComponent
+import androidx.window.layout.WindowLayoutInfo
+import androidx.window.layout.WindowMetricsCalculatorCompat
import androidx.window.layout.adapter.extensions.ExtensionsWindowLayoutInfoAdapter.translate
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
-import androidx.window.extensions.layout.FoldingFeature as OEMFoldingFeature
-import androidx.window.extensions.layout.WindowLayoutInfo as OEMWindowLayoutInfo
-import java.util.function.Consumer as JavaConsumer
-import androidx.window.layout.WindowLayoutInfo
-import androidx.window.layout.WindowMetricsCalculatorCompat
-import com.nhaarman.mockitokotlin2.times
class ExtensionWindowLayoutInfoBackendTest {
@@ -149,6 +150,28 @@
val consumerCaptor = argumentCaptor<JavaConsumer<OEMWindowLayoutInfo>>()
verify(component).addWindowLayoutInfoListener(eq(activity), consumerCaptor.capture())
verify(component).removeWindowLayoutInfoListener(consumerCaptor.firstValue)
+ assertFalse(backend.hasRegisteredListeners())
+ }
+ }
+
+ @Test
+ public fun testExtensionWindowBackend_removesMultipleCallback() {
+ val component = mock<WindowLayoutComponent>()
+
+ val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
+
+ activityScenario.scenario.onActivity { activity ->
+ val consumer = TestConsumer<WindowLayoutInfo>()
+ val consumer2 = TestConsumer<WindowLayoutInfo>()
+ backend.registerLayoutChangeCallback(activity, Runnable::run, consumer)
+ backend.registerLayoutChangeCallback(activity, Runnable::run, consumer2)
+ backend.unregisterLayoutChangeCallback(consumer)
+ backend.unregisterLayoutChangeCallback(consumer2)
+
+ val consumerCaptor = argumentCaptor<JavaConsumer<OEMWindowLayoutInfo>>()
+ verify(component).addWindowLayoutInfoListener(eq(activity), consumerCaptor.capture())
+ verify(component).removeWindowLayoutInfoListener(consumerCaptor.firstValue)
+ assertFalse(backend.hasRegisteredListeners())
}
}
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt b/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt
index 9014a3d..54b6ebd 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt
@@ -20,7 +20,6 @@
import android.content.Intent
import android.util.Log
import androidx.window.core.ActivityComponentInfo
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.embedding.MatcherUtils.isActivityOrIntentMatching
import androidx.window.embedding.MatcherUtils.isIntentMatching
import androidx.window.embedding.MatcherUtils.sDebugMatchers
@@ -31,7 +30,6 @@
* end or instead of the package name, and a wildcard symbol in the end or instead of the class
* name.
*/
-@ExperimentalWindowApi
class ActivityFilter internal constructor(
/**
* Component name in the intent for the activity. Must be non-empty. Can contain a single
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityRule.kt b/window/window/src/main/java/androidx/window/embedding/ActivityRule.kt
index a76c5e6..45c928e 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityRule.kt
@@ -16,13 +16,10 @@
package androidx.window.embedding
-import androidx.window.core.ExperimentalWindowApi
-
/**
* Layout configuration rules for individual activities with split layouts. Take precedence over
* [SplitPairRule].
*/
-@ExperimentalWindowApi
class ActivityRule internal constructor(
/**
* Filters used to choose when to apply this rule. The rule may be used if any one of the
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt b/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
index 6ed10a5..676e78d 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
@@ -16,13 +16,11 @@
package androidx.window.embedding
import android.app.Activity
-import androidx.window.core.ExperimentalWindowApi
/**
* A container that holds a stack of activities, overlapping and bound to the same rectangle on the
* screen.
*/
-@ExperimentalWindowApi
class ActivityStack(
/**
* The [Activity] list in this application's process that belongs to this ActivityStack.
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
index 4116482..a845609 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
@@ -28,14 +28,12 @@
import androidx.window.extensions.embedding.SplitPairRule.Builder as SplitPairRuleBuilder
import androidx.window.extensions.embedding.SplitPlaceholderRule as OEMSplitPlaceholderRule
import androidx.window.extensions.embedding.SplitPlaceholderRule.Builder as SplitPlaceholderRuleBuilder
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.core.PredicateAdapter
import androidx.window.extensions.WindowExtensionsProvider
/**
* Adapter class that translates data classes between Extension and Jetpack interfaces.
*/
-@ExperimentalWindowApi
internal class EmbeddingAdapter(
private val predicateAdapter: PredicateAdapter
) {
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
index 9e420d5..cdb1f91 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
@@ -18,11 +18,9 @@
import android.app.Activity
import androidx.core.util.Consumer
-import androidx.window.core.ExperimentalWindowApi
import java.util.concurrent.Executor
// TODO(b/191164045): Move to window-testing or adapt for testing otherwise.
-@ExperimentalWindowApi
internal interface EmbeddingBackend {
fun setSplitRules(rules: Set<EmbeddingRule>)
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
index 5e5293e..85993d6 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
@@ -16,21 +16,19 @@
package androidx.window.embedding
+import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
import android.app.Activity
import android.util.Log
import androidx.window.core.ConsumerAdapter
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.embedding.EmbeddingInterfaceCompat.EmbeddingCallbackInterface
import androidx.window.extensions.WindowExtensionsProvider
import androidx.window.extensions.embedding.ActivityEmbeddingComponent
import java.lang.reflect.Proxy
-import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
/**
* Adapter implementation for different historical versions of activity embedding OEM interface in
* [ActivityEmbeddingComponent]. Only supports the single current version in this implementation.
*/
-@ExperimentalWindowApi
internal class EmbeddingCompat constructor(
private val embeddingExtension: ActivityEmbeddingComponent,
private val adapter: EmbeddingAdapter,
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
index 33f8f7c..7c0c08c 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
@@ -17,14 +17,12 @@
package androidx.window.embedding
import android.app.Activity
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.extensions.embedding.ActivityEmbeddingComponent
/**
* Adapter interface for different historical versions of activity embedding OEM interface in
* [ActivityEmbeddingComponent].
*/
-@ExperimentalWindowApi
internal interface EmbeddingInterfaceCompat {
fun setSplitRules(rules: Set<EmbeddingRule>)
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingRule.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingRule.kt
index 6347b3a..e3ff7dd 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingRule.kt
@@ -16,11 +16,8 @@
package androidx.window.embedding
-import androidx.window.core.ExperimentalWindowApi
-
/**
* Base abstract class for activity embedding presentation rules, such as [SplitPairRule] and
* [ActivityRule]. Allows grouping different rule types together when updating.
*/
-@ExperimentalWindowApi
-abstract class EmbeddingRule internal constructor()
\ No newline at end of file
+abstract class EmbeddingRule internal constructor()
diff --git a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
index fe412cf..5c2d3c1 100644
--- a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
@@ -22,7 +22,6 @@
import androidx.annotation.VisibleForTesting
import androidx.core.util.Consumer
import androidx.window.core.ConsumerAdapter
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.core.ExtensionsUtil
import androidx.window.core.PredicateAdapter
import androidx.window.embedding.EmbeddingInterfaceCompat.EmbeddingCallbackInterface
@@ -32,7 +31,6 @@
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
-@ExperimentalWindowApi
internal class ExtensionEmbeddingBackend @VisibleForTesting constructor(
@field:VisibleForTesting @field:GuardedBy(
"globalLock"
diff --git a/window/window/src/main/java/androidx/window/embedding/MatcherUtils.kt b/window/window/src/main/java/androidx/window/embedding/MatcherUtils.kt
index 71cace1..eb61516 100644
--- a/window/window/src/main/java/androidx/window/embedding/MatcherUtils.kt
+++ b/window/window/src/main/java/androidx/window/embedding/MatcherUtils.kt
@@ -20,12 +20,10 @@
import android.content.Intent
import android.util.Log
import androidx.window.core.ActivityComponentInfo
-import androidx.window.core.ExperimentalWindowApi
/**
* Internal utils used for matching activities with embedding rules.
*/
-@ExperimentalWindowApi
internal object MatcherUtils {
/** Checks component match allowing wildcard patterns. */
internal fun areComponentsMatching(
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitController.kt b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
index cf5aa4a..5d45231 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitController.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
@@ -19,7 +19,7 @@
import android.app.Activity
import android.content.Context
import androidx.core.util.Consumer
-import androidx.window.core.ExperimentalWindowApi
+import androidx.window.embedding.SplitController.Companion
import java.util.concurrent.Executor
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
@@ -36,7 +36,6 @@
* [androidx.startup.Initializer] and [Companion.initialize]. See Jetpack App Startup reference
* for more information.
*/
-@ExperimentalWindowApi
class SplitController private constructor() {
private val embeddingBackend: EmbeddingBackend = ExtensionEmbeddingBackend.getInstance()
private var staticSplitRules: Set<EmbeddingRule> = emptySet()
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt b/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
index b113a62..7606d64 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
@@ -17,10 +17,8 @@
package androidx.window.embedding
import android.app.Activity
-import androidx.window.core.ExperimentalWindowApi
/** Describes a split pair of two containers with activities. */
-@ExperimentalWindowApi
class SplitInfo internal constructor(
val primaryActivityStack: ActivityStack,
val secondaryActivityStack: ActivityStack,
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt b/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt
index 4e09377..95e1625 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt
@@ -20,7 +20,6 @@
import android.content.Intent
import android.util.Log
import androidx.window.core.ActivityComponentInfo
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.embedding.MatcherUtils.areComponentsMatching
import androidx.window.embedding.MatcherUtils.isIntentMatching
import androidx.window.embedding.MatcherUtils.sDebugMatchers
@@ -30,7 +29,6 @@
* Filter used to find if a pair of activities should be put in a split. Applied to the base /
* primary activity and an intent starting a secondary activity.
*/
-@ExperimentalWindowApi
class SplitPairFilter(
/**
* Component name of the primary activity in the split. Must be non-empty. Can contain a single
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
index 49e6681..de62416 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
@@ -21,7 +21,6 @@
import androidx.annotation.IntRange
import androidx.core.util.Preconditions.checkArgument
import androidx.core.util.Preconditions.checkArgumentNonnegative
-import androidx.window.core.ExperimentalWindowApi
/**
* Split configuration rules for activity pairs. Define when activities that were launched on top of
@@ -31,7 +30,6 @@
* belong to the same application and are running in the same process. The rules are always
* applied only to activities that will be started after the rules were set.
*/
-@ExperimentalWindowApi
class SplitPairRule : SplitRule {
/**
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
index c12b7b7..ae4b432 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
@@ -23,12 +23,10 @@
import androidx.annotation.IntRange
import androidx.core.util.Preconditions.checkArgument
import androidx.core.util.Preconditions.checkArgumentNonnegative
-import androidx.window.core.ExperimentalWindowApi
/**
* Configuration rules for split placeholders.
*/
-@ExperimentalWindowApi
class SplitPlaceholderRule : SplitRule {
/**
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
index 50916c7..0bda0a0 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
@@ -25,7 +25,6 @@
import androidx.annotation.IntDef
import androidx.annotation.IntRange
import androidx.annotation.RequiresApi
-import androidx.window.core.ExperimentalWindowApi
import kotlin.math.min
/**
@@ -36,7 +35,6 @@
* belong to the same application and are running in the same process. The rules are always
* applied only to activities that will be started after the rules were set.
*/
-@ExperimentalWindowApi
open class SplitRule internal constructor(
/**
* The smallest value of width of the parent window when the split should be used, in pixels.
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitRuleParser.kt b/window/window/src/main/java/androidx/window/embedding/SplitRuleParser.kt
index 8e086c9..b9b8ea1 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitRuleParser.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitRuleParser.kt
@@ -24,7 +24,6 @@
import android.util.LayoutDirection
import androidx.window.R
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.embedding.SplitRule.Companion.FINISH_ALWAYS
import androidx.window.embedding.SplitRule.Companion.FINISH_NEVER
@@ -33,7 +32,6 @@
/**
* Parses the static split rules defined in XML.
*/
-@ExperimentalWindowApi
internal class SplitRuleParser {
internal fun parseSplitRules(context: Context, staticRuleResourceId: Int): Set<EmbeddingRule>? {
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowLayoutInfoBackend.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowLayoutInfoBackend.kt
index beebba0..2c304b4 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowLayoutInfoBackend.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowLayoutInfoBackend.kt
@@ -19,6 +19,7 @@
import androidx.window.extensions.layout.WindowLayoutInfo as OEMWindowLayoutInfo
import android.app.Activity
import androidx.annotation.GuardedBy
+import androidx.annotation.VisibleForTesting
import androidx.core.util.Consumer
import androidx.window.core.ConsumerAdapter
import androidx.window.extensions.layout.WindowLayoutComponent
@@ -93,15 +94,24 @@
val activity = listenerToActivity[callback] ?: return
val multicastListener = activityToListeners[activity] ?: return
multicastListener.removeListener(callback)
+ listenerToActivity.remove(callback)
if (multicastListener.isEmpty()) {
consumerToToken.remove(multicastListener)?.dispose()
- listenerToActivity.remove(callback)
activityToListeners.remove(activity)
}
}
}
/**
+ * Returns {@code true} there is any registered listener information, {@code false} otherwise.
+ */
+ @VisibleForTesting
+ fun hasRegisteredListeners(): Boolean {
+ return !(activityToListeners.isEmpty() && listenerToActivity.isEmpty() &&
+ consumerToToken.isEmpty())
+ }
+
+ /**
* A class that implements [Consumer] by aggregating multiple instances of [Consumer]
* and multicasting each value that is consumed. [MulticastConsumer] also replays the last known
* value whenever a new consumer registers.
diff --git a/window/window/src/test/java/androidx/window/embedding/ActivityFilterTest.kt b/window/window/src/test/java/androidx/window/embedding/ActivityFilterTest.kt
index c54e6f3..6d06d87 100644
--- a/window/window/src/test/java/androidx/window/embedding/ActivityFilterTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/ActivityFilterTest.kt
@@ -18,7 +18,6 @@
import android.app.Activity
import androidx.window.core.ActivityComponentInfo
-import androidx.window.core.ExperimentalWindowApi
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@@ -27,7 +26,6 @@
/**
* The unit tests for [ActivityFilter] that will test construction.
*/
-@OptIn(ExperimentalWindowApi::class)
class ActivityFilterTest {
@Test(expected = IllegalArgumentException::class)
diff --git a/window/window/src/test/java/androidx/window/embedding/ActivityRuleTest.kt b/window/window/src/test/java/androidx/window/embedding/ActivityRuleTest.kt
index beb20e3..aa266a3 100644
--- a/window/window/src/test/java/androidx/window/embedding/ActivityRuleTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/ActivityRuleTest.kt
@@ -17,13 +17,11 @@
package androidx.window.embedding
import androidx.window.core.ActivityComponentInfo
-import androidx.window.core.ExperimentalWindowApi
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
-@OptIn(ExperimentalWindowApi::class)
class ActivityRuleTest {
@Test
diff --git a/window/window/src/test/java/androidx/window/embedding/ActivityStackTest.kt b/window/window/src/test/java/androidx/window/embedding/ActivityStackTest.kt
index 2ef71b1..1af074c 100644
--- a/window/window/src/test/java/androidx/window/embedding/ActivityStackTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/ActivityStackTest.kt
@@ -17,13 +17,11 @@
package androidx.window.embedding
import android.app.Activity
-import androidx.window.core.ExperimentalWindowApi
-import org.mockito.kotlin.mock
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
+import org.mockito.kotlin.mock
-@OptIn(ExperimentalWindowApi::class)
class ActivityStackTest {
@Test
diff --git a/window/window/src/test/java/androidx/window/embedding/MatcherUtilsTest.kt b/window/window/src/test/java/androidx/window/embedding/MatcherUtilsTest.kt
index 325bcf4..085a9d0 100644
--- a/window/window/src/test/java/androidx/window/embedding/MatcherUtilsTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/MatcherUtilsTest.kt
@@ -17,7 +17,6 @@
package androidx.window.embedding
import androidx.window.core.ActivityComponentInfo
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.embedding.MatcherUtils.areComponentsMatching
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@@ -26,7 +25,6 @@
/**
* Unit tests for [MatcherUtils].
*/
-@OptIn(ExperimentalWindowApi::class)
class MatcherUtilsTest {
@Test
diff --git a/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt b/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt
index e642d25..8f15b16 100644
--- a/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt
@@ -17,13 +17,11 @@
package androidx.window.embedding
import android.app.Activity
-import androidx.window.core.ExperimentalWindowApi
-import org.mockito.kotlin.mock
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
+import org.mockito.kotlin.mock
-@OptIn(ExperimentalWindowApi::class)
class SplitInfoTest {
@Test
diff --git a/work/work-runtime/build.gradle b/work/work-runtime/build.gradle
index ca651ca..83a47ba 100644
--- a/work/work-runtime/build.gradle
+++ b/work/work-runtime/build.gradle
@@ -80,6 +80,7 @@
androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has its own MockMaker
androidTestImplementation(project(":internal-testutils-runtime"))
testImplementation(libs.junit)
+ testImplementation(libs.truth)
lintPublish(project(":work:work-runtime-lint"))
}
diff --git a/work/work-runtime/src/main/java/androidx/work/InputMerger.java b/work/work-runtime/src/main/java/androidx/work/InputMerger.java
index a59805c..695e865 100644
--- a/work/work-runtime/src/main/java/androidx/work/InputMerger.java
+++ b/work/work-runtime/src/main/java/androidx/work/InputMerger.java
@@ -62,7 +62,7 @@
public static InputMerger fromClassName(@NonNull String className) {
try {
Class<?> clazz = Class.forName(className);
- return (InputMerger) clazz.newInstance();
+ return (InputMerger) clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
Logger.get().error(TAG, "Trouble instantiating + " + className, e);
}
diff --git a/work/work-runtime/src/main/java/androidx/work/WorkInfo.java b/work/work-runtime/src/main/java/androidx/work/WorkInfo.java
index 19a9821..7560af3 100644
--- a/work/work-runtime/src/main/java/androidx/work/WorkInfo.java
+++ b/work/work-runtime/src/main/java/androidx/work/WorkInfo.java
@@ -145,6 +145,7 @@
WorkInfo workInfo = (WorkInfo) o;
if (mRunAttemptCount != workInfo.mRunAttemptCount) return false;
+ if (mGeneration != workInfo.mGeneration) return false;
if (!mId.equals(workInfo.mId)) return false;
if (mState != workInfo.mState) return false;
if (!mOutputData.equals(workInfo.mOutputData)) return false;
@@ -160,6 +161,7 @@
result = 31 * result + mTags.hashCode();
result = 31 * result + mProgress.hashCode();
result = 31 * result + mRunAttemptCount;
+ result = 31 * result + mGeneration;
return result;
}
diff --git a/work/work-runtime/src/test/java/androidx/work/WorkInfoTest.kt b/work/work-runtime/src/test/java/androidx/work/WorkInfoTest.kt
new file mode 100644
index 0000000..c45eb03
--- /dev/null
+++ b/work/work-runtime/src/test/java/androidx/work/WorkInfoTest.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.work
+
+import com.google.common.truth.Truth.assertThat
+import java.util.UUID
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class WorkInfoTest {
+
+ @Test
+ fun testEqualityWithGeneration() {
+ val id = UUID.randomUUID()
+ val info1 = WorkInfo(id, WorkInfo.State.RUNNING, Data.EMPTY, listOf("a"), Data.EMPTY, 0, 1)
+ val info2 = WorkInfo(id, WorkInfo.State.RUNNING, Data.EMPTY, listOf("a"), Data.EMPTY, 0, 4)
+ assertThat(info1 == info2).isFalse()
+ }
+}
\ No newline at end of file